diff --git a/.gitignore b/.gitignore index c98f27fe..b2106bba 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,11 @@ xcuserdata *.xcuserstate *.xcscmblueprint +# Swift Package Manager +.build/ +.swiftpm/ +Package.resolved + ## Obj-C/Swift specific *.hmap *.ipa diff --git a/.jazzy.yaml b/.jazzy.yaml deleted file mode 100644 index 2486f9a7..00000000 --- a/.jazzy.yaml +++ /dev/null @@ -1,60 +0,0 @@ -xcodebuild_arguments: - - -scheme - - InstantSearch -output: docs/reference -# Avoid putting the DocSet within the HTML docs. -# WARNING: The path is relative to the output directory. -docset_path: ../../build/docset -hide_documentation_coverage: true -clean: true -abstract: - - LICENSE -readme: docgen/jazzy/index.md -custom_categories: - - name: Core - children: - - InstantSearch - - name: Widgets - children: - - ActivityIndicatorWidget - - DatePickerWidget - - HitsCollectionWidget - - OneValueSwitchWidget - - RefinementCollectionWidget - - RefinementTableWidget - - SearchBarWidget - - SegmentedControlWidget - - SliderWidget - - StatsButtonWidget - - StatsLabelWidget - - StepperWidget - - SwitchWidget - - TextFieldWidget - - TwoValuesSwitchWidget - - name: Base ViewController - children: - - HitsCollectionViewController - - HitsTableViewController - - RefinementCollectionViewController - - RefinementTableViewController - - name: Controllers - children: - - HitsController - - RefinementController - - name: Protocols - children: - - AlgoliaWidget - - HitsCollectionViewDataSource - - HitsCollectionViewDelegate - - HitsTableViewDataSource - - HitsTableViewDelegate - - RefinableDelegate - - RefinementCollectionViewDataSource - - RefinementCollectionViewDelegate - - RefinementTableViewDataSource - - RefinementTableViewDelegate - - ResultingDelegate - - SearchableViewModel - - name: Helpers - children: - - UILabel diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..b6a65719 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,15 @@ +excluded: + - .build + - ./Tests +# implicitly +line_length: 300 +identifier_name: + excluded: + - on + - or + - id +disabled_rules: + - todo + - implicit_getter +nesting: + type_level: 2 diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/InstantSearch.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/InstantSearch.xcscheme deleted file mode 100644 index b23cab7c..00000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/InstantSearch.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dd52f449..00000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -branches: - only: - - master -osx_image: xcode9 -language: objective-c -env: - matrix: - - FASTLANE_TARGET="ios" -install: true -before_script: - - gem update fastlane -script: - - set -o pipefail - - fastlane ${FASTLANE_TARGET} test diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index a323b16e..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,31 +0,0 @@ -Change Log -========== -## 2.1.0 (2017-12-12) - -- Multi-Index functionality added to all widgets with the introduction of the index and variant field. -- Adding 2 new widgets: `MultiHitsTableWidget` and `MultiHitsCollectionWidget` that can handle multiple indices in different sections. -- Introduction of 6 ViewModels that encapsulate the business logic of the widgets. You can use them to easily access the search state for better customization. -`SearchViewModel`, `HitsViewModel`, `MultiHitsViewModel`, `NumericControlViewModel`, `FacetControlViewModel`, `RefinementMenuViewModel`. - -## 2.0.0 (2017-10-01) - -### Swift Version - -- Add support for Swift 4 - -## 1.0.1 (2017-07-31) - -### Dependency Managers - -- Add support for Carthage - -## 1.0.0 (2017-07-17) - -**First official release of InstantSearch iOS!** - -### Features - -- 15 customizable widgets to use in your apps. Checkout the documentation of those widgets in the [community website](https://community.algolia.com/instantsearch-ios/widgets.html). -- 4 base controllers: `HitsTableViewController`, `RefinementTableViewController`, `HitsCollectionViewController`, `RefinementCollectionViewController`. -- Custom widget creation. [Follow documentation](https://community.algolia.com/instantsearch-ios/widgets.html#custom-widgets). -- Getting Started Guide. [Follow guide](https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/). diff --git a/Cartfile b/Cartfile index 2e7624bb..ce040c27 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,3 @@ -github "algolia/instantsearch-core-swift" ~> 6.5 -github "apple/swift-log" ~> 1.2 +github "algolia/algoliasearch-client-swift" ~> 8.0 +github "algolia/instantsearch-ios-insights" ~> 2.3.1 +github "apple/swift-log" ~> 1.3 diff --git a/Cartfile.resolved b/Cartfile.resolved deleted file mode 100644 index c17ab64b..00000000 --- a/Cartfile.resolved +++ /dev/null @@ -1,3 +0,0 @@ -github "algolia/algoliasearch-client-swift" "7.0.3" -github "algolia/instantsearch-core-swift" "6.5.0" -github "algolia/instantsearch-ios-insights" "2.3.2" diff --git a/Gemfile b/Gemfile index 4e0aacc0..ed964db6 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,6 @@ source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } -gem 'cocoapods', '~> 1.8.0' -gem 'fastlane', '~> 2.141.0' +gem 'cocoapods', '~> 1.9' +gem 'fastlane', '~> 2.151' gem 'xcov' -gem 'jazzy' diff --git a/Gemfile.lock b/Gemfile.lock index cbf30bf9..9193cbb5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,23 +2,39 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (3.0.2) - activesupport (4.2.11.1) + activesupport (4.2.11.3) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) - algoliasearch (1.27.1) + algoliasearch (1.27.3) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) + aws-eventstream (1.1.0) + aws-partitions (1.341.0) + aws-sdk-core (3.103.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.36.0) + aws-sdk-core (~> 3, >= 3.99.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.74.0) + aws-sdk-core (~> 3, >= 3.102.1) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.2.1) + aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.3) claide (1.0.3) - cocoapods (1.8.4) + cocoapods (1.9.3) activesupport (>= 4.0.2, < 5) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.8.4) + cocoapods-core (= 1.9.3) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 1.2.2, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -33,71 +49,75 @@ GEM molinillo (~> 0.6.6) nap (~> 1.0) ruby-macho (~> 1.4) - xcodeproj (>= 1.11.1, < 2.0) - cocoapods-core (1.8.4) + xcodeproj (>= 1.14.0, < 2.0) + cocoapods-core (1.9.3) activesupport (>= 4.0.2, < 6) algoliasearch (~> 1.0) concurrent-ruby (~> 1.1) fuzzy_match (~> 2.0.4) nap (~> 1.0) + netrc (~> 0.11) + typhoeus (~> 1.0) cocoapods-deintegrate (1.0.4) cocoapods-downloader (1.3.0) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.0) cocoapods-stats (1.1.0) - cocoapods-trunk (1.4.1) + cocoapods-trunk (1.5.0) nap (>= 0.8, < 2.0) netrc (~> 0.11) - cocoapods-try (1.1.0) + cocoapods-try (1.2.0) colored (1.2) colored2 (3.1.2) commander-fastlane (4.4.6) highline (~> 1.7.2) - concurrent-ruby (1.1.5) - declarative (0.0.10) + concurrent-ruby (1.1.6) + declarative (0.0.20) declarative-option (0.1.0) - digest-crc (0.4.1) + digest-crc (0.6.1) + rake (~> 13.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.5) - emoji_regex (1.0.1) + emoji_regex (3.0.0) escape (0.0.4) - excon (0.72.0) - faraday (0.17.3) + ethon (0.12.0) + ffi (>= 1.3.0) + excon (0.75.0) + faraday (1.0.1) multipart-post (>= 1.2, < 3) faraday-cookie_jar (0.0.6) faraday (>= 0.7.4) http-cookie (~> 1.0.0) - faraday_middleware (0.13.1) - faraday (>= 0.7.4, < 1.0) + faraday_middleware (1.0.0) + faraday (~> 1.0) fastimage (2.1.7) - fastlane (2.141.0) + fastlane (2.151.2) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) - babosa (>= 1.0.2, < 2.0.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) colored commander-fastlane (>= 4.4.6, < 5.0.0) dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 2.0) + emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 1.0.0) - faraday (~> 0.17) + faraday (>= 0.17, < 2.0) faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 0.13.1) + faraday_middleware (>= 0.13.1, < 2.0) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-api-client (>= 0.29.2, < 0.37.0) + google-api-client (>= 0.37.0, < 0.39.0) google-cloud-storage (>= 1.15.0, < 2.0.0) highline (>= 1.7.2, < 2.0.0) json (< 3.0.0) - jwt (~> 2.1.0) + jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) - multi_xml (~> 0.5) multipart-post (~> 2.0.0) plist (>= 3.1.0, < 4.0.0) - public_suffix (~> 2.0.0) - rubyzip (>= 1.3.0, < 2.0.0) + rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) simctl (~> 1.6.3) slack-notifier (>= 2.0.0, < 3.0.0) @@ -109,11 +129,11 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - ffi (1.12.1) + ffi (1.13.1) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-api-client (0.36.4) + google-api-client (0.38.0) addressable (~> 2.5, >= 2.5.1) googleauth (~> 0.9) httpclient (>= 2.8.1, < 3.0) @@ -124,59 +144,47 @@ GEM google-cloud-core (1.5.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.3.0) - faraday (~> 0.11) - google-cloud-errors (1.0.0) - google-cloud-storage (1.25.1) + google-cloud-env (1.3.2) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.0.1) + google-cloud-storage (1.26.2) addressable (~> 2.5) digest-crc (~> 0.4) google-api-client (~> 0.33) google-cloud-core (~> 1.2) googleauth (~> 0.9) mini_mime (~> 1.0) - googleauth (0.10.0) - faraday (~> 0.12) + googleauth (0.13.0) + faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.12) + signet (~> 0.14) highline (1.7.10) http-cookie (1.0.3) domain_name (~> 0.5) httpclient (2.8.3) i18n (0.9.5) concurrent-ruby (~> 1.0) - jazzy (0.13.1) - cocoapods (~> 1.5) - mustache (~> 1.1) - open4 - redcarpet (~> 3.4) - rouge (>= 2.0.6, < 4.0) - sassc (~> 2.1) - sqlite3 (~> 1.3) - xcinvoke (~> 0.3.0) - json (2.3.0) - jwt (2.1.0) - liferaft (0.0.6) + jmespath (1.4.0) + json (2.3.1) + jwt (2.2.1) memoist (0.16.2) mini_magick (4.10.1) mini_mime (1.0.2) - minitest (5.14.0) + minitest (5.14.1) molinillo (0.6.6) - multi_json (1.14.1) - multi_xml (0.6.0) + multi_json (1.15.0) multipart-post (2.0.0) - mustache (1.1.1) nanaimo (0.2.6) nap (1.1.0) naturally (2.2.0) netrc (0.11.0) - open4 (1.3.4) - os (1.0.1) + os (1.1.0) plist (3.5.0) - public_suffix (2.0.5) - redcarpet (3.5.0) + public_suffix (4.0.5) + rake (13.0.1) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) @@ -184,45 +192,42 @@ GEM retriable (3.1.2) rouge (2.0.7) ruby-macho (1.4.0) - rubyzip (1.3.0) - sassc (2.2.1) - ffi (~> 1.9) + rubyzip (2.3.0) security (0.1.3) - signet (0.12.0) + signet (0.14.0) addressable (~> 2.3) - faraday (~> 0.9) + faraday (>= 0.17.3, < 2.0) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simctl (1.6.7) + simctl (1.6.8) CFPropertyList naturally slack-notifier (2.3.2) - sqlite3 (1.4.2) terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) thread_safe (0.3.6) tty-cursor (0.7.1) - tty-screen (0.7.0) + tty-screen (0.8.0) tty-spinner (0.9.3) tty-cursor (~> 0.7) - tzinfo (1.2.6) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (1.2.7) thread_safe (~> 0.1) uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.6) - unicode-display_width (1.6.1) + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) word_wrap (1.0.0) - xcinvoke (0.3.0) - liferaft (~> 0.0.6) - xcodeproj (1.14.0) + xcodeproj (1.17.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.2.6) - xcov (1.7.2) + xcov (1.7.3) fastlane (>= 2.141.0, < 3.0.0) multipart-post slack-notifier @@ -233,15 +238,14 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.0) xcpretty (~> 0.2, >= 0.0.7) - xcresult (0.2.0) + xcresult (0.2.1) PLATFORMS ruby DEPENDENCIES - cocoapods (~> 1.8.0) - fastlane (~> 2.141.0) - jazzy + cocoapods (~> 1.9) + fastlane (~> 2.151) xcov BUNDLED WITH diff --git a/InstantSearch.podspec b/InstantSearch.podspec index 1d19d7ad..8a1f95e9 100644 --- a/InstantSearch.podspec +++ b/InstantSearch.podspec @@ -1,34 +1,37 @@ Pod::Spec.new do |s| - s.name = "InstantSearch" - s.module_name = 'InstantSearch' - s.version = "5.2.3" - s.summary = "A library of widgets and helpers to build instant-search applications on iOS." - s.homepage = "https://github.com/algolia/instantsearch-ios" - s.license = { type: 'Apache 2.0', file: 'LICENSE.md' } - s.author = { "Algolia" => "contact@algolia.com" } - s.source = { git: "https://github.com/algolia/instantsearch-ios.git", tag: s.version.to_s } - s.social_media_url = 'https://twitter.com/algolia' - s.ios.deployment_target = '8.0' - s.requires_arc = true - s.default_subspec = "UI" - s.swift_version = '5.0' - s.swift_versions = ['4.0', '4.2', '5.0'] + s.name = 'InstantSearch' + s.version = '7.0.0' + s.platforms = { :ios => "8.0", :osx => "10.10", :watchos => "3.0", :tvos => "9.0" } - s.subspec "UI" do |ss| - ss.source_files = 'Sources/**/*.{swift}' - ss.dependency 'InstantSearchCore', '~> 6.5' - end + s.license = { type: 'Apache 2.0', file: 'LICENSE.md' } + s.summary = 'A library of widgets and helpers to build instant-search applications on iOS.' + s.homepage = 'https://github.com/algolia/instantsearch-ios' + s.author = { "Algolia" => "contact@algolia.com" } + s.source = { :git => 'https://github.com/algolia/instantsearch-ios.git', :tag => s.version } - s.subspec "Core" do |ss| - ss.dependency 'InstantSearchCore', '~> 6.5' - end - - s.subspec "Client" do |ss| - ss.dependency 'InstantSearchClient', '~> 7.0' - end - - # Dependencies - # ------------ - s.dependency 'InstantSearchCore', '~> 6.5' - s.dependency 'Logging' + s.swift_version = "5.1" + + s.default_subspec = 'UI' + + s.subspec "Core" do |ss| + ss.source_files = 'Sources/InstantSearchCore/**/*.{swift}' + ss.dependency 'AlgoliaSearchClient', '~> 8.0.0' + ss.dependency 'InstantSearchInsights', '~> 2.3' + ss.ios.deployment_target = '8.0' + ss.osx.deployment_target = '10.10' + ss.watchos.deployment_target = '3.0' + ss.tvos.deployment_target = '9.0' + end + + s.subspec "UI" do |ss| + ss.source_files = 'Sources/InstantSearch/**/*.{swift}' + ss.dependency 'InstantSearch/Core' + ss.ios.deployment_target = '8.0' + ss.osx.deployment_target = '10.10' + ss.watchos.deployment_target = '3.0' + ss.tvos.deployment_target = '9.0' + ss.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-DInstantSearchCocoaPods' } + end + + end diff --git a/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 57914328..00000000 --- a/InstantSearch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,43 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "InstantSearchClient", - "repositoryURL": "https://github.com/algolia/algoliasearch-client-swift", - "state": { - "branch": null, - "revision": "ef949269c6119a69ed574edb8ccecd67b2e1cf38", - "version": "7.0.3" - } - }, - { - "package": "InstantSearchCore", - "repositoryURL": "https://github.com/algolia/instantsearch-core-swift", - "state": { - "branch": null, - "revision": "b5c58a8878c03c5c3ac3ed92c630e7067bc85abb", - "version": "6.5.1" - } - }, - { - "package": "InstantSearchInsights", - "repositoryURL": "https://github.com/algolia/instantsearch-ios-insights", - "state": { - "branch": null, - "revision": "1d2f3462b36980c86fdf86529520fc655293b6e4", - "version": "2.3.2" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log", - "state": { - "branch": null, - "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", - "version": "1.2.0" - } - } - ] - }, - "version": 1 -} diff --git a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearch.xcscheme b/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearch.xcscheme deleted file mode 100644 index f39c0902..00000000 --- a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearch.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTests.xcscheme b/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTests.xcscheme deleted file mode 100644 index 9d321c44..00000000 --- a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTests.xcscheme +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTestsHost.xcscheme b/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTestsHost.xcscheme deleted file mode 100644 index bf7d40e4..00000000 --- a/InstantSearch.xcodeproj/xcshareddata/xcschemes/InstantSearchTestsHost.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InstantSearchTestsHost/AppDelegate.swift b/InstantSearchTestsHost/AppDelegate.swift deleted file mode 100644 index a57ca745..00000000 --- a/InstantSearchTestsHost/AppDelegate.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// AppDelegate.swift -// InstantSearchTestsHost -// -// Created by Guy Daher on 22/05/2017. -// -// - -import UIKit - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - -} - diff --git a/InstantSearchTestsHost/Assets.xcassets/AppIcon.appiconset/Contents.json b/InstantSearchTestsHost/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d8db8d65..00000000 --- a/InstantSearchTestsHost/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/InstantSearchTestsHost/Base.lproj/LaunchScreen.storyboard b/InstantSearchTestsHost/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index fdf3f97d..00000000 --- a/InstantSearchTestsHost/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InstantSearchTestsHost/Base.lproj/Main.storyboard b/InstantSearchTestsHost/Base.lproj/Main.storyboard deleted file mode 100644 index 273375fc..00000000 --- a/InstantSearchTestsHost/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InstantSearchTestsHost/Info.plist b/InstantSearchTestsHost/Info.plist deleted file mode 100644 index 1c80ee11..00000000 --- a/InstantSearchTestsHost/Info.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 5.2.3 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/InstantSearchTestsHost/ViewController.swift b/InstantSearchTestsHost/ViewController.swift deleted file mode 100644 index a42a40e8..00000000 --- a/InstantSearchTestsHost/ViewController.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ViewController.swift -// InstantSearchTestsHost -// -// Created by Guy Daher on 22/05/2017. -// -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - -} - diff --git a/LICENSE.md b/LICENSE.md index 0fa1803a..8b1b30c6 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,203 +1,201 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2019 Algolia - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019 Algolia + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index eece6c2b..00000000 --- a/Package.resolved +++ /dev/null @@ -1,43 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "InstantSearchClient", - "repositoryURL": "https://github.com/algolia/algoliasearch-client-swift", - "state": { - "branch": null, - "revision": "ef949269c6119a69ed574edb8ccecd67b2e1cf38", - "version": "7.0.3" - } - }, - { - "package": "InstantSearchCore", - "repositoryURL": "https://github.com/algolia/instantsearch-core-swift", - "state": { - "branch": null, - "revision": "b5c58a8878c03c5c3ac3ed92c630e7067bc85abb", - "version": "6.5.1" - } - }, - { - "package": "InstantSearchInsights", - "repositoryURL": "https://github.com/algolia/instantsearch-ios-insights", - "state": { - "branch": null, - "revision": "1d2f3462b36980c86fdf86529520fc655293b6e4", - "version": "2.3.2" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log.git", - "state": { - "branch": null, - "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", - "version": "1.2.0" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index 842eabae..d7a7ad32 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,29 +6,35 @@ import PackageDescription let package = Package( name: "InstantSearch", platforms: [ - .iOS(SupportedPlatform.IOSVersion.v8), + .iOS(.v8), + .macOS(.v10_10), + .watchOS(.v2), + .tvOS(.v9) ], products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "InstantSearch", - targets: ["InstantSearch"]), + targets: ["InstantSearch", "InstantSearchCore"]), + .library( + name: "InstantSearchCore", + targets: ["InstantSearchCore"]) ], dependencies: [ - // Dependencies declare other packages that this package depends on. - .package(url:"https://github.com/algolia/instantsearch-core-swift", from: "6.5.1"), - .package(url:"https://github.com/apple/swift-log.git", from: "1.2.0"), + .package(name: "AlgoliaSearchClient", url:"https://github.com/algolia/algoliasearch-client-swift", from: "8.0.0"), + .package(name: "InstantSearchInsights", url:"https://github.com/algolia/instantsearch-ios-insights", from: "2.3.2") ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. + .target( + name: "InstantSearchCore", + dependencies: ["AlgoliaSearchClient", "InstantSearchInsights"]), .target( name: "InstantSearch", - dependencies: ["InstantSearchCore", "Logging"], - path: "./Sources"), + dependencies: ["InstantSearchCore"]), + .testTarget( + name: "InstantSearchCoreTests", + dependencies: ["InstantSearchCore", "AlgoliaSearchClient", "InstantSearchInsights"]), .testTarget( name: "InstantSearchTests", - dependencies: ["InstantSearch", "InstantSearchCore", "Logging"], - path: "./Tests") + dependencies: ["InstantSearch"]) ] ) diff --git a/Readme.md b/Readme.md index 43560515..7c3e9543 100644 --- a/Readme.md +++ b/Readme.md @@ -1,12 +1,11 @@ -![InstantSearch iOS](./docgen/assets/img/instantsearch-banner.png) - -

-Platform iOS -Swift 4 compatible -Objective-C compatible -CocoaPods compatible -License: MIT -

+![InstantSearch iOS](./Resources/instantsearch-banner.png) + +[![Pod Version](http://img.shields.io/cocoapods/v/InstantSearch.svg?style=flat)](https://github.com/algolia/instantsearch-ios/) +[![Pod Platform](http://img.shields.io/cocoapods/p/InstantSearch.svg?style=flat)](https://github.com/algolia/instantsearch-ios/) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg)](https://github.com/algolia/instantsearch-ios/) +[![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://swift.org/package-manager/) +[![Mac Catalyst compatible](https://img.shields.io/badge/Catalyst-compatible-brightgreen.svg)](https://developer.apple.com/documentation/xcode/creating_a_mac_version_of_your_ipad_app/) +[![Licence](http://img.shields.io/cocoapods/l/InstantSearch.svg?style=flat)](https://opensource.org/licenses/Apache-2.0) By [Algolia](http://algolia.com). @@ -14,42 +13,47 @@ InstantSearch family: **InstantSearch iOS** | [InstantSearch Android][instantsea **InstantSearch iOS** is a framework providing components and helpers to help you build the best instant-search experience on iOS with Algolia. It is built on top of Algolia's [Swift API Client](https://github.com/algolia/algoliasearch-client-swift) library to provide you a high-level solution to quickly build various search interfaces. - - ## Demo You can see InstantSearch iOS in action in our [Examples repository][examples-url], in which we published search experiences built with InstantSearch and written in Swift:

- +

[examples-url]: https://github.com/algolia/instantsearch-swift-examples ## Installation -### CocoaPods +### Swift Package Manager -[CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects. +The Swift Package Manager is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies. +Since the release of Swift 5 and Xcode 11, SPM is compatible with the iOS, macOS and tvOS build systems for creating apps. -To install InstantSearch, simply add the following line to your Podfile: +To use SwiftPM, you should use Xcode 11 to open your project. Click `File` -> `Swift Packages` -> `Add Package Dependency`, enter [InstantSearch repo's URL](https://github.com/algolia/instantsearch-ios). +If you consider to use only the business logic modules of InstantSearch and don't need the set of provided UIKit controllers in your project, select only 'InstantSearchCore' in the provided list of products. -#### Swift 4.2+ +If you're a framework author and use InstantSearch as a dependency, update your `Package.swift` file: -```ruby -pod 'InstantSearch', '~> 5.0' -# pod 'InstantSearch/UI' for access to everything -# pod 'InstantSearch/Core' for access to everything except the UI controllers -# pod 'InstantSearch/Client' for access only to the API Client +```swift +let package = Package( + // 7.0.0 ..< 8.0.0 + dependencies: [ + .package(url: "https://github.com/algolia/instantsearch-ios", from: "7.0.0") + ], + // ... +) ``` -#### Swift 4.1 +### CocoaPods + +[CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects. + +To install InstantSearch, simply add the following line to your Podfile: ```ruby -pod 'InstantSearch', '~> 2.0' -# pod 'InstantSearch/Widgets' for access to everything -# pod 'InstantSearch/Core' for access to everything except the UI widgets -# pod 'InstantSearch/Client' for access only to the API Client +pod 'InstantSearch', '~> 7.0.0' +# pod 'InstantSearch/Core' for access to everything except the UI controllers ``` Then, run the following command: @@ -62,36 +66,22 @@ $ pod update [Carthage](https://github.com/Carthage/Carthage) is a simple, decentralized dependency manager for Cocoa. -To install InstantSearch, simply add the following line to your Cartfile: - -#### Swift 4.2+ - -```ruby -github "algolia/instantsearch-ios" ~> 5.0 # for access to everything -# github "algolia/instantsearch-core-swift" ~> 6.0 # for access to everything except the UI widgets -# github "algolia/algoliasearch-client-swift" ~> 7.0 # for access only to the API Client -``` - -#### Swift 4.1 - +- To install InstantSearch, simply add the following line to your Cartfile: ```ruby -github "algolia/instantsearch-ios" ~> 2.0 # for access to everything -# github "algolia/instantsearch-core-swift" ~> 3.0 # for access to everything except the UI widgets -# github "algolia/algoliasearch-client-swift" ~> 5.0 # for access only to the API Client +github "algolia/instantsearch-ios" ~> 7.0.0 ``` -### SwiftPM +- Launch the following commands from the project directory + ```shell + carthage update + ./Carthage/Checkouts/instant-search-ios/carthage-prebuild + carthage build + ``` -The API client is the only library of the framework available on SwiftPM. - -#### Swift 4.2+ - -To install the API Client, add `.package(url:"https://github.com/algolia/algoliasearch-client-swift", from: "6.0.0")` to your package dependencies array in Package.swift, then add `AlgoliaSearch` to your target dependencies. - - -#### Swift 4.1 - -To install the API Client, add `.package(url:"https://github.com/algolia/algoliasearch-client-swift", from: "5.0.0")` to your package dependencies array in Package.swift, then add `AlgoliaSearch` to your target dependencies. + > NOTE: At this time, Carthage does not provide a way to build only specific repository subcomponents (or equivalent of CocoaPods's subspecs). All components and their dependencies will be built with the above command. However, you don't need to copy frameworks you aren't using into your project. For instance, if you aren't using UI components from `InstantSearch`, feel free to delete that framework from the Carthage Build directory after `carthage update` completes keeping only `InstantSearchCore`. + + If this is your first time using Carthage in the project, you'll need to go through some additional steps as explained [over at Carthage](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). + ## Documentation @@ -174,6 +164,8 @@ Run your app and you will the most basic search experience: a `UISearchBar` with To get a more meaningful search experience, please follow our [Getting Started Guide](https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/). +If you only require business logic modules in your project and use `InstantSearchCore` framework, add `import InstantSearchCore` to your source files. + ## Getting Help - **Need help**? Ask a question to the [Algolia Community](https://discourse.algolia.com/) or on [Stack Overflow](http://stackoverflow.com/questions/tagged/algolia). diff --git a/Resources/instant-results.gif b/Resources/instant-results.gif new file mode 100644 index 00000000..24910162 Binary files /dev/null and b/Resources/instant-results.gif differ diff --git a/docgen/assets/img/instantsearch-banner.png b/Resources/instantsearch-banner.png similarity index 100% rename from docgen/assets/img/instantsearch-banner.png rename to Resources/instantsearch-banner.png diff --git a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift b/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift deleted file mode 100644 index 9b1fdb4e..00000000 --- a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// CollectionViewMultiIndexHitsController.swift -// InstantSearchCore -// -// Created by Vladislav Fitc on 25/03/2019. -// Copyright © 2019 Algolia. All rights reserved. -// - -import Foundation -import InstantSearchCore -import UIKit - -public class MultiIndexHitsCollectionController: NSObject, MultiIndexHitsController, HitsCollectionViewContainer { - - public let collectionView: UICollectionView - - public var hitsCollectionView: UICollectionView { - return collectionView - } - - @available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol") - public weak var hitsSource: MultiIndexHitsSource? { - didSet { - dataSource?.hitsSource = hitsSource - delegate?.hitsSource = hitsSource - } - } - - @available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol") - public var dataSource: MultiIndexHitsCollectionViewDataSource? { - didSet { - dataSource?.hitsSource = hitsSource - collectionView.dataSource = dataSource - } - } - - public var delegate: MultiIndexHitsCollectionViewDelegate? { - didSet { - delegate?.hitsSource = hitsSource - collectionView.delegate = delegate - } - } - - public init(collectionView: UICollectionView) { - self.collectionView = collectionView - } - -} diff --git a/Sources/FatalErrorUtil.swift b/Sources/FatalErrorUtil.swift deleted file mode 100644 index 9038fab3..00000000 --- a/Sources/FatalErrorUtil.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// FatalErrorUtil.swift -// InstantSearch -// -// Created by Vladislav Fitc on 04/09/2019. -// - -import Foundation - -// overrides Swift global `fatalError` -public func fatalError(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Never { - FatalErrorUtil.fatalErrorClosure(message(), file, line) - unreachable() -} - -/// This is a `noreturn` function that pauses forever -public func unreachable() -> Never { - repeat { - RunLoop.current.run() - } while (true) -} - -/// Utility functions that can replace and restore the `fatalError` global function. -public struct FatalErrorUtil { - - // Called by the custom implementation of `fatalError`. - static var fatalErrorClosure: (String, StaticString, UInt) -> Never = defaultFatalErrorClosure - - // backup of the original Swift `fatalError` - private static let defaultFatalErrorClosure = { Swift.fatalError($0, file: $1, line: $2) } - - /// Replace the `fatalError` global function with something else. - public static func replaceFatalError(closure: @escaping (String, StaticString, UInt) -> Never) { - fatalErrorClosure = closure - } - - /// Restore the `fatalError` global function back to the original Swift implementation - public static func restoreFatalError() { - fatalErrorClosure = defaultFatalErrorClosure - } -} diff --git a/Sources/Info.plist b/Sources/Info.plist deleted file mode 100644 index 5313a0db..00000000 --- a/Sources/Info.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 5.1.0 - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/Sources/InstantSearch-Bridging-Header.h b/Sources/InstantSearch-Bridging-Header.h deleted file mode 100644 index ce3b8314..00000000 --- a/Sources/InstantSearch-Bridging-Header.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// InstantSearch-Bridging-Header.h -// InstantSearch -// -// Created by Guy Daher on 12/05/2017. -// -// - -#ifndef InstantSearch_Bridging_Header_h -#define InstantSearch_Bridging_Header_h - - -#endif /* InstantSearch_Bridging_Header_h */ diff --git a/Sources/Controller/ClearRefinements/ClearRefinementsButtonController.swift b/Sources/InstantSearch/ClearRefinements/ClearRefinementsButtonController.swift similarity index 76% rename from Sources/Controller/ClearRefinements/ClearRefinementsButtonController.swift rename to Sources/InstantSearch/ClearRefinements/ClearRefinementsButtonController.swift index 26f1fe84..95e071fc 100644 --- a/Sources/Controller/ClearRefinements/ClearRefinementsButtonController.swift +++ b/Sources/InstantSearch/ClearRefinements/ClearRefinementsButtonController.swift @@ -5,23 +5,26 @@ // Created by Vladislav Fitc on 24/05/2019. // -import Foundation -import InstantSearchCore +#if !InstantSearchCocoaPods +@_exported import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public class FilterClearButtonController: FilterClearController { - + public let button: UIButton - + public var onClick: (() -> Void)? - + public init(button: UIButton) { self.button = button button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) } - + @objc private func didTapButton() { onClick?() } - + } +#endif diff --git a/Sources/Controller/CurrentFilters/CurrentFiltersSearchTextFieldController.swift b/Sources/InstantSearch/CurrentFilters/CurrentFiltersSearchTextFieldController.swift similarity index 92% rename from Sources/Controller/CurrentFilters/CurrentFiltersSearchTextFieldController.swift rename to Sources/InstantSearch/CurrentFilters/CurrentFiltersSearchTextFieldController.swift index 7095c0b3..0851dc2c 100644 --- a/Sources/Controller/CurrentFilters/CurrentFiltersSearchTextFieldController.swift +++ b/Sources/InstantSearch/CurrentFilters/CurrentFiltersSearchTextFieldController.swift @@ -5,29 +5,32 @@ // Created by Vladislav Fitc on 29/01/2020. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(macOS)) import UIKit @available(iOS 13.0, *) public class SearchTextFieldCurrentFiltersController: CurrentFiltersController { - + public var items: [FilterAndID] = [] public var onRemoveItem: ((FilterAndID) -> Void)? public let searchTextField: UISearchTextField - + public init(searchTextField: UISearchTextField) { self.searchTextField = searchTextField searchTextField.addTarget(self, action: #selector(didChange), for: .editingChanged) } - + public convenience init(searchBar: UISearchBar) { self.init(searchTextField: searchBar.searchTextField) } - + public func setItems(_ items: [FilterAndID]) { self.items = Array(items) } - + public func reload() { let tokens = items .enumerated() @@ -38,7 +41,7 @@ public class SearchTextFieldCurrentFiltersController: CurrentFiltersController { } searchTextField.tokens = tokens } - + @objc private func didChange(_ textField: UITextField) { let previousFiltersIndices = Set(0.. Void)? - + public let tableView: UITableView - + public var selectableItems: [SelectableItem] = [] public var facetPresenter: FacetPresenter? - + let cellID: String - + public init(tableView: UITableView, cellID: String = "FacetList") { self.tableView = tableView self.cellID = cellID @@ -28,42 +30,43 @@ open class FacetListTableController: NSObject, FacetListController { tableView.delegate = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID) } - + // MARK: - RefinementFacetsViewController - + public func setSelectableItems(selectableItems: [SelectableItem]) { self.selectableItems = selectableItems } - + public func reload() { tableView.reloadData() } - + } extension FacetListTableController: UITableViewDataSource { - + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return selectableItems.count } - + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) let selectableRefinement = selectableItems[indexPath.row] let facetPresenter = self.facetPresenter ?? DefaultPresenter.Facet.present cell.textLabel?.text = facetPresenter(selectableRefinement.item) cell.accessoryType = selectableRefinement.isSelected ? .checkmark : .none - + return cell } - + } extension FacetListTableController: UITableViewDelegate { - + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selectableItem = selectableItems[indexPath.row] self.onClick?(selectableItem.item) } - + } +#endif diff --git a/Sources/Controller/FilterList/FilterListTableController.swift b/Sources/InstantSearch/FilterList/FilterListTableController.swift similarity index 94% rename from Sources/Controller/FilterList/FilterListTableController.swift rename to Sources/InstantSearch/FilterList/FilterListTableController.swift index ff17bdc7..43ace190 100644 --- a/Sources/Controller/FilterList/FilterListTableController.swift +++ b/Sources/InstantSearch/FilterList/FilterListTableController.swift @@ -5,23 +5,25 @@ // Created by Vladislav Fitc on 02/08/2019. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit open class FilterListTableController: NSObject, FilterListController, UITableViewDataSource, UITableViewDelegate { - + public typealias Item = F - + open var onClick: ((F) -> Void)? - + public let tableView: UITableView - + public var selectableItems: [SelectableItem] = [] public var filterPresenter: FilterPresenter? - + let cellID: String - + public init(tableView: UITableView, cellID: String = "FilterListFacet") { self.tableView = tableView self.cellID = cellID @@ -30,38 +32,39 @@ open class FilterListTableController: NSObject, FilterListControl tableView.delegate = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID) } - + // MARK: - FilterListController - + open func setSelectableItems(selectableItems: [SelectableItem]) { self.selectableItems = selectableItems } - + open func reload() { tableView.reloadData() } - + // MARK: - UITableViewDataSource - + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return selectableItems.count } - + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) let filter = selectableItems[indexPath.row] let filterPresenter = self.filterPresenter ?? DefaultPresenter.Filter.present cell.textLabel?.text = filterPresenter(Filter(filter.item)) cell.accessoryType = filter.isSelected ? .checkmark : .none - + return cell } - + // MARK: - UITableViewDelegate - + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selectableItem = selectableItems[indexPath.row] onClick?(selectableItem.item) } - + } +#endif diff --git a/Sources/Helpers/UICollectionView+Convenience.swift b/Sources/InstantSearch/Helpers/UICollectionView+Convenience.swift similarity index 85% rename from Sources/Helpers/UICollectionView+Convenience.swift rename to Sources/InstantSearch/Helpers/UICollectionView+Convenience.swift index cb0a1ef8..580cc324 100644 --- a/Sources/Helpers/UICollectionView+Convenience.swift +++ b/Sources/InstantSearch/Helpers/UICollectionView+Convenience.swift @@ -6,16 +6,19 @@ // import Foundation + +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public extension UICollectionView { - + func scrollToFirstNonEmptySection() { (0.. 0 }) .flatMap { IndexPath(item: 0, section: $0) } .flatMap { scrollToItem(at: $0, at: .top, animated: false) } - + } - + } +#endif diff --git a/Sources/Helpers/UITableView+Convenience.swift b/Sources/InstantSearch/Helpers/UITableView+Convenience.swift similarity index 85% rename from Sources/Helpers/UITableView+Convenience.swift rename to Sources/InstantSearch/Helpers/UITableView+Convenience.swift index 6c5435b7..2ad1120e 100644 --- a/Sources/Helpers/UITableView+Convenience.swift +++ b/Sources/InstantSearch/Helpers/UITableView+Convenience.swift @@ -6,15 +6,18 @@ // import Foundation + +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public extension UITableView { - + func scrollToFirstNonEmptySection() { (0.. 0 }) .flatMap { IndexPath(row: 0, section: $0) } .flatMap { scrollToRow(at: $0, at: .top, animated: false) } } - + } +#endif diff --git a/Sources/Controller/Hierarchical/HierarchicalTableViewController.swift b/Sources/InstantSearch/Hierarchical/HierarchicalTableViewController.swift similarity index 94% rename from Sources/Controller/Hierarchical/HierarchicalTableViewController.swift rename to Sources/InstantSearch/Hierarchical/HierarchicalTableViewController.swift index 8f399d3b..8c259991 100644 --- a/Sources/Controller/Hierarchical/HierarchicalTableViewController.swift +++ b/Sources/InstantSearch/Hierarchical/HierarchicalTableViewController.swift @@ -6,8 +6,10 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit open class HierarchicalTableViewController: NSObject, HierarchicalController { @@ -63,3 +65,4 @@ extension HierarchicalTableViewController: UITableViewDelegate { onClick?(item.facet.value) } } +#endif diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionController.swift b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionController.swift similarity index 86% rename from Sources/Controller/Hits/CollectionView/HitsCollectionController.swift rename to Sources/InstantSearch/Hits/CollectionView/HitsCollectionController.swift index 33b8d1ec..f55095c9 100644 --- a/Sources/Controller/Hits/CollectionView/HitsCollectionController.swift +++ b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionController.swift @@ -6,8 +6,10 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public typealias HitViewConfigurator = (HitsView, Hit, IndexPath) -> SingleHitView @@ -16,43 +18,42 @@ public typealias HitClickHandler = (HitsView, Hit, IndexPath) -> public typealias CollectionViewCellConfigurator = HitViewConfigurator public typealias CollectionViewClickHandler = HitClickHandler +@available(*, unavailable, message: "Use your own UICollectionViewController conforming to HitsController protocol") public class HitsCollectionController: NSObject, HitsController, HitsCollectionViewContainer { - + public let collectionView: UICollectionView - + public var hitsCollectionView: UICollectionView { return collectionView } - + public weak var hitsSource: Source? - @available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol") public var dataSource: HitsCollectionViewDataSource? { didSet { dataSource?.hitsSource = hitsSource collectionView.dataSource = dataSource } } - - @available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol") + public var delegate: HitsCollectionViewDelegate? { didSet { delegate?.hitsSource = hitsSource collectionView.delegate = delegate } } - + public init(collectionView: UICollectionView) { self.collectionView = collectionView } - + // These functions are implemented in the protocol extension, but should be there till // compiler bug is fixed public func reload() { hitsCollectionView.reloadData() } - + public func scrollToTop() { guard hitsCollectionView.numberOfItems(inSection: 0) != 0 else { return } let indexPath = IndexPath(item: 0, section: 0) @@ -60,3 +61,4 @@ public class HitsCollectionController: NSObject, HitsControl } } +#endif diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionViewContainer.swift b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewContainer.swift similarity index 87% rename from Sources/Controller/Hits/CollectionView/HitsCollectionViewContainer.swift rename to Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewContainer.swift index 33916c31..81d042ad 100644 --- a/Sources/Controller/Hits/CollectionView/HitsCollectionViewContainer.swift +++ b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewContainer.swift @@ -5,39 +5,43 @@ // Created by Vladislav Fitc on 02/09/2019. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public protocol HitsCollectionViewContainer { - + var hitsCollectionView: UICollectionView { get } - + } public extension HitsController where Self: HitsCollectionViewContainer { - + func reload() { hitsCollectionView.reloadData() } - + func scrollToTop() { guard hitsCollectionView.numberOfItems(inSection: 0) != 0 else { return } let indexPath = IndexPath(row: 0, section: 0) self.hitsCollectionView.scrollToItem(at: indexPath, at: .top, animated: false) } - + } public extension MultiIndexHitsController where Self: HitsCollectionViewContainer { - + func reload() { hitsCollectionView.reloadData() } - + func scrollToTop() { guard hitsCollectionView.numberOfItems(inSection: 0) != 0 else { return } let indexPath = IndexPath(item: 0, section: 0) hitsCollectionView.scrollToItem(at: indexPath, at: .top, animated: false) } - + } +#endif diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDataSource.swift b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDataSource.swift similarity index 83% rename from Sources/Controller/Hits/CollectionView/HitsCollectionViewDataSource.swift rename to Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDataSource.swift index f73efbc3..0f399e86 100644 --- a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDataSource.swift +++ b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDataSource.swift @@ -5,45 +5,49 @@ // Created by Vladislav Fitc on 02/08/2019. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit -@available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol") +@available(*, unavailable, message: "Use your own UICollectionViewController conforming to HitsController protocol") open class HitsCollectionViewDataSource: NSObject, UICollectionViewDataSource { - + public var cellConfigurator: CollectionViewCellConfigurator public var templateCellProvider: () -> UICollectionViewCell public weak var hitsSource: DataSource? - + public init(cellConfigurator: @escaping CollectionViewCellConfigurator) { self.cellConfigurator = cellConfigurator self.templateCellProvider = { return .init() } } - + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - + guard let hitsSource = hitsSource else { Logger.missingHitsSourceWarning() return 0 } return hitsSource.numberOfHits() - + } - + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - + guard let hitsSource = hitsSource else { Logger.missingHitsSourceWarning() return templateCellProvider() } - + guard let hit = hitsSource.hit(atIndex: indexPath.row) else { return templateCellProvider() } - + return cellConfigurator(collectionView, hit, indexPath) - + } - + } +#endif diff --git a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDelegate.swift b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDelegate.swift similarity index 76% rename from Sources/Controller/Hits/CollectionView/HitsCollectionViewDelegate.swift rename to Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDelegate.swift index a76e5c6d..cf183f69 100644 --- a/Sources/Controller/Hits/CollectionView/HitsCollectionViewDelegate.swift +++ b/Sources/InstantSearch/Hits/CollectionView/HitsCollectionViewDelegate.swift @@ -5,21 +5,24 @@ // Created by Vladislav Fitc on 02/08/2019. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit -@available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol") +@available(*, unavailable, message: "Use your own UICollectionViewController conforming to HitsController protocol") open class HitsCollectionViewDelegate: NSObject, UICollectionViewDelegate { - + public var clickHandler: CollectionViewClickHandler public weak var hitsSource: DataSource? - + public init(clickHandler: @escaping CollectionViewClickHandler) { self.clickHandler = clickHandler } - + open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - + guard let hitsSource = hitsSource else { Logger.missingHitsSourceWarning() return @@ -29,7 +32,8 @@ open class HitsCollectionViewDelegate: NSObject, UIColle return } clickHandler(collectionView, hit, indexPath) - + } - + } +#endif diff --git a/Sources/Controller/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift b/Sources/InstantSearch/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift similarity index 82% rename from Sources/Controller/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift rename to Sources/InstantSearch/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift index 379af40c..a51e6c19 100644 --- a/Sources/Controller/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift +++ b/Sources/InstantSearch/Hits/CollectionView/UICollectionViewController+HitsCollectionViewContainer.swift @@ -6,12 +6,15 @@ // import Foundation + +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit extension UICollectionViewController: HitsCollectionViewContainer { - + public var hitsCollectionView: UICollectionView { return collectionView } - + } +#endif diff --git a/Sources/Controller/Hits/TableView/HitsTableController.swift b/Sources/InstantSearch/Hits/TableView/HitsTableController.swift similarity index 85% rename from Sources/Controller/Hits/TableView/HitsTableController.swift rename to Sources/InstantSearch/Hits/TableView/HitsTableController.swift index 14be48a1..0701af5e 100644 --- a/Sources/Controller/Hits/TableView/HitsTableController.swift +++ b/Sources/InstantSearch/Hits/TableView/HitsTableController.swift @@ -6,59 +6,61 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public typealias TableViewCellConfigurator = HitViewConfigurator public typealias TableViewClickHandler = HitClickHandler +@available(*, unavailable, message: "Use your own UITableViewController conforming to HitsController protocol") public class HitsTableController: NSObject, HitsController, HitsTableViewContainer { - + public var hitsTableView: UITableView { return tableView } - + public let tableView: UITableView - + public weak var hitsSource: Source? { didSet { dataSource?.hitsSource = hitsSource delegate?.hitsSource = hitsSource } } - - @available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol") + public var dataSource: HitsTableViewDataSource? { didSet { dataSource?.hitsSource = hitsSource tableView.dataSource = dataSource } } - - @available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol") + public var delegate: HitsTableViewDelegate? { didSet { delegate?.hitsSource = hitsSource tableView.delegate = delegate } } - + public init(tableView: UITableView) { self.tableView = tableView } - + // These functions are implemented in the protocol extension, but should be there till // compiler bug is fixed public func reload() { hitsTableView.reloadData() } - + public func scrollToTop() { guard hitsTableView.numberOfRows(inSection: 0) != 0 else { return } let indexPath = IndexPath(row: 0, section: 0) self.hitsTableView.scrollToRow(at: indexPath, at: .top, animated: false) } - + } +#endif diff --git a/Sources/Controller/Hits/TableView/HitsTableViewContainer.swift b/Sources/InstantSearch/Hits/TableView/HitsTableViewContainer.swift similarity index 87% rename from Sources/Controller/Hits/TableView/HitsTableViewContainer.swift rename to Sources/InstantSearch/Hits/TableView/HitsTableViewContainer.swift index addd86ab..17300944 100644 --- a/Sources/Controller/Hits/TableView/HitsTableViewContainer.swift +++ b/Sources/InstantSearch/Hits/TableView/HitsTableViewContainer.swift @@ -5,39 +5,43 @@ // Created by Vladislav Fitc on 02/09/2019. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public protocol HitsTableViewContainer { - + var hitsTableView: UITableView { get } - + } public extension HitsController where Self: HitsTableViewContainer { - + func reload() { hitsTableView.reloadData() } - + func scrollToTop() { guard hitsTableView.numberOfRows(inSection: 0) != 0 else { return } let indexPath = IndexPath(row: 0, section: 0) self.hitsTableView.scrollToRow(at: indexPath, at: .top, animated: false) } - + } public extension MultiIndexHitsController where Self: HitsTableViewContainer { - + func reload() { hitsTableView.reloadData() } - + func scrollToTop() { guard hitsTableView.numberOfRows(inSection: 0) != 0 else { return } let indexPath = IndexPath(item: 0, section: 0) hitsTableView.scrollToRow(at: indexPath, at: .top, animated: false) } - + } +#endif diff --git a/Sources/Controller/Hits/TableView/HitsTableViewDataSource.swift b/Sources/InstantSearch/Hits/TableView/HitsTableViewDataSource.swift similarity index 82% rename from Sources/Controller/Hits/TableView/HitsTableViewDataSource.swift rename to Sources/InstantSearch/Hits/TableView/HitsTableViewDataSource.swift index a5fb23eb..bf230b1f 100644 --- a/Sources/Controller/Hits/TableView/HitsTableViewDataSource.swift +++ b/Sources/InstantSearch/Hits/TableView/HitsTableViewDataSource.swift @@ -5,45 +5,49 @@ // Created by Vladislav Fitc on 02/08/2019. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit -@available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol") +@available(*, unavailable, message: "Use your own UITableViewController conforming to HitsController protocol") open class HitsTableViewDataSource: NSObject, UITableViewDataSource { - + public var cellConfigurator: TableViewCellConfigurator public var templateCellProvider: () -> UITableViewCell public weak var hitsSource: DataSource? - + public init(cellConfigurator: @escaping TableViewCellConfigurator) { self.cellConfigurator = cellConfigurator self.templateCellProvider = { return .init() } } - + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - + guard let hitsSource = hitsSource else { Logger.missingHitsSourceWarning() return 0 } - + return hitsSource.numberOfHits() - + } - + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - + guard let hitsSource = hitsSource else { Logger.missingHitsSourceWarning() return .init() } - + guard let hit = hitsSource.hit(atIndex: indexPath.row) else { return templateCellProvider() } - + return cellConfigurator(tableView, hit, indexPath) - + } - + } +#endif diff --git a/Sources/Controller/Hits/TableView/HitsTableViewDelegate.swift b/Sources/InstantSearch/Hits/TableView/HitsTableViewDelegate.swift similarity index 75% rename from Sources/Controller/Hits/TableView/HitsTableViewDelegate.swift rename to Sources/InstantSearch/Hits/TableView/HitsTableViewDelegate.swift index b732f2a6..6f73ddb6 100644 --- a/Sources/Controller/Hits/TableView/HitsTableViewDelegate.swift +++ b/Sources/InstantSearch/Hits/TableView/HitsTableViewDelegate.swift @@ -5,21 +5,24 @@ // Created by Vladislav Fitc on 02/08/2019. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit -@available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol") +@available(*, unavailable, message: "Use your own UITableViewController conforming to HitsController protocol") open class HitsTableViewDelegate: NSObject, UITableViewDelegate { - + public var clickHandler: TableViewClickHandler public weak var hitsSource: DataSource? - + public init(clickHandler: @escaping TableViewClickHandler) { self.clickHandler = clickHandler } - + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - + guard let hitsSource = hitsSource else { Logger.missingHitsSourceWarning() return @@ -29,7 +32,8 @@ open class HitsTableViewDelegate: NSObject, UITableViewD return } clickHandler(tableView, hit, indexPath) - + } - + } +#endif diff --git a/Sources/Controller/Hits/TableView/UITableViewController+HitsTableViewContainer.swift b/Sources/InstantSearch/Hits/TableView/UITableViewController+HitsTableViewContainer.swift similarity index 80% rename from Sources/Controller/Hits/TableView/UITableViewController+HitsTableViewContainer.swift rename to Sources/InstantSearch/Hits/TableView/UITableViewController+HitsTableViewContainer.swift index 150b0680..c756543b 100644 --- a/Sources/Controller/Hits/TableView/UITableViewController+HitsTableViewContainer.swift +++ b/Sources/InstantSearch/Hits/TableView/UITableViewController+HitsTableViewContainer.swift @@ -6,12 +6,15 @@ // import Foundation + +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit extension UITableViewController: HitsTableViewContainer { - + public var hitsTableView: UITableView { return tableView } - + } +#endif diff --git a/Sources/Controller/LoadingIndicator/ActivityIndicatorController.swift b/Sources/InstantSearch/LoadingIndicator/ActivityIndicatorController.swift similarity index 86% rename from Sources/Controller/LoadingIndicator/ActivityIndicatorController.swift rename to Sources/InstantSearch/LoadingIndicator/ActivityIndicatorController.swift index 523f50c4..c76e63f8 100644 --- a/Sources/Controller/LoadingIndicator/ActivityIndicatorController.swift +++ b/Sources/InstantSearch/LoadingIndicator/ActivityIndicatorController.swift @@ -5,14 +5,16 @@ // Created by Vladislav Fitc on 24/05/2019. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public class ActivityIndicatorController: LoadingController { let activityIndicator: UIActivityIndicatorView - + public init (activityIndicator: UIActivityIndicatorView) { self.activityIndicator = activityIndicator } @@ -20,13 +22,14 @@ public class ActivityIndicatorController: LoadingController { public func startLoading() { activityIndicator.startAnimating() } - + public func stopLoading() { activityIndicator.stopAnimating() } - + public func setItem(_ item: Bool) { item ? activityIndicator.startAnimating() : activityIndicator.stopAnimating() } - + } +#endif diff --git a/Sources/Controller/Logging.swift b/Sources/InstantSearch/Log.swift similarity index 60% rename from Sources/Controller/Logging.swift rename to Sources/InstantSearch/Log.swift index 07737763..04a7b6f1 100644 --- a/Sources/Controller/Logging.swift +++ b/Sources/InstantSearch/Log.swift @@ -5,96 +5,67 @@ // Created by Vladislav Fitc on 31/01/2020. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore import Logging -typealias SwiftLog = Logging.Logger - struct Logger { - - static var loggingService: Loggable = SwiftLog(label: "com.algolia.InstantSearch") - + + static var loggingService: Loggable = { + var swiftLog = Logging.Logger(label: "com.algolia.InstantSearch") + print("InstantSearch: Default minimal log severity level is info. Change InstantSearch.Logger.minLogServerityLevel value if you want to change it.") + swiftLog.logLevel = .info + return swiftLog + }() + private init() {} - + static func trace(_ message: String) { loggingService.log(level: .trace, message: message) } - + static func debug(_ message: String) { loggingService.log(level: .debug, message: message) } - + static func info(_ message: String) { loggingService.log(level: .info, message: message) } - + static func notice(_ message: String) { loggingService.log(level: .notice, message: message) } - + static func warning(_ message: String) { loggingService.log(level: .warning, message: message) } - + static func error(_ message: String) { loggingService.log(level: .error, message: message) } - + static func critical(_ message: String) { loggingService.log(level: .critical, message: message) } - -} -enum LogLevel { - case trace, debug, info, notice, warning, error, critical } +#endif extension Logger { - + static func missingHitsSourceWarning() { warning("Missing hits source") } - + static func missingCellConfiguratorWarning(forSection section: Int) { warning("No cell configurator found for section \(section)") } - + static func missingClickHandlerWarning(forSection section: Int) { warning("No click handler found for section \(section)") } - + static func error(_ error: Error) { self.error("\(error)") } } - -extension LogLevel { - - var swiftLogLevel: SwiftLog.Level { - switch self { - case .trace: return .trace - case .debug: return .debug - case .info: return .info - case .notice: return .notice - case .warning: return .warning - case .error: return .error - case .critical: return .critical - } - } - -} - -protocol Loggable { - - func log(level: LogLevel, message: String) - -} - -extension SwiftLog: Loggable { - - func log(level: LogLevel, message: String) { - self.log(level: level.swiftLogLevel, SwiftLog.Message(stringLiteral: message), metadata: .none) - } - -} diff --git a/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift new file mode 100644 index 00000000..a27805a0 --- /dev/null +++ b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionController.swift @@ -0,0 +1,36 @@ +// +// CollectionViewMultiIndexHitsController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 25/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// +// swiftlint:disable weak_delegate + +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) +import UIKit + +@available(*, unavailable, message: "Use your own UICollectionViewController conforming to MultiIndexHitsController protocol") +public class MultiIndexHitsCollectionController: NSObject, MultiIndexHitsController, HitsCollectionViewContainer { + + public let collectionView: UICollectionView + + public var hitsCollectionView: UICollectionView { + return collectionView + } + + public weak var hitsSource: MultiIndexHitsSource? + + public var dataSource: MultiIndexHitsCollectionViewDataSource? + + public var delegate: MultiIndexHitsCollectionViewDelegate? + + public init(collectionView: UICollectionView) { + self.collectionView = collectionView + } + +} +#endif diff --git a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift similarity index 86% rename from Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift rename to Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift index 20ac74ba..218ab870 100644 --- a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift +++ b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDataSource.swift @@ -5,28 +5,31 @@ // Created by Vladislav Fitc on 02/08/2019. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit -@available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol") -open class MultiIndexHitsCollectionViewDataSource: NSObject { - +@available(*, unavailable, message: "Use your own UICollectionViewController conforming to HitsController protocol") +open class MultiIndexHitsCollectionViewDataSource: NSObject, UICollectionViewDataSource { + private typealias CellConfigurator = (UICollectionView, Int) throws -> UICollectionViewCell - + public weak var hitsSource: MultiIndexHitsSource? - + private var cellConfigurators: [Int: CellConfigurator] - + override init() { cellConfigurators = [:] super.init() } - + public func setCellConfigurator(forSection section: Int, templateCellProvider: @escaping () -> UICollectionViewCell = { return .init() }, _ cellConfigurator: @escaping CollectionViewCellConfigurator) { cellConfigurators[section] = { [weak self] (collectionView, row) in - + guard let hitsSource = self?.hitsSource else { Logger.missingHitsSourceWarning() return .init() @@ -39,11 +42,7 @@ open class MultiIndexHitsCollectionViewDataSource: NSObject { return cellConfigurator(collectionView, hit, IndexPath(row: row, section: section)) } } - -} -extension MultiIndexHitsCollectionViewDataSource: UICollectionViewDataSource { - open func numberOfSections(in collectionView: UICollectionView) -> Int { guard let hitsSource = hitsSource else { Logger.missingHitsSourceWarning() @@ -51,7 +50,7 @@ extension MultiIndexHitsCollectionViewDataSource: UICollectionViewDataSource { } return hitsSource.numberOfSections() } - + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { guard let hitsSource = hitsSource else { Logger.missingHitsSourceWarning() @@ -59,7 +58,7 @@ extension MultiIndexHitsCollectionViewDataSource: UICollectionViewDataSource { } return hitsSource.numberOfHits(inSection: section) } - + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cellConfigurator = cellConfigurators[indexPath.section] else { Logger.missingCellConfiguratorWarning(forSection: indexPath.section) @@ -72,5 +71,6 @@ extension MultiIndexHitsCollectionViewDataSource: UICollectionViewDataSource { return .init() } } - + } +#endif diff --git a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift similarity index 79% rename from Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift rename to Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift index f774b988..087e505d 100644 --- a/Sources/Controller/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift +++ b/Sources/InstantSearch/MultiIndexHits/CollectionView/MultiIndexHitsCollectionViewDelegate.swift @@ -5,45 +5,44 @@ // Created by Vladislav Fitc on 02/08/2019. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit -@available(*, deprecated, message: "Use your own UICollectionViewController conforming to HitsController protocol") -open class MultiIndexHitsCollectionViewDelegate: NSObject { - +@available(*, unavailable, message: "Use your own UICollectionViewController conforming to HitsController protocol") +open class MultiIndexHitsCollectionViewDelegate: NSObject, UICollectionViewDelegate { + typealias ClickHandler = (UICollectionView, Int) throws -> Void - + public weak var hitsSource: MultiIndexHitsSource? - + private var clickHandlers: [Int: ClickHandler] - + public override init() { clickHandlers = [:] super.init() } - + public func setClickHandler(forSection section: Int, _ clickHandler: @escaping CollectionViewClickHandler) { clickHandlers[section] = { [weak self] (collectionView, row) in guard let delegate = self else { return } - + guard let hitsSource = delegate.hitsSource else { Logger.missingHitsSourceWarning() return } - + guard let hit: Hit = try hitsSource.hit(atIndex: row, inSection: section) else { return } - + clickHandler(collectionView, hit, IndexPath(item: row, section: section)) - + } } - -} -extension MultiIndexHitsCollectionViewDelegate: UICollectionViewDelegate { - open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let clickHandler = clickHandlers[indexPath.section] else { Logger.missingClickHandlerWarning(forSection: indexPath.section) @@ -55,5 +54,6 @@ extension MultiIndexHitsCollectionViewDelegate: UICollectionViewDelegate { Logger.error(error) } } - + } +#endif diff --git a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableController.swift b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableController.swift similarity index 78% rename from Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableController.swift rename to Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableController.swift index 8316fc48..1a06c442 100644 --- a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableController.swift +++ b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableController.swift @@ -6,43 +6,45 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit +@available(*, unavailable, message: "Use your own UITableViewController conforming to MultiIndexHitsController protocol") public class MultiIndexHitsTableController: NSObject, HitsTableViewContainer, MultiIndexHitsController { - + public var hitsTableView: UITableView { return tableView } - + public let tableView: UITableView - + public weak var hitsSource: MultiIndexHitsSource? { didSet { dataSource?.hitsSource = hitsSource delegate?.hitsSource = hitsSource } } - - @available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol") + public var dataSource: MultiIndexHitsTableViewDataSource? { didSet { dataSource?.hitsSource = hitsSource tableView.dataSource = dataSource } } - - @available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol") + public var delegate: MultiIndexHitsTableViewDelegate? { didSet { delegate?.hitsSource = hitsSource tableView.delegate = delegate } } - + public init(tableView: UITableView) { self.tableView = tableView } - + } +#endif diff --git a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift similarity index 84% rename from Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift rename to Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift index bbf7df63..d5463832 100644 --- a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift +++ b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDataSource.swift @@ -5,48 +5,44 @@ // Created by Vladislav Fitc on 02/08/2019. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit -@available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol") -open class MultiIndexHitsTableViewDataSource: NSObject { - +@available(*, unavailable, message: "Use your own UITableViewController conforming to MultiIndexHitsController protocol") +open class MultiIndexHitsTableViewDataSource: NSObject, UITableViewDataSource { + private typealias CellConfigurator = (UITableView, Int) throws -> UITableViewCell - + public weak var hitsSource: MultiIndexHitsSource? - + private var cellConfigurators: [Int: CellConfigurator] - + public override init() { cellConfigurators = [:] super.init() } - + public func setCellConfigurator(forSection section: Int, templateCellProvider: @escaping () -> UITableViewCell = { return .init() }, _ cellConfigurator: @escaping TableViewCellConfigurator) { cellConfigurators[section] = { [weak self] (tableView, row) in - guard let dataSource = self else { - return .init() - } - + guard let hitsSource = self?.hitsSource else { Logger.missingHitsSourceWarning() return .init() } - + guard let hit: Hit = try hitsSource.hit(atIndex: row, inSection: section) else { return templateCellProvider() } - + return cellConfigurator(tableView, hit, IndexPath(row: row, section: section)) } } - -} -extension MultiIndexHitsTableViewDataSource: UITableViewDataSource { - open func numberOfSections(in tableView: UITableView) -> Int { guard let hitsSource = hitsSource else { Logger.missingHitsSourceWarning() @@ -54,7 +50,7 @@ extension MultiIndexHitsTableViewDataSource: UITableViewDataSource { } return hitsSource.numberOfSections() } - + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let hitsSource = hitsSource else { Logger.missingHitsSourceWarning() @@ -62,7 +58,7 @@ extension MultiIndexHitsTableViewDataSource: UITableViewDataSource { } return hitsSource.numberOfHits(inSection: section) } - + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cellConfigurator = cellConfigurators[indexPath.section] else { Logger.missingCellConfiguratorWarning(forSection: indexPath.section) @@ -75,5 +71,6 @@ extension MultiIndexHitsTableViewDataSource: UITableViewDataSource { return .init() } } - + } +#endif diff --git a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift similarity index 79% rename from Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift rename to Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift index 8f3099cf..f10e7aa1 100644 --- a/Sources/Controller/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift +++ b/Sources/InstantSearch/MultiIndexHits/TableView/MultiIndexHitsTableViewDelegate.swift @@ -5,23 +5,26 @@ // Created by Vladislav Fitc on 02/08/2019. // -import Foundation +#if !InstantSearchCocoaPods +import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit -@available(*, deprecated, message: "Use your own UITableViewController conforming to HitsController protocol") -open class MultiIndexHitsTableViewDelegate: NSObject { - +@available(*, unavailable, message: "Use your own UITableViewController conforming to MultiIndexHitsController protocol") +open class MultiIndexHitsTableViewDelegate: NSObject, UITableViewDelegate { + typealias ClickHandler = (UITableView, Int) throws -> Void - + public weak var hitsSource: MultiIndexHitsSource? - + private var clickHandlers: [Int: ClickHandler] - + public override init() { clickHandlers = [:] super.init() } - + public func setClickHandler(forSection section: Int, _ clickHandler: @escaping TableViewClickHandler) { clickHandlers[section] = { [weak self] (tableView, row) in guard let delegate = self else { return } @@ -30,20 +33,16 @@ open class MultiIndexHitsTableViewDelegate: NSObject { Logger.missingHitsSourceWarning() return } - + guard let hit: Hit = try hitsSource.hit(atIndex: row, inSection: section) else { return } clickHandler(tableView, hit, IndexPath(item: row, section: section)) - + } } - -} -extension MultiIndexHitsTableViewDelegate: UITableViewDelegate { - open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let clickHandler = clickHandlers[indexPath.section] else { Logger.missingClickHandlerWarning(forSection: indexPath.section) @@ -56,5 +55,6 @@ extension MultiIndexHitsTableViewDelegate: UITableViewDelegate { return } } - + } +#endif diff --git a/Sources/Controller/Numeric Comparator/NumericStepperController.swift b/Sources/InstantSearch/Numeric Comparator/NumericStepperController.swift similarity index 90% rename from Sources/Controller/Numeric Comparator/NumericStepperController.swift rename to Sources/InstantSearch/Numeric Comparator/NumericStepperController.swift index bb9a2f92..78619168 100644 --- a/Sources/Controller/Numeric Comparator/NumericStepperController.swift +++ b/Sources/InstantSearch/Numeric Comparator/NumericStepperController.swift @@ -6,8 +6,10 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(macOS)) import UIKit public class NumericStepperController: NumberController { @@ -34,9 +36,10 @@ public class NumericStepperController: NumberController { public init(stepper: UIStepper) { self.stepper = stepper } - + public func invalidate() { stepper.value = 0 } - + } +#endif diff --git a/Sources/Controller/Numeric Comparator/NumericTextFieldController.swift b/Sources/InstantSearch/Numeric Comparator/NumericTextFieldController.swift similarity index 91% rename from Sources/Controller/Numeric Comparator/NumericTextFieldController.swift rename to Sources/InstantSearch/Numeric Comparator/NumericTextFieldController.swift index 29a824cc..0cf95b6d 100644 --- a/Sources/Controller/Numeric Comparator/NumericTextFieldController.swift +++ b/Sources/InstantSearch/Numeric Comparator/NumericTextFieldController.swift @@ -6,8 +6,10 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(macOS) || os(tvOS)) import UIKit public class NumericTextFieldController: NSObject, NumberController { @@ -29,7 +31,9 @@ public class NumericTextFieldController: NSObject, NumberController { public init(textField: UITextField) { self.textField = textField super.init() + #if (os(iOS) || os(macOS)) textField.addDoneCancelToolbar(onDone: (target: self, action: #selector(doneButtonTappedForMyNumericTextField))) + #endif } @objc func doneButtonTappedForMyNumericTextField() { @@ -37,13 +41,13 @@ public class NumericTextFieldController: NSObject, NumberController { self.computation?.just(value: intText) textField.resignFirstResponder() } - + public func invalidate() { textField.text = nil } } - +#if (os(iOS) || os(macOS)) extension UITextField { func addDoneCancelToolbar(onDone: (target: Any, action: Selector)? = nil, onCancel: (target: Any, action: Selector)? = nil) { let onCancel = onCancel ?? (target: self, action: #selector(cancelButtonTapped)) @@ -65,3 +69,5 @@ extension UITextField { @objc func doneButtonTapped() { self.resignFirstResponder() } @objc func cancelButtonTapped() { self.resignFirstResponder() } } +#endif +#endif diff --git a/Sources/Controller/QuerySuggestions/QuerySuggestionsViewController.swift b/Sources/InstantSearch/QuerySuggestions/QuerySuggestionsViewController.swift similarity index 95% rename from Sources/Controller/QuerySuggestions/QuerySuggestionsViewController.swift rename to Sources/InstantSearch/QuerySuggestions/QuerySuggestionsViewController.swift index 1fea3836..43841d28 100644 --- a/Sources/Controller/QuerySuggestions/QuerySuggestionsViewController.swift +++ b/Sources/InstantSearch/QuerySuggestions/QuerySuggestionsViewController.swift @@ -5,56 +5,58 @@ // Created by Vladislav Fitc on 20/01/2020. // -import Foundation -import UIKit +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) +import UIKit public class QuerySuggestionsViewController: UITableViewController, HitsController, QueryInputController { - + public var onQuerySubmitted: ((String?) -> Void)? public var onQueryChanged: ((String?) -> Void)? - + public var hitsSource: HitsInteractor>? - + public var didSelect: ((Hit) -> Void)? - + public var isHighlightingInverted: Bool = false { didSet { tableView.reloadData() } } - + let cellID = "suggestionCellID" - + public override init(style: UITableView.Style) { super.init(style: style) tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID) } - + required init?(coder: NSCoder) { super.init(coder: coder) tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID) } - + public func scrollToTop() { tableView.scrollToFirstNonEmptySection() } - + public func reload() { tableView.reloadData() } - + public func setQuery(_ query: String?) { // external query change doesn't affect } - + public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return hitsSource?.numberOfHits() ?? 0 } - + public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: cellID) else { return .init() } - + if let hightlightSuggestion = hitsSource?.hit(atIndex: indexPath.row)?.hightlightedString(forKey: "query") { if let textLabel = cell.textLabel { textLabel.attributedText = NSAttributedString(highlightedString: hightlightSuggestion, inverted: isHighlightingInverted, attributes: [.font: UIFont.boldSystemFont(ofSize: textLabel.font.pointSize)]) @@ -68,5 +70,6 @@ public class QuerySuggestionsViewController: UITableViewController, HitsControll didSelect?(querySuggestionHit) onQuerySubmitted?(querySuggestionHit.object.query) } - + } +#endif diff --git a/Sources/Controller/SearchBox/SearchBarController.swift b/Sources/InstantSearch/SearchBox/SearchBarController.swift similarity index 91% rename from Sources/Controller/SearchBox/SearchBarController.swift rename to Sources/InstantSearch/SearchBox/SearchBarController.swift index cf950baa..94a3fb90 100644 --- a/Sources/Controller/SearchBox/SearchBarController.swift +++ b/Sources/InstantSearch/SearchBox/SearchBarController.swift @@ -6,8 +6,10 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit @available(iOS, deprecated: 13.0, message: "Use TextFieldController instead") @@ -15,7 +17,7 @@ public class SearchBarController: NSObject, QueryInputController { public var onQueryChanged: ((String?) -> Void)? public var onQuerySubmitted: ((String?) -> Void)? - + public let searchBar: UISearchBar public init(searchBar: UISearchBar) { @@ -24,11 +26,11 @@ public class SearchBarController: NSObject, QueryInputController { setupSearchBar() } - + public func setQuery(_ query: String?) { searchBar.text = query } - + private func setupSearchBar() { searchBar.delegate = self searchBar.returnKeyType = .search @@ -47,3 +49,4 @@ extension SearchBarController: UISearchBarDelegate { } } +#endif diff --git a/Sources/Controller/SearchBox/TextFieldController.swift b/Sources/InstantSearch/SearchBox/TextFieldController.swift similarity index 89% rename from Sources/Controller/SearchBox/TextFieldController.swift rename to Sources/InstantSearch/SearchBox/TextFieldController.swift index 0569096e..c1234e91 100644 --- a/Sources/Controller/SearchBox/TextFieldController.swift +++ b/Sources/InstantSearch/SearchBox/TextFieldController.swift @@ -6,28 +6,32 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public class TextFieldController: NSObject, QueryInputController { - + public var onQueryChanged: ((String?) -> Void)? public var onQuerySubmitted: ((String?) -> Void)? - - let textField: UITextField + + public let textField: UITextField public init(textField: UITextField) { self.textField = textField super.init() setupTextField() } - + +#if os(iOS) @available(iOS 13.0, *) public convenience init(searchBar: UISearchBar) { self.init(textField: searchBar.searchTextField) } - +#endif + public func setQuery(_ query: String?) { textField.text = query } @@ -36,17 +40,18 @@ public class TextFieldController: NSObject, QueryInputController { guard let searchText = textField.text else { return } onQueryChanged?(searchText) } - + @objc func textFieldSubmitted(textField: UITextField) { guard let searchText = textField.text else { return } onQuerySubmitted?(searchText) } - + private func setupTextField() { textField.returnKeyType = .search textField.addTarget(self, action: #selector(textFieldTextChanged), for: .editingChanged) textField.addTarget(self, action: #selector(textFieldTextChanged), for: .editingDidEnd) textField.addTarget(self, action: #selector(textFieldSubmitted), for: .editingDidEndOnExit) } - + } +#endif diff --git a/Sources/Controller/Segmented/SegmentedController.swift b/Sources/InstantSearch/Segmented/SegmentedController.swift similarity index 91% rename from Sources/Controller/Segmented/SegmentedController.swift rename to Sources/InstantSearch/Segmented/SegmentedController.swift index beb145fa..179858a6 100644 --- a/Sources/Controller/Segmented/SegmentedController.swift +++ b/Sources/InstantSearch/Segmented/SegmentedController.swift @@ -6,18 +6,20 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public class SegmentedController: NSObject, SelectableSegmentController { - + public typealias Key = Int - + public let segmentedControl: UISegmentedControl - + public var onClick: ((Int) -> Void)? - + public init(segmentedControl: UISegmentedControl) { self.segmentedControl = segmentedControl super.init() @@ -27,19 +29,20 @@ public class SegmentedController: NSObject, SelectableSegment public func setSelected(_ selected: Int?) { segmentedControl.selectedSegmentIndex = selected ?? UISegmentedControl.noSegment } - + public func setItems(items: [Int: String]) { segmentedControl.removeAllSegments() - + for item in items { segmentedControl.insertSegment(withTitle: item.value, at: item.key, animated: false) } } - + @objc private func didSelectSegment(_ segmentedControl: UISegmentedControl) { if segmentedControl.selectedSegmentIndex != UISegmentedControl.noSegment { onClick?(segmentedControl.selectedSegmentIndex) } } - + } +#endif diff --git a/Sources/Controller/Selectable/FilterSwitchController.swift b/Sources/InstantSearch/Selectable/FilterSwitchController.swift similarity index 88% rename from Sources/Controller/Selectable/FilterSwitchController.swift rename to Sources/InstantSearch/Selectable/FilterSwitchController.swift index 71c38c7b..d2342276 100644 --- a/Sources/Controller/Selectable/FilterSwitchController.swift +++ b/Sources/InstantSearch/Selectable/FilterSwitchController.swift @@ -6,33 +6,36 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(macOS)) import UIKit public class FilterSwitchController: SelectableController { - + public typealias Item = F - + public let `switch`: UISwitch - + public var onClick: ((Bool) -> Void)? - + public init(`switch`: UISwitch) { self.switch = `switch` `switch`.addTarget(self, action: #selector(didToggleSwitch), for: .valueChanged) } - + @objc func didToggleSwitch(_ switch: UISwitch) { onClick?(`switch`.isOn) } - + public func setSelected(_ isSelected: Bool) { self.switch.isOn = isSelected } - + public func setItem(_ item: F) { - + } - + } +#endif diff --git a/Sources/Controller/Selectable/SelectableFilterButtonController.swift b/Sources/InstantSearch/Selectable/SelectableFilterButtonController.swift similarity index 88% rename from Sources/Controller/Selectable/SelectableFilterButtonController.swift rename to Sources/InstantSearch/Selectable/SelectableFilterButtonController.swift index 4c351de3..3710da1e 100644 --- a/Sources/Controller/Selectable/SelectableFilterButtonController.swift +++ b/Sources/InstantSearch/Selectable/SelectableFilterButtonController.swift @@ -6,34 +6,37 @@ // Copyright © 2019 Algolia. All rights reserved. //b -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public class SelectableFilterButtonController: SelectableController { - + public typealias Item = F - + public let button: UIButton - + public var onClick: ((Bool) -> Void)? - + public init(button: UIButton) { self.button = button button.addTarget(self, action: #selector(didToggleButton), for: .touchUpInside) } - + @objc func didToggleButton(_ button: UIButton) { onClick?(!button.isSelected) } - + public func setSelected(_ isSelected: Bool) { self.button.isSelected = isSelected } - + public func setItem(_ item: F) { let title = DefaultPresenter.Filter.present(Filter(item)) button.setTitle(title, for: .normal) } - + } +#endif diff --git a/Sources/Controller/Sort By/SelectIndexController.swift b/Sources/InstantSearch/Sort By/SelectIndexController.swift similarity index 90% rename from Sources/Controller/Sort By/SelectIndexController.swift rename to Sources/InstantSearch/Sort By/SelectIndexController.swift index c77d0392..ec4b2bda 100644 --- a/Sources/Controller/Sort By/SelectIndexController.swift +++ b/Sources/InstantSearch/Sort By/SelectIndexController.swift @@ -6,8 +6,10 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit public class SelectIndexController: NSObject, SelectableSegmentController { @@ -38,3 +40,4 @@ public class SelectIndexController: NSObject, SelectableSegmentController { } } +#endif diff --git a/Sources/Controller/Stats/LabelStatsController.swift b/Sources/InstantSearch/Stats/LabelStatsController.swift similarity index 88% rename from Sources/Controller/Stats/LabelStatsController.swift rename to Sources/InstantSearch/Stats/LabelStatsController.swift index 726ae438..f2c3ae8e 100644 --- a/Sources/Controller/Stats/LabelStatsController.swift +++ b/Sources/InstantSearch/Stats/LabelStatsController.swift @@ -6,8 +6,10 @@ // Copyright © 2019 Algolia. All rights reserved. // -import Foundation +#if !InstantSearchCocoaPods import InstantSearchCore +#endif +#if canImport(UIKit) && (os(iOS) || os(tvOS) || os(macOS)) import UIKit extension Optional where Wrapped == Bool { @@ -30,7 +32,7 @@ public class LabelStatsController: StatsTextController { public init (label: UILabel) { self.label = label } - + public func setItem(_ item: String?) { label.text = item } @@ -38,15 +40,16 @@ public class LabelStatsController: StatsTextController { } public class AttributedLabelStatsController: ItemAttributedTextController { - + public let label: UILabel - + public init (label: UILabel) { self.label = label } - + public func setItem(_ item: NSAttributedString?) { label.attributedText = item } } +#endif diff --git a/Sources/InstantSearchCore/Common/AsyncOperation.swift b/Sources/InstantSearchCore/Common/AsyncOperation.swift new file mode 100644 index 00000000..822b27d1 --- /dev/null +++ b/Sources/InstantSearchCore/Common/AsyncOperation.swift @@ -0,0 +1,66 @@ +// +// AsyncOperation.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +open class AsyncOperation: Operation { + + public enum State: String { + case ready, executing, finished + + fileprivate var keyPath: String { + return "is" + rawValue.capitalized + } + } + + public var state = State.ready { + willSet { + willChangeValue(forKey: newValue.keyPath) + willChangeValue(forKey: state.keyPath) + } + didSet { + didChangeValue(forKey: oldValue.keyPath) + didChangeValue(forKey: state.keyPath) + } + } + +} + +extension AsyncOperation { + // NSOperation Overrides + override open var isReady: Bool { + return super.isReady && state == .ready + } + + override open var isExecuting: Bool { + return state == .executing + } + + override open var isFinished: Bool { + return state == .finished + } + + override open var isAsynchronous: Bool { + return true + } + + override open func start() { + if isCancelled { + state = .finished + return + } + + main() + state = .executing + } + + open override func cancel() { + state = .finished + } + +} diff --git a/Sources/InstantSearchCore/Common/Attribute.swift b/Sources/InstantSearchCore/Common/Attribute.swift new file mode 100644 index 00000000..d2fc663a --- /dev/null +++ b/Sources/InstantSearchCore/Common/Attribute.swift @@ -0,0 +1,13 @@ +// Attribute.swift +// AlgoliaSearch +// +// Created by Guy Daher on 10/12/2018. +// Copyright © 2018 Algolia. All rights reserved. +// +import Foundation + +extension Attribute { + + public static let tags = Attribute("_tags") + +} diff --git a/Sources/InstantSearchCore/Common/Connection.swift b/Sources/InstantSearchCore/Common/Connection.swift new file mode 100644 index 00000000..0c0cb135 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Connection.swift @@ -0,0 +1,16 @@ +// +// Connection.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 19/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol Connection { + + func connect() + func disconnect() + +} diff --git a/Sources/InstantSearchCore/Common/Constants.swift b/Sources/InstantSearchCore/Common/Constants.swift new file mode 100644 index 00000000..e80d98d7 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Constants.swift @@ -0,0 +1,27 @@ +// +// Constants.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 07/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public struct Constants { + public struct Defaults { + + // Hits + public static let hitsPerPage: UInt = 20 + public static let infiniteScrolling: InfiniteScrolling = .on(withOffset: 5) + public static let showItemsOnEmptyQuery: Bool = true + + // Refinement + public static let operatorRefinement = "or" + public static let refinementOperator: RefinementOperator = .or + public static let refinedFirst = true + + public static let limit = 10 + public static let areMultipleSelectionsAllowed = false + } +} diff --git a/Sources/InstantSearchCore/Common/Event/EventInteractor.swift b/Sources/InstantSearchCore/Common/Event/EventInteractor.swift new file mode 100644 index 00000000..82f5370a --- /dev/null +++ b/Sources/InstantSearchCore/Common/Event/EventInteractor.swift @@ -0,0 +1,16 @@ +// +// EventInteractor.swift +// InstantSearchCore +// +// Created by Guy Daher on 10/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol EventInteractor { + associatedtype Arg + + var onTriggered: Observer { get } + +} diff --git a/Sources/InstantSearchCore/Common/GeoLocation.swift b/Sources/InstantSearchCore/Common/GeoLocation.swift new file mode 100644 index 00000000..3407efe9 --- /dev/null +++ b/Sources/InstantSearchCore/Common/GeoLocation.swift @@ -0,0 +1,39 @@ +// +// GeoLocation.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 05/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public struct GeoLocation: Codable, RawRepresentable { + + public typealias RawValue = String + + public let latitude: Double + public let longitude: Double + + public init(latitude: Double, longitude: Double) { + self.latitude = latitude + self.longitude = longitude + } + + public init?(rawValue: RawValue) { + let components = rawValue.split(separator: ",") + guard + components.count == 2, + let latitude = Double(components[0]), + let longitude = Double(components[1]) else + { + return nil + } + self.init(latitude: latitude, longitude: longitude) + } + + public var rawValue: String { + return "\(latitude),\(longitude)" + } + +} diff --git a/Sources/InstantSearchCore/Common/Geolocated.swift b/Sources/InstantSearchCore/Common/Geolocated.swift new file mode 100644 index 00000000..2039e9a4 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Geolocated.swift @@ -0,0 +1,13 @@ +// +// Geolocated.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/10/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol Geolocated { + var geolocation: Point? { get } +} diff --git a/Sources/InstantSearchCore/Common/Hit+Place.swift b/Sources/InstantSearchCore/Common/Hit+Place.swift new file mode 100644 index 00000000..f1b1ee9b --- /dev/null +++ b/Sources/InstantSearchCore/Common/Hit+Place.swift @@ -0,0 +1,84 @@ +// +// Hit+Place.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 24/10/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension Hit { + + func getBestHighlightedForm(from highlightResults: [HighlightResult]) -> HighlightedString? { + + guard let defaultValue = highlightResults.first?.value.taggedString.input else { return nil } + + let bestAttributesOrder = highlightResults + .filter { $0.matchLevel != .none } + .enumerated() + .sorted { lhs, rhs in + // If matched words count is the same, use the earliest occurency in the list + guard lhs.element.matchedWords.count != rhs.element.matchedWords.count else { + return lhs.offset < rhs.offset + } + return lhs.element.matchedWords.count > rhs.element.matchedWords.count + } + .map { $0.offset } + .first + + guard let theBestAttributeIndex = bestAttributesOrder else { return HighlightedString(string: defaultValue) } + + return highlightResults[theBestAttributeIndex].value + + } + + func getBestHighlightedForm(forKey key: String) -> HighlightedString? { + + guard case .dictionary(let dictionary) = highlightResult else { + return nil + } + + let highlightResults: [HighlightResult] + + switch dictionary[key] { + case .array(let highlightResultsList): + highlightResults = highlightResultsList.compactMap { + if case .value(let val) = $0 { + return val + } else { + return nil + } + } + + case .value(let value): + highlightResults = [value] + + default: + return nil + } + + return getBestHighlightedForm(from: highlightResults) + + } + +} + +extension Hit: CustomStringConvertible where T == Place { + + public var description: String { + let cityKey = object.isCity! ? "locale_names" : "city" + let country = getBestHighlightedForm(forKey: "country") + let county = getBestHighlightedForm(forKey: "county") + let city = getBestHighlightedForm(forKey: cityKey) + let streetName = object.isCity! ? nil : getBestHighlightedForm(forKey: "locale_names") + + return [streetName, city, county, country] + .compactMap { $0 } + .filter { !$0.taggedString.input.isEmpty } + .map { $0.taggedString.output } + .joined(separator: ", ") + + } + +} diff --git a/Sources/InstantSearchCore/Common/IndexQueryState.swift b/Sources/InstantSearchCore/Common/IndexQueryState.swift new file mode 100644 index 00000000..19aaf900 --- /dev/null +++ b/Sources/InstantSearchCore/Common/IndexQueryState.swift @@ -0,0 +1,36 @@ +// +// IndexQueryState.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 27/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@_exported import AlgoliaSearchClient +/// Structure containing all necessary components to perform a search + +public struct IndexQueryState { + + /// Index in which search will be performed + public var indexName: IndexName + + /// Query describing a search request + public var query: Query + + public init(indexName: IndexName, query: Query = .init()) { + self.indexName = indexName + self.query = query + } + +} + +extension IndexQueryState: Builder {} + +extension Array where Element == IndexQueryState { + + init(indices: [AlgoliaSearchClient.Index], query: Query = .init()) { + self = indices.map { IndexQueryState(indexName: $0.name, query: query) } + } + +} diff --git a/Sources/InstantSearchCore/Common/Item/ItemController.swift b/Sources/InstantSearchCore/Common/Item/ItemController.swift new file mode 100644 index 00000000..f4ebae09 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Item/ItemController.swift @@ -0,0 +1,25 @@ +// +// ItemController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 31/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol ItemController: class { + + associatedtype Item + + func setItem(_ item: Item) + func invalidate() + +} + +public extension ItemController { + + func invalidate() { + } + +} diff --git a/Sources/InstantSearchCore/Common/Item/ItemInteractor+Controller.swift b/Sources/InstantSearchCore/Common/Item/ItemInteractor+Controller.swift new file mode 100644 index 00000000..b7eaeb11 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Item/ItemInteractor+Controller.swift @@ -0,0 +1,42 @@ +// +// ItemInteractor+Controller.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension ItemInteractor { + + struct ControllerConnection: Connection where Controller.Item == Output { + + public let interactor: ItemInteractor + public let controller: Controller + public let presenter: Presenter + + public func connect() { + interactor.onItemChanged.subscribePast(with: controller) { controller, item in + controller.setItem(self.presenter(item)) + }.onQueue(.main) + } + + public func disconnect() { + interactor.onItemChanged.cancelSubscription(for: controller) + } + + } + +} + +public extension ItemInteractor { + + @discardableResult func connectController(_ controller: Controller, + presenter: @escaping Presenter) -> ControllerConnection { + let connection = ControllerConnection(interactor: self, controller: controller, presenter: presenter) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Common/Item/ItemInteractor.swift b/Sources/InstantSearchCore/Common/Item/ItemInteractor.swift new file mode 100644 index 00000000..98ef4fa7 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Item/ItemInteractor.swift @@ -0,0 +1,26 @@ +// +// ItemInteractor.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 31/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class ItemInteractor { + + public var item: Item { + didSet { + onItemChanged.fire(item) + } + } + + public let onItemChanged: Observer + + init(item: Item) { + self.item = item + self.onItemChanged = .init() + } + +} diff --git a/Sources/InstantSearchCore/Common/Number/Boundable.swift b/Sources/InstantSearchCore/Common/Number/Boundable.swift new file mode 100644 index 00000000..d47ae2e7 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Number/Boundable.swift @@ -0,0 +1,15 @@ +// +// NumberRangeInteractor+Searcher.swift +// InstantSearchCore +// +// Created by Guy Daher on 14/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol Boundable: class { + associatedtype Number: Comparable & DoubleRepresentable + + func applyBounds(bounds: ClosedRange?) +} diff --git a/Sources/InstantSearchCore/Common/Number/DoubleRepresentable.swift b/Sources/InstantSearchCore/Common/Number/DoubleRepresentable.swift new file mode 100644 index 00000000..e44c2425 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Number/DoubleRepresentable.swift @@ -0,0 +1,39 @@ +// +// DoubleRepresentable.swift +// InstantSearchCore +// +// Created by Guy Daher on 14/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol DoubleRepresentable { + + init(_ double: Double) + func toDouble() -> Double +} + +extension Int: DoubleRepresentable { + public func toDouble() -> Double { + return Double(self) + } +} + +extension UInt: DoubleRepresentable { + public func toDouble() -> Double { + return Double(self) + } +} + +extension Float: DoubleRepresentable { + public func toDouble() -> Double { + return Double(self) + } +} + +extension Double: DoubleRepresentable { + public func toDouble() -> Double { + return Double(self) + } +} diff --git a/Sources/InstantSearchCore/Common/Point+CoreLocation.swift b/Sources/InstantSearchCore/Common/Point+CoreLocation.swift new file mode 100644 index 00000000..e7ab7f44 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Point+CoreLocation.swift @@ -0,0 +1,35 @@ +// +// Point+CoreLocation.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +import CoreLocation + +public extension Point { + + init(_ coordinate: CLLocationCoordinate2D) { + self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) + } + +} + +public extension CLLocationCoordinate2D { + + init(_ geolocation: Point) { + self.init(latitude: geolocation.latitude, longitude: geolocation.longitude) + } + +} + +public extension CLLocation { + + convenience init(_ geolocation: Point) { + self.init(latitude: geolocation.latitude, longitude: geolocation.longitude) + } + +} diff --git a/Sources/InstantSearchCore/Common/Presenter/Presenter.swift b/Sources/InstantSearchCore/Common/Presenter/Presenter.swift new file mode 100644 index 00000000..f4b9338f --- /dev/null +++ b/Sources/InstantSearchCore/Common/Presenter/Presenter.swift @@ -0,0 +1,11 @@ +// +// Presenter.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias Presenter = (I) -> O diff --git a/Sources/InstantSearchCore/Common/QuerySuggestion.swift b/Sources/InstantSearchCore/Common/QuerySuggestion.swift new file mode 100644 index 00000000..548871b0 --- /dev/null +++ b/Sources/InstantSearchCore/Common/QuerySuggestion.swift @@ -0,0 +1,16 @@ +// +// QuerySuggestion.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 20/01/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation + +public struct QuerySuggestion: Codable { + + public let query: String + public let popularity: Int + +} diff --git a/Sources/InstantSearchCore/Common/Reloadable.swift b/Sources/InstantSearchCore/Common/Reloadable.swift new file mode 100644 index 00000000..88f028db --- /dev/null +++ b/Sources/InstantSearchCore/Common/Reloadable.swift @@ -0,0 +1,15 @@ +// +// Reloadable.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 13/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol Reloadable { + + func reload() + +} diff --git a/Sources/InstantSearchCore/Common/ResultUpdatable.swift b/Sources/InstantSearchCore/Common/ResultUpdatable.swift new file mode 100644 index 00000000..b609877c --- /dev/null +++ b/Sources/InstantSearchCore/Common/ResultUpdatable.swift @@ -0,0 +1,23 @@ +// +// ResultUpdatable.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 12/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol ResultUpdatable { + + /// Result type + associatedtype Result + + /// Triggered once result is updated + var onResultsUpdated: Observer { get } + + /// Updates result + /// - returns: Operation encapsulating result update + func update(_ result: Result) -> Operation + +} diff --git a/Sources/InstantSearchCore/Common/Selectable/SelectableController.swift b/Sources/InstantSearchCore/Common/Selectable/SelectableController.swift new file mode 100644 index 00000000..32ba7774 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Selectable/SelectableController.swift @@ -0,0 +1,17 @@ +// +// SelectableController.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 03/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol SelectableController: ItemController { + + var onClick: ((Bool) -> Void)? { get set } + + func setSelected(_ isSelected: Bool) + +} diff --git a/Sources/InstantSearchCore/Common/Selectable/SelectableInteractor.swift b/Sources/InstantSearchCore/Common/Selectable/SelectableInteractor.swift new file mode 100644 index 00000000..a94ff464 --- /dev/null +++ b/Sources/InstantSearchCore/Common/Selectable/SelectableInteractor.swift @@ -0,0 +1,33 @@ +// +// SelectableInteractor.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 03/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class SelectableInteractor: ItemInteractor { + + public var isSelected: Bool { + didSet { + onSelectedChanged.fire(isSelected) + } + } + + public let onSelectedChanged: Observer + public let onSelectedComputed: Observer + + public override init(item: Item) { + self.isSelected = false + self.onSelectedChanged = .init() + self.onSelectedComputed = .init() + super.init(item: item) + } + + public func computeIsSelected(selecting: Bool) { + onSelectedComputed.fire(selecting) + } + +} diff --git a/Sources/InstantSearchCore/Common/SelectableList/SelectableListController.swift b/Sources/InstantSearchCore/Common/SelectableList/SelectableListController.swift new file mode 100644 index 00000000..69a56cda --- /dev/null +++ b/Sources/InstantSearchCore/Common/SelectableList/SelectableListController.swift @@ -0,0 +1,19 @@ +// +// SelectableListController.swift +// InstantSearchCore +// +// Created by Guy Daher on 26/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol SelectableListController: class, Reloadable { + + associatedtype Item + + var onClick: ((Item) -> Void)? { get set } + + func setSelectableItems(selectableItems: [SelectableItem]) + +} diff --git a/Sources/InstantSearchCore/Common/SelectableList/SelectableListInteractor.swift b/Sources/InstantSearchCore/Common/SelectableList/SelectableListInteractor.swift new file mode 100644 index 00000000..f67b89c4 --- /dev/null +++ b/Sources/InstantSearchCore/Common/SelectableList/SelectableListInteractor.swift @@ -0,0 +1,71 @@ +// +// SelectableListInteractor.swift +// InstantSearchCore +// +// Created by Guy Daher on 18/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public enum SelectionMode { + case single + case multiple +} + +public class SelectableListInteractor { + + public var items: [Item] { + didSet { + if oldValue != items { + onItemsChanged.fire(items) + } + } + } + + public var selections: Set { + didSet { + if oldValue != selections { + onSelectionsChanged.fire(selections) + } + } + } + + public let onItemsChanged: Observer<[Item]> + public let onSelectionsChanged: Observer> + public let onSelectionsComputed: Observer> + + public let selectionMode: SelectionMode + + public init(items: [Item] = [], selectionMode: SelectionMode) { + self.items = items + self.selections = [] + self.onItemsChanged = .init() + self.onSelectionsChanged = .init() + self.onSelectionsComputed = .init() + self.selectionMode = selectionMode + } + + public func computeSelections(selectingItemForKey key: Key) { + + let computedSelections: Set + + switch (selectionMode, selections.contains(key)) { + case (.single, true): + computedSelections = [] + + case (.single, false): + computedSelections = [key] + + case (.multiple, true): + computedSelections = selections.subtracting([key]) + + case (.multiple, false): + computedSelections = selections.union([key]) + } + + onSelectionsComputed.fire(computedSelections) + + } + +} diff --git a/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentController.swift b/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentController.swift new file mode 100644 index 00000000..d685d4e7 --- /dev/null +++ b/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentController.swift @@ -0,0 +1,20 @@ +// +// SelectableSegmentController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 13/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol SelectableSegmentController: class { + + associatedtype SegmentKey: Hashable + + var onClick: ((SegmentKey) -> Void)? { get set } + + func setSelected(_ selected: SegmentKey?) + func setItems(items: [SegmentKey: String]) + +} diff --git a/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentInteractor.swift b/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentInteractor.swift new file mode 100644 index 00000000..3b79b443 --- /dev/null +++ b/Sources/InstantSearchCore/Common/SelectableSegment/SelectableSegmentInteractor.swift @@ -0,0 +1,42 @@ +// +// SelectableSegmentInteractor.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class SelectableSegmentInteractor { + + public var items: [SegmentKey: Segment] { + didSet { + onItemsChanged.fire(items) + } + } + + public var selected: SegmentKey? { + didSet { + onSelectedChanged.fire(selected) + } + } + + public let onItemsChanged: Observer<[SegmentKey: Segment]> + public let onSelectedChanged: Observer + public let onSelectedComputed: Observer + + public init(items: [SegmentKey: Segment]) { + self.items = items + self.selected = .none + self.onItemsChanged = .init() + self.onSelectedChanged = .init() + self.onSelectedComputed = .init() + onItemsChanged.fire(items) + } + + public func computeSelected(selecting keyToSelect: SegmentKey?) { + onSelectedComputed.fire(keyToSelect) + } + +} diff --git a/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersConnector.swift b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersConnector.swift new file mode 100644 index 00000000..8429af81 --- /dev/null +++ b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersConnector.swift @@ -0,0 +1,37 @@ +// +// CurrentFiltersConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class CurrentFiltersConnector: Connection { + + public let filterState: FilterState + public let groupIDs: Set? + public let interactor: CurrentFiltersInteractor + + public let filterStateConnection: Connection + + public init(filterState: FilterState, + groupIDs: Set? = nil, + interactor: CurrentFiltersInteractor = .init()) { + self.filterState = filterState + self.groupIDs = groupIDs + self.interactor = interactor + self.filterStateConnection = interactor.connectFilterState(filterState, filterGroupIDs: groupIDs) + + } + + public func connect() { + filterStateConnection.connect() + } + + public func disconnect() { + filterStateConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor+FilterState.swift b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor+FilterState.swift new file mode 100644 index 00000000..a7b25e43 --- /dev/null +++ b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor+FilterState.swift @@ -0,0 +1,91 @@ +// +// CurrentFiltersInteractor+FilterState.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 19/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension CurrentFiltersInteractor { + + struct FilterStateConnection: Connection { + + public let interactor: CurrentFiltersInteractor + public let filterState: FilterState + public let filterGroupIDs: Set? + + public init(interactor: CurrentFiltersInteractor, + filterState: FilterState, + filterGroupIDs: Set? = nil) { + self.interactor = interactor + self.filterState = filterState + self.filterGroupIDs = filterGroupIDs + } + + public func connect() { + + let filterGroupIDs = self.filterGroupIDs + + filterState.onChange.subscribePast(with: interactor) { [weak filterState] interactor, _ in + guard let filterState = filterState else { return } + if let filterGroupIDs = filterGroupIDs { + interactor.items = filterState.getFiltersAndID().filter { filterGroupIDs.contains($0.id) } + } else { + interactor.items = filterState.getFiltersAndID() + } + } + + interactor.onItemsComputed.subscribePast(with: filterState) { filterState, items in + + if let filterGroupIDs = filterGroupIDs { + filterState.filters.removeAll(fromGroupWithIDs: Array(filterGroupIDs)) + items.forEach({ (filterAndID) in + filterState.filters.add(filterAndID.filter.filter, toGroupWithID: filterAndID.id) + }) + } else { + filterState.filters.removeAll() + items.forEach({ (filterAndID) in + filterState.filters.add(filterAndID.filter.filter, toGroupWithID: filterAndID.id) + }) + } + + filterState.notifyChange() + } + + } + + public func disconnect() { + filterState.onChange.cancelSubscription(for: interactor) + interactor.onItemsComputed.cancelSubscription(for: filterState) + } + + } + +} + +public extension CurrentFiltersInteractor { + + @discardableResult func connectFilterState(_ filterState: FilterState, + filterGroupIDs: Set? = nil) -> FilterStateConnection { + let connection = FilterStateConnection(interactor: self, filterState: filterState, filterGroupIDs: filterGroupIDs) + connection.connect() + return connection + } + + func connectFilterState(_ filterState: FilterState, + filterGroupID: FilterGroup.ID?) -> FilterStateConnection { + if let filterGroupID = filterGroupID { + return connectFilterState(filterState, filterGroupIDs: Set([filterGroupID])) + } else { + return connectFilterState(filterState) + } + } + + func connectFilterState(_ filterState: FilterState, + filterGroupID: FilterGroup.ID) -> FilterStateConnection { + return connectFilterState(filterState, filterGroupIDs: Set([filterGroupID])) + } + +} diff --git a/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor.swift b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor.swift new file mode 100644 index 00000000..29b5e6a4 --- /dev/null +++ b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersInteractor.swift @@ -0,0 +1,23 @@ +// +// CurrentFiltersInteractor.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias CurrentFiltersInteractor = ItemsListInteractor + +public struct FilterAndID: Hashable { + public let filter: Filter + public let id: FilterGroup.ID + public var text: String + + public init(filter: Filter, id: FilterGroup.ID, text: String = "") { + self.filter = filter + self.id = id + self.text = text + } +} diff --git a/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersListController.swift b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersListController.swift new file mode 100644 index 00000000..90836f43 --- /dev/null +++ b/Sources/InstantSearchCore/CurrentFilters/CurrentFiltersListController.swift @@ -0,0 +1,11 @@ +// +// CurrentFiltersListController.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol CurrentFiltersListController: ItemListController where Item == FilterAndID {} diff --git a/Sources/InstantSearchCore/CurrentFilters/ItemListController.swift b/Sources/InstantSearchCore/CurrentFilters/ItemListController.swift new file mode 100644 index 00000000..80be3af2 --- /dev/null +++ b/Sources/InstantSearchCore/CurrentFilters/ItemListController.swift @@ -0,0 +1,25 @@ +// +// CurrentFiltersController.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol ItemListController: class { + + associatedtype Item: Hashable + + func setItems(_ items: [Item]) + + // TODO: Potentially we could change from Item to a Int which is position of item in list. + // It is enough to identify the items in interactor, so in that way we only pass the + // Filter without the ID + var onRemoveItem: ((Item) -> Void)? { get set } + + func reload() +} + +public protocol CurrentFiltersController: ItemListController where Item == FilterAndID {} diff --git a/Sources/InstantSearchCore/CurrentFilters/ItemListInteractor+Controller.swift b/Sources/InstantSearchCore/CurrentFilters/ItemListInteractor+Controller.swift new file mode 100644 index 00000000..d05b8ebd --- /dev/null +++ b/Sources/InstantSearchCore/CurrentFilters/ItemListInteractor+Controller.swift @@ -0,0 +1,62 @@ +// +// ItemListInteractor+Controller.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 19/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension ItemsListInteractor { + + struct ControllerConnection: Connection where Controller.Item == Item, Item == FilterAndID { + + public let interactor: ItemsListInteractor + public let controller: Controller + public let presenter: Presenter + + public init(interactor: ItemsListInteractor, + controller: Controller, + presenter: @escaping Presenter = DefaultPresenter.Filter.present) { + self.interactor = interactor + self.controller = controller + self.presenter = presenter + } + + public func connect() { + + controller.onRemoveItem = { [weak interactor] item in + let filterAndID = FilterAndID(filter: item.filter, id: item.id) + interactor?.remove(item: filterAndID) + } + + let presenter = self.presenter + + interactor.onItemsChanged.subscribePast(with: controller) { controller, items in + let itemsWithPresenterApplied = items.map { FilterAndID(filter: $0.filter, id: $0.id, text: presenter($0.filter))} + controller.setItems(itemsWithPresenterApplied) + controller.reload() + }.onQueue(.main) + + } + + public func disconnect() { + controller.onRemoveItem = .none + interactor.onItemsChanged.cancelSubscription(for: controller) + } + + } + +} + +public extension ItemsListInteractor { + + @discardableResult func connectController(_ controller: C, + presenter: @escaping Presenter = DefaultPresenter.Filter.present) -> ControllerConnection where C.Item == Item, Item == FilterAndID { + let connection = ControllerConnection(interactor: self, controller: controller, presenter: presenter) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/CurrentFilters/ItemsListInteractor.swift b/Sources/InstantSearchCore/CurrentFilters/ItemsListInteractor.swift new file mode 100644 index 00000000..c32d7633 --- /dev/null +++ b/Sources/InstantSearchCore/CurrentFilters/ItemsListInteractor.swift @@ -0,0 +1,38 @@ +// +// ItemsListInteractor.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class ItemsListInteractor { + + public var items: Set { + didSet { + if oldValue != items { + onItemsChanged.fire(items) + } + } + } + + public let onItemsChanged: Observer> + public let onItemsComputed: Observer> + + public init(items: Set = []) { + self.items = items + self.onItemsChanged = .init() + self.onItemsComputed = .init() + } + + public func remove(item: Item) { + onItemsComputed.fire(items.subtracting([item])) + } + + public func add(item: Item) { + onItemsComputed.fire(items.union([item])) + } + +} diff --git a/Sources/InstantSearchCore/Extensions/Optional+Collection.swift b/Sources/InstantSearchCore/Extensions/Optional+Collection.swift new file mode 100644 index 00000000..630c0b72 --- /dev/null +++ b/Sources/InstantSearchCore/Extensions/Optional+Collection.swift @@ -0,0 +1,22 @@ +// +// Optional+Collection.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 12/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension Optional where Wrapped: Collection { + + var isNilOrEmpty: Bool { + switch self { + case .none: + return true + case .some(let collection): + return collection.isEmpty + } + } + +} diff --git a/Sources/InstantSearchCore/Extensions/Optional+String.swift b/Sources/InstantSearchCore/Extensions/Optional+String.swift new file mode 100644 index 00000000..e784bfac --- /dev/null +++ b/Sources/InstantSearchCore/Extensions/Optional+String.swift @@ -0,0 +1,22 @@ +// +// Optional+String.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 13/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension Optional where Wrapped == String { + + var isNilOrEmpty: Bool { + switch self { + case .some(let string): + return string.isEmpty + case .none: + return true + } + } + +} diff --git a/Sources/InstantSearchCore/Extensions/Query+Facets.swift b/Sources/InstantSearchCore/Extensions/Query+Facets.swift new file mode 100644 index 00000000..ca05b869 --- /dev/null +++ b/Sources/InstantSearchCore/Extensions/Query+Facets.swift @@ -0,0 +1,17 @@ +// +// Query+Facets.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 17/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension Query { + + mutating func updateQueryFacets(with attribute: Attribute) { + facets = (facets ?? []).union([attribute]) + } + +} diff --git a/Sources/InstantSearchCore/Extensions/Result+ConvenientInit.swift b/Sources/InstantSearchCore/Extensions/Result+ConvenientInit.swift new file mode 100644 index 00000000..e19e1012 --- /dev/null +++ b/Sources/InstantSearchCore/Extensions/Result+ConvenientInit.swift @@ -0,0 +1,50 @@ +// +// Result.swift +// InstantSearchCore-iOS +// +// Created by Guy Daher on 07/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension Result where Failure == Error { + + init(value: Success?, error: Failure?) { + switch (value, error) { + case (_, .some(let error)): + self = .failure(error) + case (.some(let value), _): + self = .success(value) + default: + self = .failure(ResultError.invalidResultInput) + } + } + +} + +public extension Result where Success: Decodable, Failure == Error { + + init(rawValue: [String: Any]?, error: Failure?) { + switch (rawValue, error) { + case (.none, .some(let error)): + self = .failure(error) + case (.some(let rawValue), .none): + do { + let data = try JSONSerialization.data(withJSONObject: rawValue, options: []) + let decoder = JSONDecoder() + let result = try decoder.decode(Success.self, from: data) + self = .success(result) + } catch let error { + self = .failure(error) + } + default: + self = .failure(ResultError.invalidResultInput) + } + } + +} + +public enum ResultError: Error { + case invalidResultInput +} diff --git a/Sources/InstantSearchCore/FacetList/FacetListConnector.swift b/Sources/InstantSearchCore/FacetList/FacetListConnector.swift new file mode 100644 index 00000000..d3fb1aaf --- /dev/null +++ b/Sources/InstantSearchCore/FacetList/FacetListConnector.swift @@ -0,0 +1,120 @@ +// +// FacetListConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 26/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class FacetListConnector: Connection { + + public enum Searcher { + case singleIndex(SingleIndexSearcher) + case facet(FacetSearcher) + } + + public let filterState: FilterState + public let searcher: Searcher + public let interactor: FacetListInteractor + public let attribute: Attribute + + public let filterStateConnection: Connection + public let searcherConnection: Connection + + internal init(searcher: Searcher, + filterState: FilterState, + interactor: FacetListInteractor = .init(), + attribute: Attribute, + operator: RefinementOperator, + groupName: String? = nil) { + + self.filterState = filterState + self.searcher = searcher + self.interactor = interactor + self.attribute = attribute + + self.filterStateConnection = interactor.connectFilterState(filterState, + with: attribute, + operator: `operator`, + groupName: groupName) + switch searcher { + case .facet(let facetSearcher): + searcherConnection = interactor.connectFacetSearcher(facetSearcher) + + case .singleIndex(let singleIndexSearcher): + searcherConnection = interactor.connectSearcher(singleIndexSearcher, with: attribute) + } + + } + + public convenience init(searcher: SingleIndexSearcher, + filterState: FilterState, + attribute: Attribute, + operator: RefinementOperator, + groupName: String? = nil, + interactor: FacetListInteractor = .init()) { + self.init(searcher: .singleIndex(searcher), + filterState: filterState, + interactor: interactor, + attribute: attribute, + operator: `operator`, + groupName: groupName) + } + + public convenience init(searcher: FacetSearcher, + filterState: FilterState, + attribute: Attribute, + operator: RefinementOperator, + groupName: String? = nil, + interactor: FacetListInteractor = .init()) { + self.init(searcher: .facet(searcher), + filterState: filterState, + interactor: interactor, + attribute: attribute, + operator: `operator`, + groupName: groupName) + } + + public convenience init(searcher: SingleIndexSearcher, + filterState: FilterState, + attribute: Attribute, + facets: [Facet], + selectionMode: SelectionMode, + operator: RefinementOperator, + groupName: String? = nil) { + self.init(searcher: .singleIndex(searcher), + filterState: filterState, + interactor: .init(facets: facets, selectionMode: selectionMode), + attribute: attribute, + operator: `operator`, + groupName: groupName) + } + + public convenience init(searcher: FacetSearcher, + filterState: FilterState, + attribute: Attribute, + facets: [Facet], + selectionMode: SelectionMode, + operator: RefinementOperator, + groupName: String? = nil) { + self.init(searcher: .facet(searcher), + filterState: filterState, + interactor: .init(facets: facets, selectionMode: selectionMode), + attribute: attribute, + operator: `operator`, + groupName: groupName) + } + + public func connect() { + filterStateConnection.connect() + searcherConnection.connect() + } + + public func disconnect() { + filterStateConnection.disconnect() + searcherConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/FacetList/FacetListController.swift b/Sources/InstantSearchCore/FacetList/FacetListController.swift new file mode 100644 index 00000000..5c050632 --- /dev/null +++ b/Sources/InstantSearchCore/FacetList/FacetListController.swift @@ -0,0 +1,11 @@ +// +// FacetListController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 17/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol FacetListController: SelectableListController where Item == Facet {} diff --git a/Sources/InstantSearchCore/FacetList/FacetListInteractor+Controller.swift b/Sources/InstantSearchCore/FacetList/FacetListInteractor+Controller.swift new file mode 100644 index 00000000..dcd84019 --- /dev/null +++ b/Sources/InstantSearchCore/FacetList/FacetListInteractor+Controller.swift @@ -0,0 +1,90 @@ +// +// FacetListInteractor+Controller.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension FacetList { + + public struct ControllerConnection: Connection { + + public let facetListInteractor: FacetListInteractor + public let controller: Controller + public let presenter: SelectableListPresentable? + public let externalReload: Bool + + public init(facetListInteractor: FacetListInteractor, + controller: Controller, + presenter: SelectableListPresentable? = nil, + externalReload: Bool = false) { + self.facetListInteractor = facetListInteractor + self.controller = controller + self.presenter = presenter + self.externalReload = externalReload + } + + public func connect() { + + ControllerConnection.setControllerItemsWith(facets: facetListInteractor.items, selections: facetListInteractor.selections, controller: controller, presenter: presenter) + + controller.onClick = { [weak facetListInteractor] facet in + facetListInteractor?.computeSelections(selectingItemForKey: facet.value) + } + + facetListInteractor.onItemsChanged.subscribePast(with: controller) { [weak interactor = self.facetListInteractor, presenter = self.presenter] controller, facets in + guard let interactor = interactor else { return } + ControllerConnection.setControllerItemsWith(facets: facets, selections: interactor.selections, controller: controller, presenter: presenter) + } + + facetListInteractor.onSelectionsChanged.subscribePast(with: controller) { [weak interactor = self.facetListInteractor, presenter = self.presenter] controller, selections in + guard let interactor = interactor else { return } + ControllerConnection.setControllerItemsWith(facets: interactor.items, selections: selections, controller: controller, presenter: presenter) + } + + } + + public func disconnect() { + controller.onClick = nil + facetListInteractor.onItemsChanged.cancelSubscription(for: controller) + facetListInteractor.onSelectionsChanged.cancelSubscription(for: controller) + } + + /// Add missing refinements with a count of 0 to all returned facets + /// Example: if in result we have color: [(red, 10), (green, 5)] and that in the refinements + /// we have "color: red" and "color: yellow", the final output would be [(red, 10), (green, 5), (yellow, 0)] + private static func merge(_ facets: [Facet], withSelectedValues selections: Set) -> [SelectableItem] { + return facets.map { SelectableItem($0, selections.contains($0.value)) } + } + + private static func setControllerItemsWith(facets: [Facet], selections: Set, controller: Controller, presenter: SelectableListPresentable?) { + let updatedFacets = merge(facets, withSelectedValues: selections) + let sortedFacetValues = presenter?.transform(refinementFacets: updatedFacets) ?? updatedFacets + controller.setSelectableItems(selectableItems: sortedFacetValues) + DispatchQueue.main.async { [weak controller] in + controller?.reload() + } + } + + } + +} + +public extension FacetListInteractor { + + @discardableResult func connectController(_ controller: C, + with presenter: SelectableListPresentable? = nil, + externalReload: Bool = false) -> FacetList.ControllerConnection { + + let connection = FacetList.ControllerConnection(facetListInteractor: self, + controller: controller, + presenter: presenter, + externalReload: externalReload) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/FacetList/FacetListInteractor+FacetSearcher.swift b/Sources/InstantSearchCore/FacetList/FacetListInteractor+FacetSearcher.swift new file mode 100644 index 00000000..82b7631b --- /dev/null +++ b/Sources/InstantSearchCore/FacetList/FacetListInteractor+FacetSearcher.swift @@ -0,0 +1,54 @@ +// +// FacetListInteractor+FacetSearcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension FacetListInteractor { + + struct FacetSearcherConnection: Connection { + + public let interactor: FacetListInteractor + public let searcher: FacetSearcher + + public func connect() { + + // When new facet search results then update items + + searcher.onResults.subscribePast(with: interactor) { interactor, facetResults in + interactor.update(facetResults) + } + + // For the case of SFFV, very possible that we forgot to add the + // attribute as searchable in `attributesForFaceting`. + + searcher.onError.subscribe(with: interactor) { _, error in + if let error = error.1 as? HTTPError, error.statusCode == 400 { + assertionFailure(error.message?.description ?? "") + } + } + + } + + public func disconnect() { + searcher.onResults.cancelSubscription(for: interactor) + searcher.onError.cancelSubscription(for: interactor) + } + + } + +} + +public extension FacetListInteractor { + + @discardableResult func connectFacetSearcher(_ facetSearcher: FacetSearcher) -> FacetSearcherConnection { + let connection = FacetSearcherConnection(interactor: self, searcher: facetSearcher) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/FacetList/FacetListInteractor+FilterState.swift b/Sources/InstantSearchCore/FacetList/FacetListInteractor+FilterState.swift new file mode 100644 index 00000000..24847e9e --- /dev/null +++ b/Sources/InstantSearchCore/FacetList/FacetListInteractor+FilterState.swift @@ -0,0 +1,100 @@ +// +// FacetListInteractor+FilterState.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public enum FacetList { + + public struct FilterStateConnection: Connection { + + public let interactor: FacetListInteractor + public let filterState: FilterState + public let attribute: Attribute + public let `operator`: RefinementOperator + public let groupName: String? + + public func connect() { + let groupName = self.groupName ?? attribute.rawValue + let groupID: FilterGroup.ID + switch `operator` { + case .and: + groupID = .and(name: groupName) + case .or: + groupID = .or(name: groupName, filterType: .facet) + } + connect(filterState, to: interactor, with: attribute, via: groupID) + } + + public func disconnect() { + interactor.onSelectionsComputed.cancelSubscription(for: filterState) + filterState.onChange.cancelSubscription(for: interactor) + } + + private func connect(_ filterState: FilterState, + to interactor: FacetListInteractor, + with attribute: Attribute, + via groupID: FilterGroup.ID) { + whenSelectionsComputedThenUpdateFilterState(interactor: interactor, filterState: filterState, attribute: attribute, via: groupID) + whenFilterStateChangedThenUpdateSelections(interactor: interactor, filterState: filterState, via: groupID) + } + + private func whenSelectionsComputedThenUpdateFilterState(interactor: FacetListInteractor, + filterState: FilterState, + attribute: Attribute, + via groupID: FilterGroup.ID) { + interactor.onSelectionsComputed.subscribePast(with: filterState) { filterState, selections in + let filters = selections.map { Filter.Facet(attribute: attribute, stringValue: $0) } + filterState.removeAll(fromGroupWithID: groupID) + filterState.addAll(filters: filters, toGroupWithID: groupID) + filterState.notifyChange() + } + + } + + private func whenFilterStateChangedThenUpdateSelections(interactor: FacetListInteractor, + filterState: FilterState, + via groupID: FilterGroup.ID) { + + func extractString(from filter: Filter.Facet) -> String? { + if case .string(let stringValue) = filter.value { + return stringValue + } else { + return nil + } + } + + filterState.onChange.subscribePast(with: interactor) { interactor, filterState in + let filters: [Filter.Facet] + switch groupID { + case .and(name: let groupName): + filters = filterState[and: groupName].filters() + case .or(name: let groupName, _): + filters = filterState[or: groupName].filters() + case .hierarchical: + return + } + interactor.selections = Set(filters.compactMap(extractString)) + } + } + + } + +} + +public extension FacetListInteractor { + + @discardableResult func connectFilterState(_ filterState: FilterState, + with attribute: Attribute, + operator: RefinementOperator, + groupName: String? = nil) -> FacetList.FilterStateConnection { + let connection = FacetList.FilterStateConnection(interactor: self, filterState: filterState, attribute: attribute, operator: `operator`, groupName: groupName) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/FacetList/FacetListInteractor+SingleIndexSearcher.swift b/Sources/InstantSearchCore/FacetList/FacetListInteractor+SingleIndexSearcher.swift new file mode 100644 index 00000000..f4354c9d --- /dev/null +++ b/Sources/InstantSearchCore/FacetList/FacetListInteractor+SingleIndexSearcher.swift @@ -0,0 +1,47 @@ +// +// FacetListInteractor+Searcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +public extension FacetListInteractor { + + struct SingleIndexSearcherConnection: Connection { + + public let facetListInteractor: FacetListInteractor + public let searcher: SingleIndexSearcher + public let attribute: Attribute + + public func connect() { + + // When new search results then update items + + searcher.onResults.subscribePast(with: facetListInteractor) { [attribute] interactor, searchResults in + interactor.items = searchResults.disjunctiveFacets?[attribute] ?? searchResults.facets?[attribute] ?? [] + } + + searcher.indexQueryState.query.updateQueryFacets(with: attribute) + + } + + public func disconnect() { + searcher.onResults.cancelSubscription(for: facetListInteractor) + } + + } + +} + +public extension FacetListInteractor { + + @discardableResult func connectSearcher(_ searcher: SingleIndexSearcher, with attribute: Attribute) -> SingleIndexSearcherConnection { + let connection = SingleIndexSearcherConnection(facetListInteractor: self, searcher: searcher, attribute: attribute) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/FacetList/FacetListInteractor.swift b/Sources/InstantSearchCore/FacetList/FacetListInteractor.swift new file mode 100644 index 00000000..10bf7598 --- /dev/null +++ b/Sources/InstantSearchCore/FacetList/FacetListInteractor.swift @@ -0,0 +1,65 @@ +// +// RefinementFacetInteractor.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 19/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient + +public class FacetListInteractor: SelectableListInteractor { + + public let onResultsUpdated: Observer + private let mutationQueue: OperationQueue + + public init(facets: [Facet] = [], selectionMode: SelectionMode = .multiple) { + self.onResultsUpdated = .init() + self.mutationQueue = .init() + super.init(items: facets, selectionMode: selectionMode) + self.mutationQueue.maxConcurrentOperationCount = 1 + self.mutationQueue.qualityOfService = .userInitiated + } + +} + +extension FacetListInteractor: ResultUpdatable { + + @discardableResult public func update(_ facetResults: FacetSearchResponse) -> Operation { + + let updateOperation = BlockOperation { [weak self] in + self?.items = facetResults.facetHits + self?.onResultsUpdated.fire(facetResults) + } + + mutationQueue.addOperation(updateOperation) + + return updateOperation + + } + +} + +public enum FacetSortCriterion { + + case count(order: Order) + case alphabetical(order: Order) + case isRefined + + public enum Order { + case ascending + case descending + } +} + +public enum RefinementOperator { + // when operator is 'and' + one single value can be selected, + // we want to keep the other values visible, so we have to do a disjunctive facet + // In the case of multi value that can be selected in conjunctive case, + // then we avoid doing a disjunctive facet and just do normal conjusctive facet + // and only the remaining possible facets will appear. + case and + case or + +} diff --git a/Sources/InstantSearchCore/FacetList/FacetListPresenter.swift b/Sources/InstantSearchCore/FacetList/FacetListPresenter.swift new file mode 100644 index 00000000..5e564669 --- /dev/null +++ b/Sources/InstantSearchCore/FacetList/FacetListPresenter.swift @@ -0,0 +1,87 @@ +// +// FacetListPresenter.swift +// InstantSearchCore +// +// Created by Guy Daher on 18/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias SelectableItem = (item: T, isSelected: Bool) + +public protocol SelectableListPresentable { + + func transform(refinementFacets: [SelectableItem]) -> [SelectableItem] +} + +/// Takes care of building the content of a refinement list given the following: +/// - The list of Facets + Associated Count +/// - The list of Facets that have been refined +/// - Layout settings such as sortBy +public class FacetListPresenter: SelectableListPresentable { + + let sortBy: [FacetSortCriterion] + let limit: Int + let showEmptyFacets: Bool + + public init(sortBy: [FacetSortCriterion] = [.count(order: .descending)], + limit: Int = .max, + showEmptyFacets: Bool = true) { + self.sortBy = sortBy + self.limit = limit + self.showEmptyFacets = showEmptyFacets + } + + /// Builds the final list to be displayed in the refinement list + public func transform(refinementFacets: [SelectableItem]) -> [SelectableItem] { + let filteredOutput = refinementFacets.filter { showEmptyFacets ? true : !$0.item.isEmpty } + let sortedOutput = filteredOutput.sorted(by: sorter(using: sortBy)) + let upperBoundIndex = min(limit, sortedOutput.count) + let boundedOutput = sortedOutput[.. (SelectableItem, SelectableItem) -> (Bool) { + return { (lhs, rhs) in + + let lhsChecked: Bool = lhs.isSelected + let rhsChecked: Bool = rhs.isSelected + + let leftCount = lhs.item.count + let rightCount = rhs.item.count + let leftValueLowercased = lhs.item.value.lowercased() + let rightValueLowercased = rhs.item.value.lowercased() + + // tiebreaking algorithm to do determine the sorting. + for sortCriterion in sortCriterions { + + switch sortCriterion { + case .isRefined where lhsChecked != rhsChecked: + return lhsChecked + + case .count(order: .descending) where leftCount != rightCount: + return leftCount > rightCount + + case .count(order: .ascending) where leftCount != rightCount: + return leftCount < rightCount + + case .alphabetical(order: .descending) where leftValueLowercased != rightValueLowercased: + return leftValueLowercased > rightValueLowercased + + // Sort by Name ascending. Else, Biggest Count wins by default + case .alphabetical(order: .ascending) where leftValueLowercased != rightValueLowercased: + return leftValueLowercased < rightValueLowercased + + default: + break + } + + } + + return true + + } + } + +} diff --git a/Sources/InstantSearchCore/FilterClear/FilterClearConnector.swift b/Sources/InstantSearchCore/FilterClear/FilterClearConnector.swift new file mode 100644 index 00000000..d71c1730 --- /dev/null +++ b/Sources/InstantSearchCore/FilterClear/FilterClearConnector.swift @@ -0,0 +1,36 @@ +// +// FilterClearConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 26/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class FilterClearConnector: Connection { + + public let filterState: FilterState + public let interactor: FilterClearInteractor + public let filterStateConnection: Connection + + init(filterState: FilterState, + interactor: FilterClearInteractor = .init(), + clearMode: ClearMode = .specified, + filterGroupIDs: [FilterGroup.ID]? = nil) { + self.filterState = filterState + self.interactor = interactor + self.filterStateConnection = interactor.connectFilterState(filterState, + filterGroupIDs: filterGroupIDs, + clearMode: clearMode) + } + + public func connect() { + filterStateConnection.connect() + } + + public func disconnect() { + filterStateConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/FilterClear/FilterClearController.swift b/Sources/InstantSearchCore/FilterClear/FilterClearController.swift new file mode 100644 index 00000000..78de6612 --- /dev/null +++ b/Sources/InstantSearchCore/FilterClear/FilterClearController.swift @@ -0,0 +1,15 @@ +// +// ClearRefinementsController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 24/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol FilterClearController: class { + + var onClick: (() -> Void)? { get set } + +} diff --git a/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterClearController.swift b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterClearController.swift new file mode 100644 index 00000000..5a6f0a57 --- /dev/null +++ b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterClearController.swift @@ -0,0 +1,39 @@ +// +// FilterClearInteractor+FilterClearController.swift +// InstantSearchCore +// +// Created by Guy Daher on 13/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension FilterClearInteractor { + + struct ControllerConnection: Connection { + + public let interactor: FilterClearInteractor + public let controller: FilterClearController + + public func connect() { + controller.onClick = { [weak interactor] in + interactor?.onTriggered.fire(()) + } + } + + public func disconnect() { + controller.onClick = .none + } + } + +} + +public extension FilterClearInteractor { + + @discardableResult func connectController(_ controller: FilterClearController) -> ControllerConnection { + let connection = ControllerConnection(interactor: self, controller: controller) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterState.swift b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterState.swift new file mode 100644 index 00000000..ce713a64 --- /dev/null +++ b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor+FilterState.swift @@ -0,0 +1,78 @@ +// +// FilterClearInteractor+FilterState.swift +// InstantSearchCore +// +// Created by Guy Daher on 24/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension FilterClearInteractor { + + struct FilterStateConnection: Connection { + + public let filterClearInteractor: FilterClearInteractor + public let filterState: FilterState + public let filterGroupIDs: [FilterGroup.ID]? + public let clearMode: ClearMode + + public init(filterClearInteractor: FilterClearInteractor, + filterState: FilterState, + filterGroupIDs: [FilterGroup.ID]? = nil, + clearMode: ClearMode = .specified) { + self.filterClearInteractor = filterClearInteractor + self.filterState = filterState + self.filterGroupIDs = filterGroupIDs + self.clearMode = clearMode + } + + public func connect() { + let filterGroupIDs = self.filterGroupIDs + let clearMode = self.clearMode + + filterClearInteractor.onTriggered.subscribe(with: filterState) { filterState, _ in + defer { + filterState.notifyChange() + } + + guard let filterGroupIDs = filterGroupIDs else { + filterState.filters.removeAll() + return + } + + switch clearMode { + case .specified: + filterState.filters.removeAll(fromGroupWithIDs: filterGroupIDs) + case .except: + filterState.filters.removeAllExcept(filterGroupIDs) + } + + } + + } + + public func disconnect() { + filterClearInteractor.onTriggered.cancelSubscription(for: filterState) + } + + } + +} + +public extension FilterClearInteractor { + + @discardableResult func connectFilterState(_ filterState: FilterState, + filterGroupIDs: [FilterGroup.ID]? = nil, + clearMode: ClearMode = .specified) -> FilterStateConnection { + let connection = FilterStateConnection(filterClearInteractor: self, filterState: filterState, filterGroupIDs: filterGroupIDs, clearMode: clearMode) + connection.connect() + return connection + } + +} + +public enum ClearMode { + case specified + case except +} diff --git a/Sources/InstantSearchCore/FilterClear/FilterClearInteractor.swift b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor.swift new file mode 100644 index 00000000..311013ae --- /dev/null +++ b/Sources/InstantSearchCore/FilterClear/FilterClearInteractor.swift @@ -0,0 +1,17 @@ +// +// FilterClearInteractor.swift +// InstantSearchCore +// +// Created by Guy Daher on 13/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class FilterClearInteractor: EventInteractor { + public let onTriggered: Observer + + public init() { + onTriggered = .init() + } +} diff --git a/Sources/InstantSearchCore/FilterList/FacetFilterListConnector.swift b/Sources/InstantSearchCore/FilterList/FacetFilterListConnector.swift new file mode 100644 index 00000000..f9064162 --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/FacetFilterListConnector.swift @@ -0,0 +1,32 @@ +// +// FacetFilterListConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 26/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias FacetFilter = Filter.Facet +public typealias NumericFilter = Filter.Numeric +public typealias TagFilter = Filter.Tag + +public typealias FacetFilterListConnector = FilterListConnector + +public extension FacetFilterListConnector where Filter == FacetFilter { + + convenience init(filters: [Filter] = [], + selectionMode: SelectionMode = .multiple, + filterState: FilterState, + `operator`: RefinementOperator, + groupName: String) { + let interactor = FacetFilterListInteractor(items: filters, + selectionMode: selectionMode) + self.init(filterState: filterState, + interactor: interactor, + operator: `operator`, + groupName: groupName) + } + +} diff --git a/Sources/InstantSearchCore/FilterList/FilterListConnector.swift b/Sources/InstantSearchCore/FilterList/FilterListConnector.swift new file mode 100644 index 00000000..8cffaadb --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/FilterListConnector.swift @@ -0,0 +1,36 @@ +// +// FilterListConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 26/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class FilterListConnector: Connection { + + public let filterState: FilterState + public let interactor: FilterListInteractor + public let connectionFilterState: Connection + + public init(filterState: FilterState, + interactor: FilterListInteractor, + `operator`: RefinementOperator, + groupName: String) { + self.filterState = filterState + self.interactor = interactor + self.connectionFilterState = interactor.connectFilterState(filterState, + operator: `operator`, + groupName: groupName) + } + + public func connect() { + connectionFilterState.connect() + } + + public func disconnect() { + connectionFilterState.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/FilterList/FilterListController.swift b/Sources/InstantSearchCore/FilterList/FilterListController.swift new file mode 100644 index 00000000..73d03fc4 --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/FilterListController.swift @@ -0,0 +1,15 @@ +// +// FilterListController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 17/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol FilterListController: SelectableListController where Item: FilterType {} + +public protocol FacetFilterListController: FilterListController where Item == Filter.Facet {} +public protocol NumericFilterListController: FilterListController where Item == Filter.Numeric {} +public protocol TagFilterListController: FilterListController where Item == Filter.Tag {} diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor+Controller.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Controller.swift new file mode 100644 index 00000000..c8af2528 --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Controller.swift @@ -0,0 +1,57 @@ +// +// FilterListInteractor+Controller.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension FilterList { + + public struct ControllerConnection: Connection where Controller.Item == Filter { + + public let interactor: FilterListInteractor + public let controller: Controller + + public func connect() { + + func setControllerItemsWith(items: [Filter], selections: Set) { + let selectableItems = items.map { ($0, selections.contains($0)) } + controller.setSelectableItems(selectableItems: selectableItems) + controller.reload() + } + + setControllerItemsWith(items: interactor.items, selections: interactor.selections) + + controller.onClick = interactor.computeSelections(selectingItemForKey:) + + interactor.onItemsChanged.subscribePast(with: controller) { [weak interactor] _, items in + setControllerItemsWith(items: items, selections: interactor!.selections) + }.onQueue(.main) + + interactor.onSelectionsChanged.subscribePast(with: controller) { [weak interactor] _, selections in + setControllerItemsWith(items: interactor!.items, selections: selections) + }.onQueue(.main) + + } + + public func disconnect() { + interactor.onItemsChanged.cancelSubscription(for: controller) + interactor.onSelectionsChanged.cancelSubscription(for: controller) + } + + } + +} + +public extension SelectableListInteractor where Key == Item, Item: FilterType { + + @discardableResult func connectController(_ controller: Controller) -> FilterList.ControllerConnection { + let connection = FilterList.ControllerConnection(interactor: self, controller: controller) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor+Facet.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Facet.swift new file mode 100644 index 00000000..728709a7 --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Facet.swift @@ -0,0 +1,19 @@ +// +// FilterListInteractor+Facet.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 17/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias FacetFilterListInteractor = FilterListInteractor + +public extension FacetFilterListInteractor { + + convenience init(facetFilters: [Filter.Facet] = []) { + self.init(items: facetFilters, selectionMode: .multiple) + } + +} diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor+FilterState.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor+FilterState.swift new file mode 100644 index 00000000..682f8392 --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor+FilterState.swift @@ -0,0 +1,94 @@ +// +// FilterListInteractor+FilterState.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public enum FilterList { + + public struct FilterStateConnection: Connection { + + public let interactor: SelectableListInteractor + public let filterState: FilterState + public let `operator`: RefinementOperator + public let groupName: String + + public init(interactor: SelectableListInteractor, + filterState: FilterState, + `operator`: RefinementOperator, + groupName: String = "") { + self.interactor = interactor + self.filterState = filterState + self.operator = `operator` + self.groupName = groupName + } + + public func connect() { + switch `operator` { + case .or: + connectFilterState(filterState, to: interactor, via: filterState[or: groupName]) + case .and: + connectFilterState(filterState, to: interactor, via: SpecializedAndGroupAccessor(filterState[and: groupName])) + } + } + + public func disconnect() { + interactor.onSelectionsComputed.cancelSubscription(for: filterState) + filterState.onChange.cancelSubscription(for: interactor) + } + + private func connectFilterState, Accessor: SpecializedGroupAccessor>(_ filterState: FilterState, + to interactor: Interactor, via accessor: Accessor) where Accessor.Filter == Filter { + whenSelectionsComputedThenUpdateFilterState(interactor: interactor, filterState: filterState, via: accessor) + whenFilterStateChangedThenUpdateSelections(interactor: interactor, filterState: filterState, via: accessor) + } + + private func whenSelectionsComputedThenUpdateFilterState, Accessor: SpecializedGroupAccessor>(interactor: Interactor, + filterState: FilterState, + + via accessor: Accessor) where Accessor.Filter == Filter { + + interactor.onSelectionsComputed.subscribePast(with: filterState) { [weak interactor] filterState, filters in + + guard let strongInteractor = interactor else { return } + switch strongInteractor.selectionMode { + case .multiple: + accessor.removeAll() + + case .single: + accessor.removeAll(strongInteractor.items) + } + + accessor.addAll(filters) + + filterState.notifyChange() + } + + } + + private func whenFilterStateChangedThenUpdateSelections, Accessor: SpecializedGroupAccessor>(interactor: Interactor, filterState: FilterState, + via accessor: Accessor) where Accessor.Filter == Filter { + filterState.onChange.subscribePast(with: interactor) { interactor, _ in + interactor.selections = Set(accessor.filters()) + } + } + + } + +} + +public extension SelectableListInteractor where Key == Item, Item: FilterType { + + @discardableResult func connectFilterState(_ filterState: FilterState, + operator: RefinementOperator, + groupName: String = "") -> FilterList.FilterStateConnection { + let connection = FilterList.FilterStateConnection(interactor: self, filterState: filterState, operator: `operator`, groupName: groupName) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor+Numeric.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Numeric.swift new file mode 100644 index 00000000..45b2371c --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Numeric.swift @@ -0,0 +1,19 @@ +// +// FilterListInteractor+Numeric.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 17/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias NumericFilterListInteractor = FilterListInteractor + +public extension NumericFilterListInteractor { + + convenience init(numericFilters: [Filter.Numeric] = []) { + self.init(items: numericFilters, selectionMode: .single) + } + +} diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor+Tag.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Tag.swift new file mode 100644 index 00000000..99c6bb77 --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor+Tag.swift @@ -0,0 +1,19 @@ +// +// FilterListInteractor+Tag.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 17/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias TagFilterListInteractor = FilterListInteractor + +public extension TagFilterListInteractor { + + convenience init(tagFilters: [Filter.Tag] = []) { + self.init(items: tagFilters, selectionMode: .multiple) + } + +} diff --git a/Sources/InstantSearchCore/FilterList/FilterListInteractor.swift b/Sources/InstantSearchCore/FilterList/FilterListInteractor.swift new file mode 100644 index 00000000..284329c4 --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/FilterListInteractor.swift @@ -0,0 +1,11 @@ +// +// SelectableListInteractor+Filter.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 17/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias FilterListInteractor = SelectableListInteractor diff --git a/Sources/InstantSearchCore/FilterList/NumericFilterListConnector.swift b/Sources/InstantSearchCore/FilterList/NumericFilterListConnector.swift new file mode 100644 index 00000000..34bce9a4 --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/NumericFilterListConnector.swift @@ -0,0 +1,28 @@ +// +// NumericFilterListConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 26/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias NumericFilterListConnector = FilterListConnector + +public extension NumericFilterListConnector { + + convenience init(numericFilters: [NumericFilter] = [], + selectionMode: SelectionMode = .single, + filterState: FilterState, + `operator`: RefinementOperator, + groupName: String) { + let interactor = NumericFilterListInteractor(items: numericFilters, + selectionMode: selectionMode) + self.init(filterState: filterState, + interactor: interactor, + operator: `operator`, + groupName: groupName) + } + +} diff --git a/Sources/InstantSearchCore/FilterList/TagFilterListConnector.swift b/Sources/InstantSearchCore/FilterList/TagFilterListConnector.swift new file mode 100644 index 00000000..26755da2 --- /dev/null +++ b/Sources/InstantSearchCore/FilterList/TagFilterListConnector.swift @@ -0,0 +1,28 @@ +// +// TagFilterListConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 26/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias TagFilterListConnector = FilterListConnector + +public extension TagFilterListConnector { + + convenience init(tagFilters: [TagFilter] = [], + selectionMode: SelectionMode = .multiple, + filterState: FilterState, + `operator`: RefinementOperator, + groupName: String) { + let interactor = TagFilterListInteractor(items: tagFilters, + selectionMode: selectionMode) + self.init(filterState: filterState, + interactor: interactor, + operator: `operator`, + groupName: groupName) + } + +} diff --git a/Sources/InstantSearchCore/FilterState/Accessors/AndGroupAccessor.swift b/Sources/InstantSearchCore/FilterState/Accessors/AndGroupAccessor.swift new file mode 100644 index 00000000..882f2d99 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/Accessors/AndGroupAccessor.swift @@ -0,0 +1,79 @@ +// +// AndGroupAccessor.swift +// AlgoliaSearch OSX +// +// Created by Vladislav Fitc on 24/12/2018. +// Copyright © 2018 Algolia. All rights reserved. +// + +import Foundation + +/// Provides a convenient interface to a conjunctive group contained by FilterState +public struct AndGroupAccessor: GroupAccessor { + + let filtersContainer: FiltersContainer + let groupID: FilterGroup.ID + + /// A Boolean value indicating whether group contains at least on filter + public var isEmpty: Bool { + return filtersContainer.filters.getFilters(forGroupWithID: groupID).isEmpty + } + + init(filtersContainer: FiltersContainer, groupName: String) { + self.filtersContainer = filtersContainer + self.groupID = .and(name: groupName) + } + + /// Adds filter to group + /// - parameter filter: filter to add + public func add(_ filters: FilterType...) { + filtersContainer.filters.addAll(filters: filters, toGroupWithID: groupID) + } + + /// Adds the filters of a sequence to group + /// - parameter filters: sequence of filters to add + public func addAll(_ filters: [FilterType]) { + filtersContainer.filters.addAll(filters: filters, toGroupWithID: groupID) + } + + /// Tests whether group contains a filter + /// - parameter filter: sought filter + public func contains(_ filter: T) -> Bool { + return filtersContainer.filters.contains(filter, inGroupWithID: groupID) + } + + /// Removes all filters with specified attribute from group + /// - parameter attribute: specified attribute + public func removeAll(for attribute: Attribute) { + return filtersContainer.filters.removeAll(for: attribute, fromGroupWithID: groupID) + } + + /// Removes filter from group + /// - parameter filter: filter to remove + @discardableResult public func remove(_ filter: T) -> Bool { + return filtersContainer.filters.remove(filter, fromGroupWithID: groupID) + } + + /// Removes a sequence of filters from group + /// - parameter filters: sequence of filters to remove + @discardableResult public func removeAll(_ filters: S) -> Bool where S.Element == FilterType { + return filtersContainer.filters.removeAll(filters, fromGroupWithID: groupID) + } + + /// Removes all filters in group + public func removeAll() { + filtersContainer.filters.removeAll(fromGroupWithID: groupID) + } + + /// Removes all filters in other all groups + public func removeAllOthers() { + filtersContainer.filters.removeAllExcept([groupID]) + } + + /// Removes filter from group if contained by it, otherwise adds filter to group + /// - parameter filter: filter to toggle + public func toggle(_ filter: T) { + filtersContainer.filters.toggle(filter, inGroupWithID: groupID) + } + +} diff --git a/Sources/InstantSearchCore/FilterState/Accessors/GroupAccessor.swift b/Sources/InstantSearchCore/FilterState/Accessors/GroupAccessor.swift new file mode 100644 index 00000000..c4ad708f --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/Accessors/GroupAccessor.swift @@ -0,0 +1,128 @@ +// +// GroupAccessor.swift +// AlgoliaSearch OSX +// +// Created by Vladislav Fitc on 24/12/2018. +// Copyright © 2018 Algolia. All rights reserved. +// + +import Foundation + +protocol FiltersContainer: class { + var filters: FilterState.Storage { get set } +} + +extension FiltersContainer { + + public subscript(and groupName: String) -> AndGroupAccessor { + return .init(filtersContainer: self, groupName: groupName) + } + + public subscript(or groupName: String, type: F.Type) -> OrGroupAccessor { + return .init(filtersContainer: self, groupName: groupName) + } + + public subscript(or groupName: String) -> OrGroupAccessor { + return .init(filtersContainer: self, groupName: groupName) + } + + public subscript(hierarchical groupName: String) -> HierarchicalGroupAccessor { + return .init(filtersContainer: self, groupName: groupName) + } + +} + +public class ReadOnlyFiltersContainer { + + class StorageContainer: FiltersContainer { + var filters: FilterState.Storage + init(filterState: FilterState) { + self.filters = filterState.filters + } + } + + let filtersContainer: FiltersContainer + + init(filterState: FilterState) { + self.filtersContainer = StorageContainer(filterState: filterState) + } + + public subscript(and groupName: String) -> ReadOnlyGroupAccessor { + return ReadOnlyGroupAccessor(SpecializedAndGroupAccessor(filtersContainer[and: groupName])) + } + + public subscript(or groupName: String) -> ReadOnlyGroupAccessor { + return ReadOnlyGroupAccessor(filtersContainer[or: groupName]) + } + + public subscript(hierarchical groupName: String) -> ReadOnlyGroupAccessor { + return ReadOnlyGroupAccessor(filtersContainer[hierarchical: groupName]) + } + +} + +extension ReadOnlyFiltersContainer: FilterGroupsConvertible { + public func toFilterGroups() -> [FilterGroupType] { + return filtersContainer.filters.toFilterGroups() + } + +} + +/// Provides a convenient interface to a concrete group contained by FilterState +public protocol GroupAccessor { + + var isEmpty: Bool { get } + + func removeAll(for attribute: Attribute) + func removeAll() + func removeAllOthers() + +} + +public class ReadOnlyGroupAccessor { + + var storedIsEmpty: () -> Bool + var storedGetFilters: () -> [Filter] + var storedGetFiltersForAttribute: (Attribute) -> [Filter] + var storedContains: (Filter) -> Bool + + init(_ accessor: A) where A.Filter == Filter { + storedIsEmpty = { accessor.isEmpty } + storedGetFilters = { return accessor.filters() } + storedGetFiltersForAttribute = { return accessor.filters(for: $0) } + storedContains = { return accessor.contains($0) } + } + + var isEmpty: Bool { + return storedIsEmpty() + } + + func filters() -> [Filter] { + return storedGetFilters() + } + + func filters(for attribute: Attribute) -> [Filter] { + return storedGetFiltersForAttribute(attribute) + } + + func contains(_ filter: Filter) -> Bool { + return storedContains(filter) + } + +} + +public protocol SpecializedGroupAccessor: GroupAccessor { + + associatedtype Filter: FilterType + + func filters() -> [Filter] + func filters(for attribute: Attribute) -> [Filter] + + func add(_ filters: Filter...) + func addAll(_ filters: S) where S.Element == Filter + func contains(_ filter: Filter) -> Bool + func remove(_ filter: Filter) + @discardableResult func removeAll(_ filters: S) -> Bool where S.Element == Filter + func toggle(_ filter: Filter) + +} diff --git a/Sources/InstantSearchCore/FilterState/Accessors/HierarchicalGroupAccessor.swift b/Sources/InstantSearchCore/FilterState/Accessors/HierarchicalGroupAccessor.swift new file mode 100644 index 00000000..e5fe05c2 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/Accessors/HierarchicalGroupAccessor.swift @@ -0,0 +1,105 @@ +// +// HierarchicalGroupAccessor.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 12/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +/// Provides a convenient interface to a hierarchical group contained by FilterState +public struct HierarchicalGroupAccessor: SpecializedGroupAccessor { + + public typealias Filter = FacetFilter + + let groupID: FilterGroup.ID + var filtersContainer: FiltersContainer + + public var isEmpty: Bool { + return filtersContainer.filters.isEmpty + } + + var hierarchicalAttributes: [Attribute] { + return filtersContainer.filters.hierarchicalAttributes(forGroupWithName: groupID.name) + } + + var hierarchicalFilters: [Filter] { + return filtersContainer.filters.hierarchicalFilters(forGroupWithName: groupID.name) + } + + func set(_ hierarchicalAttributes: [Attribute]) { + filtersContainer.filters.set(hierarchicalAttributes, forGroupWithName: groupID.name) + } + + func set(_ hierarchicalFilters: [Filter]) { + filtersContainer.filters.set(hierarchicalFilters, forGroupWithName: groupID.name) + } + + init(filtersContainer: FiltersContainer, groupName: String) { + self.filtersContainer = filtersContainer + self.groupID = .hierarchical(name: groupName) + } + + /// Adds filter to group + /// - parameter filter: filter to add + public func add(_ filters: Filter...) { + filtersContainer.filters.addAll(filters: filters, toGroupWithID: groupID) + } + + /// Adds the filters of a sequence to group + /// - parameter filters: sequence of filters to add + public func addAll(_ filters: S) where S.Element == Filter { + filtersContainer.filters.addAll(filters: filters.map { $0 as FilterType }, toGroupWithID: groupID) + } + + /// Tests whether group contains a filter + /// - parameter filter: sought filter + public func contains(_ filter: Filter) -> Bool { + return filtersContainer.filters.contains(filter, inGroupWithID: groupID) + } + + /// Removes all filters with specified attribute from group + /// - parameter attribute: specified attribute + public func removeAll(for attribute: Attribute) { + return filtersContainer.filters.removeAll(for: attribute, fromGroupWithID: groupID) + } + + /// Removes filter from group + /// - parameter filter: filter to remove + public func remove(_ filter: Filter) { + filtersContainer.filters.remove(filter, fromGroupWithID: groupID) + } + + /// Removes a sequence of filters from group + /// - parameter filters: sequence of filters to remove + @discardableResult public func removeAll(_ filters: S) -> Bool where S.Element == Filter { + filtersContainer.filters.removeAll(fromGroupWithID: groupID) + return false + } + + /// Removes all filters in group + public func removeAll() { + filtersContainer.filters.removeAll(fromGroupWithID: groupID) + } + + /// Removes all filters in other all groups + public func removeAllOthers() { + filtersContainer.filters.removeAllExcept([groupID]) + } + + /// Removes filter from group if contained by it, otherwise adds filter to group + /// - parameter filter: filter to toggle + public func toggle(_ filter: Filter) { + filtersContainer.filters.toggle(filter, inGroupWithID: groupID) + } + + public func filters(for attribute: Attribute) -> [Filter] { + return filtersContainer.filters.getFilters(for: attribute).compactMap { $0.filter as? Filter } + } + + public func filters() -> [Filter] { + return filtersContainer.filters.getFilters().compactMap { $0.filter as? Filter } + } + +} diff --git a/Sources/InstantSearchCore/FilterState/Accessors/OrGroupAccessor.swift b/Sources/InstantSearchCore/FilterState/Accessors/OrGroupAccessor.swift new file mode 100644 index 00000000..366ed3d5 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/Accessors/OrGroupAccessor.swift @@ -0,0 +1,86 @@ +// +// OrGroupAccessor.swift +// AlgoliaSearch OSX +// +// Created by Vladislav Fitc on 24/12/2018. +// Copyright © 2018 Algolia. All rights reserved. +// + +import Foundation + +/// Provides a convenient interface to a disjunctive group contained by FilterState +public struct OrGroupAccessor: SpecializedGroupAccessor { + + let filtersContainer: FiltersContainer + let groupID: FilterGroup.ID + + /// A Boolean value indicating whether group contains at least on filter + public var isEmpty: Bool { + return filtersContainer.filters.getFilters(forGroupWithID: groupID).isEmpty + } + + init(filtersContainer: FiltersContainer, groupName: String) { + self.filtersContainer = filtersContainer + let filterType = FilterGroup.ID.Filter(Filter.self)! + self.groupID = .or(name: groupName, filterType: filterType) + } + + /// Adds filter to group + /// - parameter filter: filter to add + public func add(_ filters: Filter...) { + filtersContainer.filters.addAll(filters: filters, toGroupWithID: groupID) + } + + /// Adds the filters of a sequence to group + /// - parameter filters: sequence of filters to add + public func addAll(_ filters: S) where S.Element == Filter { + filtersContainer.filters.addAll(filters: filters.map { $0 as FilterType }, toGroupWithID: groupID) + } + + /// Tests whether group contains a filter + /// - parameter filter: sought filter + public func contains(_ filter: Filter) -> Bool { + return filtersContainer.filters.contains(filter, inGroupWithID: groupID) + } + + /// Removes all filters with specified attribute from group + /// - parameter attribute: specified attribute + public func removeAll(for attribute: Attribute) { + return filtersContainer.filters.removeAll(for: attribute, fromGroupWithID: groupID) + } + + public func remove(_ filter: Filter) { + _ = filtersContainer.filters.remove(filter, fromGroupWithID: groupID) + } + + /// Removes a sequence of filters from group + /// - parameter filters: sequence of filters to remove + @discardableResult public func removeAll(_ filters: S) -> Bool where S.Element == Filter { + return filtersContainer.filters.removeAll(filters.map { $0 as FilterType }, fromGroupWithID: groupID) + } + + /// Removes all filters in group + public func removeAll() { + filtersContainer.filters.removeAll(fromGroupWithID: groupID) + } + + /// Removes all filters in other all groups + public func removeAllOthers() { + filtersContainer.filters.removeAllExcept([groupID]) + } + + /// Removes filter from group if contained by it, otherwise adds filter to group + /// - parameter filter: filter to toggleE + public func toggle(_ filter: Filter) { + filtersContainer.filters.toggle(filter, inGroupWithID: groupID) + } + + public func filters(for attribute: Attribute) -> [Filter] { + return filtersContainer.filters.getFilters(forGroupWithID: groupID).filter { $0.attribute == attribute }.compactMap { $0.filter as? Filter } + } + + public func filters() -> [Filter] { + return filtersContainer.filters.getFilters(forGroupWithID: groupID).compactMap { $0.filter as? Filter } + } + +} diff --git a/Sources/InstantSearchCore/FilterState/Accessors/SpecializedAndGroupAccessor.swift b/Sources/InstantSearchCore/FilterState/Accessors/SpecializedAndGroupAccessor.swift new file mode 100644 index 00000000..27c2138d --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/Accessors/SpecializedAndGroupAccessor.swift @@ -0,0 +1,93 @@ +// +// SpecializedAndGroupAccessor.swift +// AlgoliaSearch OSX +// +// Created by Vladislav Fitc on 26/12/2018. +// Copyright © 2018 Algolia. All rights reserved. +// + +import Foundation + +/// Provides a convenient interface to a conjunctive group contained by FilterState specialized for filters of concrete type +public struct SpecializedAndGroupAccessor: SpecializedGroupAccessor { + + private var genericAccessor: AndGroupAccessor + + var filtersContainer: FiltersContainer { + return genericAccessor.filtersContainer + } + + public var groupID: FilterGroup.ID { + return genericAccessor.groupID + } + + /// A Boolean value indicating whether group contains at least on filter + public var isEmpty: Bool { + return genericAccessor.isEmpty + } + + init(_ genericAccessor: AndGroupAccessor) { + self.genericAccessor = genericAccessor + } + + /// Adds filter to group + /// - parameter filter: filter to add + public func add(_ filters: Filter...) { + genericAccessor.addAll(filters) + } + + /// Adds the filters of a sequence to group + /// - parameter filters: sequence of filters to add + public func addAll(_ filters: S) where S.Element == Filter { + genericAccessor.addAll(filters.map { $0 as FilterType }) + } + + /// Tests whether group contains a filter + /// - parameter filter: sought filter + public func contains(_ filter: Filter) -> Bool { + return genericAccessor.contains(filter) + } + + /// Removes all filters with specified attribute from group + /// - parameter attribute: specified attribute + public func removeAll(for attribute: Attribute) { + return genericAccessor.removeAll(for: attribute) + } + + /// Removes filter from group + /// - parameter filter: filter to remove + public func remove(_ filter: Filter) { + genericAccessor.remove(filter) + } + + /// Removes a sequence of filters from group + /// - parameter filters: sequence of filters to remove + @discardableResult public func removeAll(_ filters: S) -> Bool where S.Element == Filter { + return genericAccessor.removeAll(filters.map { $0 as FilterType }) + } + + /// Removes all filters in group + public func removeAll() { + genericAccessor.removeAll() + } + + /// Removes all filters in other all groups + public func removeAllOthers() { + genericAccessor.removeAllOthers() + } + + /// Removes filter from group if contained by it, otherwise adds filter to group + /// - parameter filter: filter to toggle + public func toggle(_ filter: Filter) { + genericAccessor.toggle(filter) + } + + public func filters(for attribute: Attribute) -> [Filter] { + return filtersContainer.filters.getFilters(forGroupWithID: groupID).filter { $0.attribute == attribute }.compactMap { $0.filter as? Filter } + } + + public func filters() -> [Filter] { + return filtersContainer.filters.getFilters(forGroupWithID: groupID).compactMap { $0.filter as? Filter } + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterConverter.swift b/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterConverter.swift new file mode 100644 index 00000000..bd279609 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterConverter.swift @@ -0,0 +1,13 @@ +// +// FilterConverter.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class FilterConverter { + public init() {} +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterGroupConverter.swift b/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterGroupConverter.swift new file mode 100644 index 00000000..7ca1c714 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/Converters/FilterGroupConverter.swift @@ -0,0 +1,13 @@ +// +// FilterGroupConverter.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class FilterGroupConverter { + public init() {} +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Converters/LegacySyntax.swift b/Sources/InstantSearchCore/FilterState/FIlter/Converters/LegacySyntax.swift new file mode 100644 index 00000000..da31c0ea --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/Converters/LegacySyntax.swift @@ -0,0 +1,127 @@ +// +// LegacySyntax.swift +// AlgoliaSearch +// +// Created by Vladislav Fitc on 05/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +protocol LegacySyntaxConvertible { + var legacyForm: FiltersStorage { get } +} + +extension FilterConverter { + + public func legacy(_ filter: FilterType) -> FiltersStorage? { + return (filter as? LegacySyntaxConvertible)?.legacyForm + } + +} + +extension FilterGroupConverter { + + public func legacy(_ group: FilterGroupType) -> FiltersStorage? { + return (group as? LegacySyntaxConvertible)?.legacyForm + } + + public func legacy(_ groups: C) -> FiltersStorage? where C.Element == FilterGroupType { + let units = groups + .filter { !$0.filters.isEmpty } + .compactMap { $0 as? LegacySyntaxConvertible } + .map(\.legacyForm) + .flatMap(\.units) + return FiltersStorage(units: units) + } + +} + +extension Filter.Numeric: LegacySyntaxConvertible { + + public var legacyForm: FiltersStorage { + + switch value { + case .comparison(let `operator`, let value): + let `operator` = isNegated ? `operator`.inversion : `operator` + let expression = """ + \(attribute) \(`operator`.rawValue) \(value) + """ + return .and(.and(expression)) + + case .range(let range): + let units = [ + Filter.Numeric(attribute: attribute, operator: isNegated ? .lessThan : .greaterThanOrEqual, value: range.lowerBound), + Filter.Numeric(attribute: attribute, operator: isNegated ? .greaterThan : .lessThanOrEqual, value: range.upperBound) + ] + .compactMap { $0.legacyForm } + .flatMap(\.units) + return FiltersStorage(units: units) + } + + } + +} + +extension Filter.Facet: LegacySyntaxConvertible { + + public var legacyForm: FiltersStorage { + let scoreExpression = score.flatMap { "" } ?? "" + let valuePrefix = isNegated ? "-" : "" + let expression = """ + \(attribute):\(valuePrefix)\(value)\(scoreExpression) + """ + return .and(.and(expression)) + } + +} + +extension Filter.Tag: LegacySyntaxConvertible { + + public var legacyForm: FiltersStorage { + let valuePrefix = isNegated ? "-" : "" + let expression = """ + \(attribute):\(valuePrefix)\(value) + """ + return .and(.and(expression)) + } + +} + +extension FilterGroup.And: LegacySyntaxConvertible { + + var legacyForm: FiltersStorage { + let rawFilters = filters + .compactMap { $0 as? LegacySyntaxConvertible } + .map(\.legacyForm) + .flatMap(\.units) + .flatMap(\.rawFilters) + return .and(.and(rawFilters)) + } + +} + +extension FilterGroup.Or: LegacySyntaxConvertible { + + var legacyForm: FiltersStorage { + let rawFilters = filters + .compactMap { $0 as? LegacySyntaxConvertible } + .map(\.legacyForm) + .flatMap(\.units) + .flatMap(\.rawFilters) + return .and(.or(rawFilters)) + } + +} + +internal extension FiltersStorage.Unit { + + var rawFilters: [String] { + switch self { + case .and(let values), + .or(let values): + return values + } + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Converters/SQLSyntax.swift b/Sources/InstantSearchCore/FilterState/FIlter/Converters/SQLSyntax.swift new file mode 100644 index 00000000..2c74121b --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/Converters/SQLSyntax.swift @@ -0,0 +1,115 @@ +// +// SQLSyntax.swift +// AlgoliaSearch +// +// Created by Vladislav Fitc on 05/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +protocol SQLSyntaxConvertible { + var sqlForm: String { get } +} + +extension FilterConverter { + + public func sql(_ filter: FilterType) -> String? { + return (filter as? SQLSyntaxConvertible)?.sqlForm + } + +} + +extension FilterGroupConverter { + + public func sql(_ group: FilterGroupType) -> String? { + return (group as? SQLSyntaxConvertible)?.sqlForm + } + + public func sql(_ groupList: C) -> String? where C.Element == FilterGroupType { + guard !groupList.isEmpty else { return nil } + return groupList + .filter { !$0.filters.isEmpty } + .compactMap(sql) + .joined(separator: " AND ") + } + +} + +extension Filter.Numeric: SQLSyntaxConvertible { + + public var sqlForm: String { + let expression: String + switch value { + case .comparison(let `operator`, let value): + expression = """ + "\(attribute)" \(`operator`.rawValue) \(value) + """ + + case .range(let range): + expression = """ + "\(attribute)":\(range.lowerBound) TO \(range.upperBound) + """ + } + let prefix = isNegated ? "NOT " : "" + return prefix + expression + } + +} + +extension Filter.Facet: SQLSyntaxConvertible { + + public var sqlForm: String { + let scoreExpression = score.flatMap { "" } ?? "" + let expression = """ + "\(attribute)":"\(value)\(scoreExpression)" + """ + let prefix = isNegated ? "NOT " : "" + return prefix + expression + } + +} + +extension Filter.Tag: SQLSyntaxConvertible { + + public var sqlForm: String { + let expression = """ + "\(attribute)":"\(value)" + """ + let prefix = isNegated ? "NOT " : "" + return prefix + expression + } + +} + +extension SQLSyntaxConvertible where Self: FilterGroupType { + + func groupSQLForm(for filters: [FilterType], withSeparator separator: String) -> String { + + let compatibleFilters = filters.compactMap { $0 as? SQLSyntaxConvertible } + + if compatibleFilters.isEmpty { + return "" + } else { + return "( \(compatibleFilters.map { $0.sqlForm }.joined(separator: separator)) )" + } + + } + +} + +extension FilterGroup.And: SQLSyntaxConvertible { + + public var sqlForm: String { + return groupSQLForm(for: filters, withSeparator: " AND ") + } + +} + +extension FilterGroup.Or: SQLSyntaxConvertible { + + public var sqlForm: String { + return groupSQLForm(for: filters, withSeparator: " OR ") + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/FacetFilter.swift b/Sources/InstantSearchCore/FilterState/FIlter/FacetFilter.swift new file mode 100644 index 00000000..41b8496a --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/FacetFilter.swift @@ -0,0 +1,134 @@ +// +// Facet.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +/** Defines facet filter + # See also: + [Filter by string](https:www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-string/) + [Filter by boolean](https:www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-boolean/) + */ + +public extension Filter { + + struct Facet: FilterType, Equatable { + + public let attribute: Attribute + public let value: ValueType + public var isNegated: Bool + public let score: Int? + + public init(attribute: Attribute, value: ValueType, isNegated: Bool = false, score: Int? = nil) { + self.attribute = attribute + self.isNegated = isNegated + self.value = value + self.score = score + } + + public init(attribute: Attribute, stringValue: String, isNegated: Bool = false) { + self.init(attribute: attribute, value: .string(stringValue), isNegated: isNegated) + } + + public init(attribute: Attribute, floatValue: Float, isNegated: Bool = false) { + self.init(attribute: attribute, value: .float(floatValue), isNegated: isNegated) + } + + public init(attribute: Attribute, boolValue: Bool, isNegated: Bool = false) { + self.init(attribute: attribute, value: .bool(boolValue), isNegated: isNegated) + } + + } + +} + +extension Filter.Facet: Hashable {} + +extension Filter.Facet: RawRepresentable { + + public typealias RawValue = (Attribute, ValueType) + + public init?(rawValue: (Attribute, Filter.Facet.ValueType)) { + self.init(attribute: rawValue.0, value: rawValue.1) + } + + public var rawValue: (Attribute, Filter.Facet.ValueType) { + return (attribute, value) + } + +} + +extension Filter.Facet: CustomStringConvertible { + + public var description: String { + return "\(attribute): \(value.description)" + } + +} + +extension Filter.Facet { + + public enum ValueType: CustomStringConvertible, Hashable { + + case string(String) + case float(Float) + case bool(Bool) + + public var description: String { + switch self { + case .string(let value): + return value + case .bool(let value): + return "\(value)" + case .float(let value): + return "\(value)" + } + } + + } + +} + +extension Filter.Facet.ValueType: ExpressibleByBooleanLiteral { + + public typealias BooleanLiteralType = Bool + + public init(booleanLiteral value: BooleanLiteralType) { + self = .bool(value) + } + +} + +extension Filter.Facet.ValueType: ExpressibleByFloatLiteral { + + public typealias FloatLiteralType = Float + + public init(floatLiteral value: FloatLiteralType) { + self = .float(value) + } + +} + +extension Filter.Facet.ValueType: ExpressibleByStringLiteral { + + public typealias StringLiterlalType = String + + public init(stringLiteral value: StringLiteralType) { + self = .string(value) + } + +} + +extension Filter.Facet.ValueType: ExpressibleByIntegerLiteral { + + public typealias IntegerLiteralType = Int + + public init(integerLiteral value: IntegerLiteralType) { + self = .float(Float(value)) + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Filter.swift b/Sources/InstantSearchCore/FilterState/FIlter/Filter.swift new file mode 100644 index 00000000..38875427 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/Filter.swift @@ -0,0 +1,111 @@ +// +// Filter.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public enum Filter: Hashable { + + case facet(Facet) + case numeric(Numeric) + case tag(Tag) + + public var attribute: Attribute { + switch self { + case .facet(let filter): + return filter.attribute + case .numeric(let filter): + return filter.attribute + case .tag(let filter): + return filter.attribute + } + } + + public init(_ filter: F) { + switch filter { + case let facetFilter as Filter.Facet: + self = .facet(facetFilter) + case let numericFilter as Filter.Numeric: + self = .numeric(numericFilter) + case let tagFilter as Filter.Tag: + self = .tag(tagFilter) + default: + fatalError("Filter of type \(F.self) is not supported") + } + } + + public init(_ filter: FilterType) { + switch filter { + case let facetFilter as Filter.Facet: + self = .facet(facetFilter) + case let numericFilter as Filter.Numeric: + self = .numeric(numericFilter) + case let tagFilter as Filter.Tag: + self = .tag(tagFilter) + default: + fatalError("Filter of type \(FilterType.self) is not supported") + } + } + + public var filter: FilterType { + switch self { + case .facet(let facetFilter): + return facetFilter + + case .numeric(let numericFilter): + return numericFilter + + case .tag(let tagFilter): + return tagFilter + } + } + +} + +extension Filter: CustomStringConvertible { + + public var description: String { + switch self { + case .facet(let facetFilter): + return facetFilter.description + case .numeric(let numericFilter): + return numericFilter.description + case .tag(let tagFilter): + return tagFilter.description + } + } + +} + +/// Abstract filter protocol +public protocol FilterType { + + /// Identifier of field affected by filter + var attribute: Attribute { get } + + /// A Boolean value indicating whether filter is inverted + var isNegated: Bool { get set } + + /// Replaces isNegated property by a new value + /// parameter value: new value of isNegated + mutating func not(value: Bool) + +} + +public extension FilterType { + + mutating func not(value: Bool = true) { + isNegated = value + } + +} + +@discardableResult public prefix func ! (filter: T) -> T { + var mutableFilterCopy = filter + mutableFilterCopy.not(value: !filter.isNegated) + return mutableFilterCopy +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Groups/AndFilterGroup.swift b/Sources/InstantSearchCore/FilterState/FIlter/Groups/AndFilterGroup.swift new file mode 100644 index 00000000..f994574a --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/Groups/AndFilterGroup.swift @@ -0,0 +1,47 @@ +// +// AndFilterGroup.swift +// AlgoliaSearch OSX +// +// Created by Vladislav Fitc on 14/01/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +/// Representation of conjunctive group of filters + +extension FilterGroup { + + public struct And: FilterGroupType { + + public var filters: [FilterType] + public let name: String? + + public var isDisjuncitve: Bool { + return false + } + + public init(filters: S, name: String? = nil) where S.Element == FilterType { + self.filters = Array(filters) + self.name = name + } + + public static func and(_ filters: [FilterType]) -> FilterGroup.And { + return FilterGroup.And(filters: filters) + } + + public func withFilters(_ filters: S) -> FilterGroup.And where S.Element == FilterType { + return .init(filters: filters, name: name) + } + + } + +} + +extension FilterGroup.And: CustomStringConvertible { + + public var description: String { + return "{ \(name ?? "_"): \(filters) }" + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Groups/FilterGroupType.swift b/Sources/InstantSearchCore/FilterState/FIlter/Groups/FilterGroupType.swift new file mode 100644 index 00000000..ff9530f2 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/Groups/FilterGroupType.swift @@ -0,0 +1,37 @@ +// +// FilterGroup.swift +// AlgoliaSearch +// +// Created by Guy Daher on 14/12/2018. +// Copyright © 2018 Algolia. All rights reserved. +// + +import Foundation + +public enum FilterGroup {} + +public protocol FilterGroupType { + + var name: String? { get } + var filters: [FilterType] { get } + var isDisjuncitve: Bool { get } + + func withFilters(_ filters: S) -> Self where S.Element == FilterType + +} + +extension FilterGroupType { + + var isConjunctive: Bool { + return !isDisjuncitve + } + + public var isEmpty: Bool { + return filters.isEmpty + } + + func contains(_ filter: FilterType) -> Bool { + return filters.contains(where: { Filter($0) == Filter(filter) }) + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Groups/HierarchicalFilterGroup.swift b/Sources/InstantSearchCore/FilterState/FIlter/Groups/HierarchicalFilterGroup.swift new file mode 100644 index 00000000..4035e96b --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/Groups/HierarchicalFilterGroup.swift @@ -0,0 +1,48 @@ +// +// HierarchicalFilterGroup.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension FilterGroup { + + public struct Hierarchical: FilterGroupType { + + public var filters: [FilterType] { + return typedFilters + } + + public let name: String? + + public var isDisjuncitve: Bool { + return false + } + + public var hierarchicalAttributes: [Attribute] = [] + public var hierarchicalFilters: [Filter.Facet] = [] + + internal var typedFilters: [Filter.Facet] + + public init(filters: S, name: String? = nil) where S.Element == Filter.Facet { + self.typedFilters = Array(filters) + self.name = name + } + + public static func hierarchical(_ filters: [Filter.Facet]) -> FilterGroup.Hierarchical { + return FilterGroup.Hierarchical(filters: filters) + } + + public func withFilters(_ filters: S) -> FilterGroup.Hierarchical where S.Element == FilterType { + var updatedGroup = FilterGroup.Hierarchical(filters: filters.compactMap { $0 as? Filter.Facet }, name: name) + updatedGroup.hierarchicalAttributes = hierarchicalAttributes + updatedGroup.hierarchicalFilters = hierarchicalFilters + return updatedGroup + } + + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/Groups/OrFilterGroup.swift b/Sources/InstantSearchCore/FilterState/FIlter/Groups/OrFilterGroup.swift new file mode 100644 index 00000000..159e779f --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/Groups/OrFilterGroup.swift @@ -0,0 +1,53 @@ +// +// OrFilterGroup.swift +// AlgoliaSearch OSX +// +// Created by Vladislav Fitc on 14/01/2019. +// Copyright © 2019 Algolia. All rights reserved. +// +// swiftlint:disable type_name + +import Foundation + +/// Representation of disjunctive group of filters + +extension FilterGroup { + + public struct Or: FilterGroupType { + + public var filters: [FilterType] { + return typedFilters + } + + public let name: String? + + public var isDisjuncitve: Bool { + return true + } + + internal var typedFilters: [T] + + public init(filters: [T] = [], name: String? = nil) { + self.typedFilters = filters + self.name = name + } + + public static func or(_ filters: [T]) -> FilterGroup.Or { + return FilterGroup.Or(filters: filters) + } + + public func withFilters(_ filters: S) -> Or where S.Element == FilterType { + return .init(filters: filters.compactMap { $0 as? T }, name: name) + } + + } + +} + +extension FilterGroup.Or: CustomStringConvertible { + + public var description: String { + return "{ \(name ?? "_"): \(filters) }" + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/NumericFilter.swift b/Sources/InstantSearchCore/FilterState/FIlter/NumericFilter.swift new file mode 100644 index 00000000..810918db --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/NumericFilter.swift @@ -0,0 +1,91 @@ +// +// Numeric.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +/** Defines filter representing a numeric relation or a range + # See also: + [Reference](https:www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-numeric-value/) + */ + +public extension Filter { + + struct Numeric: FilterType, Equatable { + + public enum ValueType: Hashable { + case range(ClosedRange) + case comparison(Operator, Double) + } + + public enum Operator: String, CustomStringConvertible { + case lessThan = "<" + case lessThanOrEqual = "<=" + case equals = "=" + case notEquals = "!=" + case greaterThanOrEqual = ">=" + case greaterThan = ">" + + var inversion: Operator { + switch self { + case .equals: + return .notEquals + case .greaterThan: + return .lessThanOrEqual + case .greaterThanOrEqual: + return .lessThan + case .lessThan: + return .greaterThanOrEqual + case .lessThanOrEqual: + return .greaterThan + case .notEquals: + return .equals + } + } + + public var description: String { + return rawValue + } + + } + + public let attribute: Attribute + public let value: ValueType + public var isNegated: Bool + + init(attribute: Attribute, value: ValueType, isNegated: Bool) { + self.attribute = attribute + self.isNegated = isNegated + self.value = value + } + + public init(attribute: Attribute, range: ClosedRange, isNegated: Bool = false) { + self.init(attribute: attribute, value: .range(range), isNegated: isNegated) + } + + public init(attribute: Attribute, `operator`: Operator, value: Double, isNegated: Bool = false) { + self.init(attribute: attribute, value: .comparison(`operator`, value), isNegated: isNegated) + } + + } + +} + +extension Filter.Numeric: CustomStringConvertible { + + public var description: String { + switch value { + case .range(let range): + return "\(attribute): \(range.lowerBound) – \(range.upperBound)" + case .comparison(let compOperator, let value): + return "\(attribute) \(compOperator.description) \(value)" + } + } + +} + +extension Filter.Numeric: Hashable {} diff --git a/Sources/InstantSearchCore/FilterState/FIlter/TagFilter.swift b/Sources/InstantSearchCore/FilterState/FIlter/TagFilter.swift new file mode 100644 index 00000000..1419a2b2 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FIlter/TagFilter.swift @@ -0,0 +1,51 @@ +// +// Tag.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +/** Defines tag filter + # See also: + [Reference](https:www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-tags/) + */ + +public extension Filter { + + struct Tag: FilterType, Equatable { + + public let attribute: Attribute = .tags + public var isNegated: Bool + public let value: String + + public init(value: String, isNegated: Bool = false) { + self.isNegated = isNegated + self.value = value + } + + } + +} + +extension Filter.Tag: Hashable {} + +extension Filter.Tag: ExpressibleByStringLiteral { + + public typealias StringLiteralType = String + + public init(stringLiteral string: String) { + self.init(value: string, isNegated: false) + } + +} + +extension Filter.Tag: CustomStringConvertible { + + public var description: String { + return "\(value)" + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FilterGroupID.swift b/Sources/InstantSearchCore/FilterState/FilterGroupID.swift new file mode 100644 index 00000000..1211dc50 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FilterGroupID.swift @@ -0,0 +1,96 @@ +// +// FilterGroupID.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// +// swiftlint:disable type_name + +import Foundation + +extension FilterGroup { + + public enum ID: Hashable { + + public enum Filter { + case facet, numeric, tag + + init?(_ filterType: F.Type) { + switch filterType { + case is FacetFilter.Type: + self = .facet + case is NumericFilter.Type: + self = .numeric + case is TagFilter.Type: + self = .tag + default: + return nil + } + } + + } + + case or(name: String, filterType: Filter) + case and(name: String) + case hierarchical(name: String) + + var name: String { + switch self { + case .or(name: let name, _), + .and(name: let name), + .hierarchical(name: let name): + return name + } + } + + var isConjunctive: Bool { + switch self { + case .and, + .hierarchical: + return true + case .or: + return false + } + } + + var isDisjunctive: Bool { + return !isConjunctive + } + + init?(_ filterGroup: FilterGroupType) { + let groupName = filterGroup.name ?? "" + switch filterGroup { + case is FilterGroup.And: + self = .and(name: groupName) + case is FilterGroup.Hierarchical: + self = .hierarchical(name: groupName) + case is FilterGroup.Or: + self = .or(name: groupName, filterType: .facet) + case is FilterGroup.Or: + self = .or(name: groupName, filterType: .numeric) + case is FilterGroup.Or: + self = .or(name: groupName, filterType: .tag) + default: + return nil + } + } + + } + +} + +extension FilterGroup.ID: CustomStringConvertible { + + public var description: String { + switch self { + case .and(name: let name): + return "and<\(name)>" + case .or(name: let name, _): + return "or<\(name)>" + case .hierarchical(name: let name): + return "hierarchical<\(name)>" + } + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FilterGroupsConvertible.swift b/Sources/InstantSearchCore/FilterState/FilterGroupsConvertible.swift new file mode 100644 index 00000000..3a160797 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FilterGroupsConvertible.swift @@ -0,0 +1,15 @@ +// +// FilterGroupsConvertible.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 18/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol FilterGroupsConvertible { + + func toFilterGroups() -> [FilterGroupType] + +} diff --git a/Sources/InstantSearchCore/FilterState/FilterState+Commands.swift b/Sources/InstantSearchCore/FilterState/FilterState+Commands.swift new file mode 100644 index 00000000..e577bb07 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FilterState+Commands.swift @@ -0,0 +1,157 @@ +// +// FilterState+Commands.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 07/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +private protocol FilterStateCommand { + + func execute(on filterState: FilterState) + +} + +private extension FilterState { + + struct Add: FilterStateCommand { + + public let groupID: FilterGroup.ID + public let filters: [T] + + public init(filter: T, groupID: FilterGroup.ID) { + self.init(filters: [filter], groupID: groupID) + } + + public init(filters: S, groupID: FilterGroup.ID) where S.Element == T { + self.groupID = groupID + self.filters = Array(filters) + } + + public func execute(on filterState: FilterState) { + filterState.addAll(filters: filters, toGroupWithID: groupID) + } + + } + + struct Remove: FilterStateCommand { + + public let filters: [T] + public let groupID: FilterGroup.ID? + + public init(filter: T, groupID: FilterGroup.ID? = nil) { + self.init(filters: [filter], groupID: groupID) + } + + public init(filters: [T], groupID: FilterGroup.ID? = nil) { + self.groupID = groupID + self.filters = filters + } + + public func execute(on filterState: FilterState) { + + if let groupID = groupID { + filterState.removeAll(filters, fromGroupWithID: groupID) + } else { + _ = filterState.removeAll(filters) + } + } + + } + + enum Clear: FilterStateCommand { + + case all + case group(FilterGroup.ID) + case attribute(Attribute) + case attributeInGroup(Attribute, FilterGroup.ID) + + public func execute(on filterState: FilterState) { + + switch self { + case .all: + filterState.removeAll() + case .attribute(let attribute): + filterState.removeAll(for: attribute) + case .attributeInGroup(let attribute, let groupID): + filterState.removeAll(for: attribute, fromGroupWithID: groupID) + case .group(let groupID): + filterState.removeAll(fromGroupWithID: groupID) + } + + } + + } + + struct Toggle: FilterStateCommand { + + public let groupID: FilterGroup.ID + public let filters: [T] + + public init(filter: T, groupID: FilterGroup.ID) { + self.init(filters: [filter], groupID: groupID) + } + + public init(filters: [T], groupID: FilterGroup.ID) { + self.groupID = groupID + self.filters = filters + } + + public func execute(on filterState: FilterState) { + filterState.toggle(filters, inGroupWithID: groupID) + } + + } + +} + +public extension FilterState { + + struct Command { + + fileprivate let command: FilterStateCommand + + public static func add(filter: T, toGroupWithID groupID: FilterGroup.ID) -> Command { + return .init(command: FilterState.Add(filter: filter, groupID: groupID)) + } + + public static func add(filters: S, toGroupWithID groupID: FilterGroup.ID) -> Command where S.Element == T { + return .init(command: FilterState.Add(filters: filters, groupID: groupID)) + } + + public static func remove(filter: T, fromGroupWithID groupID: FilterGroup.ID) -> Command { + return .init(command: FilterState.Remove(filter: filter, groupID: groupID)) + } + + public static func remove(filters: [T], fromGroupWithID groupID: FilterGroup.ID) -> Command { + return .init(command: FilterState.Remove(filters: filters, groupID: groupID)) + } + + public static var removeAll: Command = .init(command: FilterState.Clear.all) + + public static func removeAll(fromGroupWithID groupID: FilterGroup.ID) -> Command { + return .init(command: FilterState.Clear.group(groupID)) + } + + public static func removeAll(for attribute: Attribute, fromGroupWithID groupID: FilterGroup.ID? = nil) -> Command { + return .init(command: FilterState.Clear.attribute(attribute)) + } + + public static func toggle(filter: T, toGroupWithID groupID: FilterGroup.ID) -> Command { + return .init(command: FilterState.Toggle(filter: filter, groupID: groupID)) + } + + public static func toggle(filters: [T], toGroupWithID groupID: FilterGroup.ID) -> Command { + return .init(command: FilterState.Toggle(filters: filters, groupID: groupID)) + } + + } + + func notify(_ commands: Command...) { + commands.forEach { $0.command.execute(on: self) } + onChange.fire(ReadOnlyFiltersContainer(filterState: self)) + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FilterState+DisjunctiveFaceting.swift b/Sources/InstantSearchCore/FilterState/FilterState+DisjunctiveFaceting.swift new file mode 100644 index 00000000..e40e88dc --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FilterState+DisjunctiveFaceting.swift @@ -0,0 +1,17 @@ +// +// FilterState+DisjunctiveFaceting.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 05/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension FilterState: DisjunctiveFacetingDelegate { + + public var disjunctiveFacetsAttributes: Set { + return filters.disjunctiveFacetsAttributes + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FilterState+FiltersReadable.swift b/Sources/InstantSearchCore/FilterState/FilterState+FiltersReadable.swift new file mode 100644 index 00000000..ccdd50b3 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FilterState+FiltersReadable.swift @@ -0,0 +1,41 @@ +// +// FilterState+FiltersReadable.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 19/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension FilterState: FiltersReadable { + + func getGroupIDs() -> Set { + return filters.getGroupIDs() + } + + public var isEmpty: Bool { + return filters.isEmpty + } + + func contains(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) -> Bool { + return filters.contains(filter, inGroupWithID: groupID) + } + + func getFilters(forGroupWithID groupID: FilterGroup.ID) -> Set { + return filters.getFilters(forGroupWithID: groupID) + } + + public func getFilters(for attribute: Attribute) -> Set { + return filters.getFilters(for: attribute) + } + + public func getFilters() -> Set { + return filters.getFilters() + } + + public func getFiltersAndID() -> Set { + return filters.getFiltersAndID() + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FilterState+FiltersWritable.swift b/Sources/InstantSearchCore/FilterState/FilterState+FiltersWritable.swift new file mode 100644 index 00000000..4c7704d5 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FilterState+FiltersWritable.swift @@ -0,0 +1,69 @@ +// +// FilterState+FiltersWritable.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 19/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension FilterState: FiltersWritable { + + func add(_ filter: FilterType, toGroupWithID groupID: FilterGroup.ID) { + filters.add(filter, toGroupWithID: groupID) + } + + func addAll(filters: S, toGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType { + self.filters.addAll(filters: filters, toGroupWithID: groupID) + } + + @discardableResult func remove(_ filter: FilterType, fromGroupWithID groupID: FilterGroup.ID) -> Bool { + return filters.remove(filter, fromGroupWithID: groupID) + } + + @discardableResult func removeAll(_ filters: S, fromGroupWithID groupID: FilterGroup.ID) -> Bool where S.Element == FilterType { + return self.filters.removeAll(filters, fromGroupWithID: groupID) + } + + func removeAll(fromGroupWithID groupID: FilterGroup.ID) { + return filters.removeAll(fromGroupWithID: groupID) + } + + func removeAll(fromGroupWithIDs groupIDs: [FilterGroup.ID]) { + return filters.removeAll(fromGroupWithIDs: groupIDs) + } + + func removeAllExcept(fromGroupWithIDs groupIDs: [FilterGroup.ID]) { + return filters.removeAllExcept(groupIDs) + } + + @discardableResult public func remove(_ filter: FilterType) -> Bool { + return filters.remove(filter) + } + + @discardableResult public func removeAll(_ filters: S) -> Bool where S.Element == FilterType { + return self.filters.removeAll(filters) + } + + func removeAll(for attribute: Attribute, fromGroupWithID groupID: FilterGroup.ID) { + filters.removeAll(for: attribute, fromGroupWithID: groupID) + } + + public func removeAll(for attribute: Attribute) { + filters.removeAll(for: attribute) + } + + public func removeAll() { + filters.removeAll() + } + + func toggle(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) { + filters.toggle(filter, inGroupWithID: groupID) + } + + func toggle(_ filters: S, inGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType { + self.filters.toggle(filters, inGroupWithID: groupID) + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FilterState+HierarchicalFaceting.swift b/Sources/InstantSearchCore/FilterState/FilterState+HierarchicalFaceting.swift new file mode 100644 index 00000000..d85cfbf5 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FilterState+HierarchicalFaceting.swift @@ -0,0 +1,38 @@ +// +// FilterState+HierarchicalFaceting.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 05/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension FilterState: HierarchicalFacetingDelegate { + + private var hierarchicalGroupName: String { + return "_hierarchical" + } + + public var hierarchicalFilters: [Filter.Facet] { + get { + return self[hierarchical: hierarchicalGroupName].hierarchicalFilters + } + + set { + self[hierarchical: hierarchicalGroupName].set(newValue) + + } + } + + public var hierarchicalAttributes: [Attribute] { + get { + return self[hierarchical: hierarchicalGroupName].hierarchicalAttributes + } + + set { + self[hierarchical: hierarchicalGroupName].set(newValue) + } + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FilterState.swift b/Sources/InstantSearchCore/FilterState/FilterState.swift new file mode 100644 index 00000000..a4c7f1f8 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FilterState.swift @@ -0,0 +1,112 @@ +// +// FilterState.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 18/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +/** + Encapsulates search filters providing a convenient interface to manage them + */ +public class FilterState { + + typealias Storage = FiltersReadable & FiltersWritable & FilterGroupsConvertible & HierarchicalManageable + + /// Filters container + var filters: Storage + + /// Triggered when an error occured during search query execution + /// - Parameter: a tuple of query and error + public var onChange: Observer + + /// Default constructor + public init() { + self.filters = GroupsStorage() + self.onChange = .init() + } + + /// Copy constructor + public init(_ filterState: FilterState) { + self.filters = filterState.filters + self.onChange = .init() + } + + /// Replace the groups of filter state by the groups of the filter state passed as parameter + /// - Parameter filterState: source filter state + public func setWithContent(of filterState: FilterState) { + self.filters = filterState.filters + } + + /// Force trigger onChange event + public func notifyChange() { + onChange.fire(ReadOnlyFiltersContainer(filterState: self)) + } + + /// Subscript providing access to a conjunctive group with specified name + /// - Parameter groupName: required group name + /// - Returns: required group accessor + public subscript(and groupName: String) -> AndGroupAccessor { + return .init(filtersContainer: self, groupName: groupName) + } + + /// Subscript providing access to disjunctive group with specified name and manually defined filter type + /// To use if filter type cannot be inferred + /// - Parameter groupName: required group name + /// - Returns: required group accessor + public subscript(or groupName: String, type: F.Type) -> OrGroupAccessor { + return .init(filtersContainer: self, groupName: groupName) + } + + /// Subscript providing access to a disjunctive group with specified name + /// - Parameter groupName: required group name + /// - Returns: required group accessor + public subscript(or groupName: String) -> OrGroupAccessor { + return .init(filtersContainer: self, groupName: groupName) + } + + /// Subscript providing access to a hierarchical group with specified name + /// - Parameter groupName: required group name + /// - Returns: required group accessor + public subscript(hierarchical groupName: String) -> HierarchicalGroupAccessor { + return .init(filtersContainer: self, groupName: groupName) + } + +} + +extension FilterState: FiltersContainer {} + +extension FilterState: FilterGroupsConvertible { + + public func toFilterGroups() -> [FilterGroupType] { + return filters.toFilterGroups() + } + +} + +extension FilterState: CustomStringConvertible { + + public var description: String { + return FilterGroupConverter().sql(toFilterGroups()) ?? "" + } + +} + +extension FilterState: CustomDebugStringConvertible { + + public var debugDescription: String { + let filterGroups = toFilterGroups() + guard !filterGroups.isEmpty else { + return "FilterState {}" + } + let body = filterGroups.map { group in + let groupName = (group.name ?? "") + let filtersDescription = FilterGroupConverter().sql(group) ?? "" + return " \"\(groupName)\": \(filtersDescription)" + }.joined(separator: "\n") + return "FilterState {\n\(body)\n}" + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FilterStateDSL.swift b/Sources/InstantSearchCore/FilterState/FilterStateDSL.swift new file mode 100644 index 00000000..e250eab4 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FilterStateDSL.swift @@ -0,0 +1,19 @@ +// +// FilterStateDSL.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 15/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class FilterStateDSL { + + var filters: FiltersReadable & FiltersWritable & FilterGroupsConvertible + + public init() { + self.filters = GroupsStorage() + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FiltersReadable.swift b/Sources/InstantSearchCore/FilterState/FiltersReadable.swift new file mode 100644 index 00000000..159f9110 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FiltersReadable.swift @@ -0,0 +1,81 @@ +// +// FiltersReadable.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 16/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +protocol FiltersReadable { + + /// A Boolean value indicating whether FilterState contains at least on filter + + var isEmpty: Bool { get } + + /// Tests whether FilterState contains a filter + /// - parameter filter: desired filter + + func contains(_ filter: FilterType) -> Bool + + /// Checks whether specified group contains a filter + /// - parameter filter: filter to check + /// - parameter groupID: target group ID + /// - returns: true if filter is contained by specified group + + func contains(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) -> Bool + + /// Returns a set of filters in group with specified ID + /// - parameter groupID: target group ID + + func getGroupIDs() -> Set + + /// Returns a set of filters for attribute + /// - parameter attribute: target attribute + + func getFilters(forGroupWithID groupID: FilterGroup.ID) -> Set + + func getFilters(for attribute: Attribute) -> Set + + /// Returns a set of all the filters contained by all the groups + func getFilters() -> Set + +} + +extension FiltersReadable { + + func contains(_ filter: FilterType) -> Bool { + return getGroupIDs().anySatisfy { contains(filter, inGroupWithID: $0) } + } + + func getFiltersAndID() -> Set { + return Set(getGroupIDs() + .map { groupID in + getFilters(forGroupWithID: groupID).map { (groupID, $0) } + } + .flatMap { $0 } + .map { FilterAndID(filter: $0.1, id: $0.0) }) + } + +} + +extension FiltersReadable { + + var disjunctiveFacetsAttributes: Set { + let attributes = getGroupIDs() + .filter { groupID in + switch groupID { + case .or(_, .facet): + return true + default: + return false + } + } + .map(getFilters(forGroupWithID:)) + .flatMap({ $0 }) + .map { $0.attribute } + return Set(attributes) + } + +} diff --git a/Sources/InstantSearchCore/FilterState/FiltersWritable.swift b/Sources/InstantSearchCore/FilterState/FiltersWritable.swift new file mode 100644 index 00000000..93797fb1 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/FiltersWritable.swift @@ -0,0 +1,127 @@ +// +// FiltersWritable.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 16/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +protocol FiltersWritable { + + /// Adds filter to a specified group + /// - parameter filter: filter to add + /// - parameter groupID: target group ID + + mutating func add(_ filter: FilterType, toGroupWithID groupID: FilterGroup.ID) + + /// Adds a sequence of filters to a specified group + /// - parameter filters: sequence of filters to add + /// - parameter groupID: target group ID + + mutating func addAll(filters: S, toGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType + + /// Removes filter from a specified group + /// - parameter filter: filter to remove + /// - parameter groupID: target group ID + /// - returns: true if removal succeeded, otherwise returns false + + @discardableResult mutating func remove(_ filter: FilterType, fromGroupWithID groupID: FilterGroup.ID) -> Bool + + /// Removes a sequence of filters from a specified group + /// - parameter filters: sequence of filters to remove + /// - parameter groupID: target group ID + /// - returns: true if at least one filter in filters sequence is contained by a specified group and so has been removed, otherwise returns false + + @discardableResult mutating func removeAll(_ filters: S, fromGroupWithID groupID: FilterGroup.ID) -> Bool where S.Element == FilterType + + /// Removes all filters from a specifed group + /// - parameter group: target group ID + + mutating func removeAll(fromGroupWithID groupID: FilterGroup.ID) + + mutating func removeAll(fromGroupWithIDs groupIDs: [FilterGroup.ID]) + + /// Removes filter from all the groups + /// - parameter filter: filter to remove + /// - returns: true if specified filter has been removed from at least one group, otherwise returns false + + @discardableResult mutating func remove(_ filter: FilterType) -> Bool + + /// Removes a sequence of filters from all the groups + /// - parameter filters: sequence of filters to remove + + mutating func removeAll(_ filters: S) -> Bool where S.Element == FilterType + + /// Removes all filters with specified attribute in a specified group + /// - parameter attribute: target attribute + /// - parameter groupID: target group ID + + mutating func removeAll(for attribute: Attribute, fromGroupWithID groupID: FilterGroup.ID) + + /// Removes all filters with specified attribute in all the groups + /// - parameter attribute: target attribute + + mutating func removeAll(for attribute: Attribute) + + /// Removes all filters from all the groups + + mutating func removeAll() + + /// Removes filter from group if contained by it, otherwise adds filter to group + /// - parameter filter: filter to toggle + /// - parameter groupID: target group ID + + mutating func toggle(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) + + /// Toggles a sequence of filters in group + /// - parameter filters: sequence of filters to toggle + /// - parameter groupID: target group ID + + mutating func toggle(_ filters: S, inGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType + +} + +extension FiltersWritable { + + mutating func add(_ filter: FilterType, toGroupWithID groupID: FilterGroup.ID) { + addAll(filters: [filter], toGroupWithID: groupID) + } + + @discardableResult mutating func remove(_ filter: FilterType, fromGroupWithID groupID: FilterGroup.ID) -> Bool { + return removeAll([filter], fromGroupWithID: groupID) + } + + @discardableResult mutating func remove(_ filter: FilterType) -> Bool { + return removeAll([filter]) + } + + mutating func removeAll(fromGroupWithID groupID: FilterGroup.ID) { + removeAll(fromGroupWithIDs: [groupID]) + } + + mutating func toggle(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) { + toggle([filter], inGroupWithID: groupID) + } + +} + +extension FiltersWritable where Self: FiltersReadable { + + mutating func toggle(_ filters: S, inGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType { + for filter in filters { + if contains(filter, inGroupWithID: groupID) { + _ = remove(filter, fromGroupWithID: groupID) + } else { + add(filter, toGroupWithID: groupID) + } + } + } + + mutating func removeAllExcept(_ groupIDsToKeep: [FilterGroup.ID]) { + let groupIDsToRemove = getGroupIDs().filter { !groupIDsToKeep.contains($0) } + removeAll(fromGroupWithIDs: Array(groupIDsToRemove)) + } + +} diff --git a/Sources/InstantSearchCore/FilterState/GroupStorage.swift b/Sources/InstantSearchCore/FilterState/GroupStorage.swift new file mode 100644 index 00000000..33ab5996 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/GroupStorage.swift @@ -0,0 +1,260 @@ +// +// GroupStorage.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 15/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +struct GroupsStorage { + + var filterGroups: [FilterGroup.ID: FilterGroupType] + + init() { + filterGroups = [:] + } + +} + +extension GroupsStorage: FilterGroupsConvertible { + + func toFilterGroups() -> [FilterGroupType] { + + let filterComparator: (FilterType, FilterType) -> Bool = { + let converter = FilterConverter() + let lhsString = converter.sql($0)! + let rhsString = converter.sql($1)! + return lhsString < rhsString + } + + let groupIDComparator: (FilterGroup.ID, FilterGroup.ID) -> Bool = { + guard $0.name != $1.name else { + switch ($0, $1) { + case (.or, .and): + return true + default: + return false + } + } + return $0.name < $1.name + } + + let transform: (FilterGroup.ID, FilterGroupType) -> FilterGroupType = { (groupID, filterGroup) in + + let sortedFilters = filterGroup.filters.sorted(by: filterComparator) + + switch groupID { + case .and: + return FilterGroup.And(filters: sortedFilters, name: groupID.name) + case .hierarchical: + return FilterGroup.And(filters: sortedFilters.compactMap { $0 as? Filter.Facet }, name: groupID.name) + case .or(_, .facet): + return FilterGroup.Or(filters: sortedFilters.compactMap { $0 as? Filter.Facet }, name: groupID.name) + case .or(_, .tag): + return FilterGroup.Or(filters: sortedFilters.compactMap { $0 as? Filter.Tag }, name: groupID.name) + case .or(_, .numeric): + return FilterGroup.Or(filters: sortedFilters.compactMap { $0 as? Filter.Numeric }, name: groupID.name) + } + + } + + return filterGroups + .sorted(by: { groupIDComparator($0.key, $1.key) }) + .compactMap(transform) + .filter { !$0.filters.isEmpty } + + } + +} + +extension GroupsStorage: FiltersReadable { + + func getFilters(forGroupWithID groupID: FilterGroup.ID) -> Set { + return Set(filterGroups[groupID]?.filters.map(Filter.init) ?? []) + } + + func getFilters(for attribute: Attribute) -> Set { + return Set(getFilters().filter { $0.attribute == attribute }) + } + + func getFiltersAndID() -> Set { + return Set(filterGroups + .map { (id, group) in group.filters.map { (id, $0) } } + .flatMap { $0 } + .map { FilterAndID(filter: Filter($0.1), id: $0.0) }) + } + + func getFilters() -> Set { + return Set(filterGroups.values.flatMap { $0.filters }.map(Filter.init)) + } + + var isEmpty: Bool { + return filterGroups.values.allSatisfy { $0.filters.isEmpty } + } + + func contains(_ filter: FilterType, inGroupWithID groupID: FilterGroup.ID) -> Bool { + return filterGroups[groupID]?.contains(filter) == true + } + + func getGroupIDs() -> Set { + return Set(filterGroups.keys) + } + +} + +extension GroupsStorage: FiltersWritable { + + private func emptyGroup(with filterGroupID: FilterGroup.ID) -> FilterGroupType { + switch filterGroupID { + case .and(name: let name): + return FilterGroup.And(filters: [], name: name) + case .hierarchical(name: let name): + return FilterGroup.Hierarchical(filters: [], name: name) + case .or(name: let name, filterType: .facet): + return FilterGroup.Or(filters: [], name: name) + case .or(name: let name, filterType: .numeric): + return FilterGroup.Or(filters: [], name: name) + case .or(name: let name, filterType: .tag): + return FilterGroup.Or(filters: [], name: name) + } + } + + mutating func addAll(filters: S, toGroupWithID groupID: FilterGroup.ID) where S.Element == FilterType { + let group = filterGroups[groupID] ?? emptyGroup(with: groupID) + let updatedFilters = Set(group.filters.map(Filter.init)).union(filters.map(Filter.init)).map { $0.filter } + filterGroups[groupID] = group.withFilters(updatedFilters) + } + + @discardableResult mutating func removeAll(_ filters: S, fromGroupWithID groupID: FilterGroup.ID) -> Bool where S.Element == FilterType { + guard let existingGroup = filterGroups[groupID] else { + return false + } + + let filtersToRemove = filters.map(Filter.init) + let updatedFilters = existingGroup.filters.filter { !filtersToRemove.contains(Filter($0)) } + let updatedGroup = existingGroup.withFilters(updatedFilters) + if updatedGroup.isEmpty { + filterGroups.removeValue(forKey: groupID) + } else { + filterGroups[groupID] = updatedGroup + } + return existingGroup.filters.count > updatedFilters.count + } + + mutating func removeAll(fromGroupWithIDs groupIDs: [FilterGroup.ID]) { + for groupID in groupIDs { + switch groupID { + case .hierarchical: + filterGroups[groupID] = filterGroups[groupID]?.withFilters([]) + default: + filterGroups.removeValue(forKey: groupID) + } + } + } + + @discardableResult mutating func removeAll(_ filters: S) -> Bool where S.Element == FilterType { + var wasRemoved = false + let filtersToRemove = Set(filters.map(Filter.init)) + for (id, group) in filterGroups { + let updatedFilters = group.filters.filter { !filtersToRemove.contains(Filter($0)) } + filterGroups[id] = group.withFilters(updatedFilters) + wasRemoved = wasRemoved || updatedFilters.count < group.filters.count + } + return wasRemoved + } + + mutating func removeAll(for attribute: Attribute, fromGroupWithID groupID: FilterGroup.ID) { + guard let existingGroup = filterGroups[groupID] else { + return + } + + let updatedFilters = existingGroup.filters.filter { $0.attribute != attribute } + filterGroups[groupID] = existingGroup.withFilters(updatedFilters) + } + + mutating func removeAll(for attribute: Attribute) { + for (groupID, group) in filterGroups { + let updatedFilters = group.filters.filter { $0.attribute != attribute } + filterGroups[groupID] = group.withFilters(updatedFilters) + } + } + + mutating func removeAll() { + filterGroups.removeAll() + } + +} + +extension GroupsStorage: HierarchicalManageable { + + func hierarchicalGroup(withName groupName: String) -> FilterGroup.Hierarchical? { + return filterGroups[.hierarchical(name: groupName)].flatMap { $0 as? FilterGroup.Hierarchical } + } + + func hierarchicalAttributes(forGroupWithName groupName: String) -> [Attribute] { + return hierarchicalGroup(withName: groupName)?.hierarchicalAttributes ?? [] + } + + func hierarchicalFilters(forGroupWithName groupName: String) -> [Filter.Facet] { + return hierarchicalGroup(withName: groupName)?.hierarchicalFilters ?? [] + + } + + mutating func set(_ hierarchicalAttributes: [Attribute], forGroupWithName groupName: String) { + let groupID: FilterGroup.ID = .hierarchical(name: groupName) + var updatedGroup: FilterGroup.Hierarchical = (filterGroups[groupID] as? FilterGroup.Hierarchical) ?? .init(filters: [], name: groupName) + updatedGroup.hierarchicalAttributes = hierarchicalAttributes + filterGroups[groupID] = updatedGroup + } + + mutating func set(_ hierarchicalFilters: [Filter.Facet], forGroupWithName groupName: String) { + let groupID: FilterGroup.ID = .hierarchical(name: groupName) + var updatedGroup: FilterGroup.Hierarchical = (filterGroups[groupID] as? FilterGroup.Hierarchical) ?? .init(filters: [], name: groupName) + updatedGroup.hierarchicalFilters = hierarchicalFilters + filterGroups[groupID] = updatedGroup + } + +} + +extension GroupsStorage { + + /// Returns a set of attributes suitable for disjunctive faceting + func getDisjunctiveFacetsAttributes() -> Set { + let attributes = filterGroups + .values + .filter { $0.isDisjuncitve } + .compactMap { $0.filters.compactMap { $0 as? Filter.Facet } } + .flatMap { $0 } + .map { $0.attribute } + return Set(attributes) + + } + + /// Returns a dictionary of all facet filters with their associated values + func getFacetFilters() -> [Attribute: Set] { + let facetFilters: [Filter.Facet] = filterGroups.values.flatMap { $0.filters.compactMap { $0 as? Filter.Facet } } + + var refinements: [Attribute: Set] = [:] + for filter in facetFilters { + let existingValues = refinements[filter.attribute, default: []] + let updatedValues = existingValues.union([filter.value]) + refinements[filter.attribute] = updatedValues + } + return refinements + } + + /// Returns a raw representaton of all facet filters with their associated values + func getRawFacetFilters() -> [String: [String]] { + return getFacetFilters() + .map { ($0.key.rawValue, $0.value.map { $0.description }) } + .reduce([String: [String]]()) { (refinements, arg1) in + let (attribute, values) = arg1 + return refinements.merging([attribute: values], uniquingKeysWith: { (_, new) -> [String] in + new + }) + } + } + +} diff --git a/Sources/InstantSearchCore/FilterState/HierarchicalManageable.swift b/Sources/InstantSearchCore/FilterState/HierarchicalManageable.swift new file mode 100644 index 00000000..b03f1400 --- /dev/null +++ b/Sources/InstantSearchCore/FilterState/HierarchicalManageable.swift @@ -0,0 +1,18 @@ +// +// HierarchicalManageable.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 16/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol HierarchicalManageable { + + func hierarchicalAttributes(forGroupWithName groupName: String) -> [Attribute] + func hierarchicalFilters(forGroupWithName groupName: String) -> [Filter.Facet] + mutating func set(_ hierarchicalAttributes: [Attribute], forGroupWithName groupName: String) + mutating func set(_ hierarchicalFilters: [Filter.Facet], forGroupWithName groupName: String) + +} diff --git a/Sources/InstantSearchCore/Helper/UserAgentSetter.swift b/Sources/InstantSearchCore/Helper/UserAgentSetter.swift new file mode 100644 index 00000000..8e24934c --- /dev/null +++ b/Sources/InstantSearchCore/Helper/UserAgentSetter.swift @@ -0,0 +1,19 @@ +// +// UserAgentSetter.swift +// +// +// Created by Vladislav Fitc on 16/06/2020. +// + +import Foundation + +struct UserAgentSetter { + + /// Add the library's version to the client's user agents, if not already present. + static let set = Self() + + init() { + UserAgentController.append(userAgent: .init(title: "InstantSearch iOS", version: Version.current.description)) + } + +} diff --git a/Sources/InstantSearchCore/Helper/Version+Current.swift b/Sources/InstantSearchCore/Helper/Version+Current.swift new file mode 100644 index 00000000..e2421ab3 --- /dev/null +++ b/Sources/InstantSearchCore/Helper/Version+Current.swift @@ -0,0 +1,2 @@ +// This is generated file. Don't modify it manually. +public extension Version { static let current: Version = .init(major: 7, minor: 0, patch: 0, prereleaseIdentifier: nil) } diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalConnector.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalConnector.swift new file mode 100644 index 00000000..d9795cd1 --- /dev/null +++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalConnector.swift @@ -0,0 +1,42 @@ +// +// HierarchicalConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class HierarchicalConnector: Connection { + + public let searcher: SingleIndexSearcher + public let filterState: FilterState + public let interactor: HierarchicalInteractor + + public let searcherConnection: Connection + public let filterStateConnection: Connection + + public init(searcher: SingleIndexSearcher, + attribute: Attribute, + filterState: FilterState, + hierarchicalAttributes: [Attribute], + separator: String) { + self.searcher = searcher + self.filterState = filterState + self.interactor = HierarchicalInteractor(hierarchicalAttributes: hierarchicalAttributes, separator: separator) + self.searcherConnection = interactor.connectSearcher(searcher: searcher) + self.filterStateConnection = interactor.connectFilterState(filterState) + } + + public func connect() { + searcherConnection.connect() + filterStateConnection.connect() + } + + public func disconnect() { + searcherConnection.disconnect() + filterStateConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalController.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalController.swift new file mode 100644 index 00000000..9131d195 --- /dev/null +++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalController.swift @@ -0,0 +1,15 @@ +// +// HierarchicalController.swift +// InstantSearchCore +// +// Created by Guy Daher on 08/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol HierarchicalController: ItemController { + + var onClick: ((String) -> Void)? { get set } + +} diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Controller.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Controller.swift new file mode 100644 index 00000000..7b5c6a65 --- /dev/null +++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Controller.swift @@ -0,0 +1,68 @@ +// +// HierarchicalInteractor+Controller.swift +// InstantSearchCore +// +// Created by Guy Daher on 08/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias HierarchicalFacet = (facet: Facet, level: Int, isSelected: Bool) + +public extension Hierarchical { + + struct ControllerConnection: Connection where Output == Controller.Item { + + public let interactor: HierarchicalInteractor + public let controller: Controller + public let presenter: ([HierarchicalFacet]) -> Output + + public func connect() { + let presenter = self.presenter + interactor.onItemChanged.subscribePast(with: controller) { [weak interactor] controller, facets in + guard let interactor = interactor else { return } + + let hierarchicalFacets = facets.enumerated() + .map { index, items in + items.map { item in + (item, index, interactor.selections.contains(item.value)) + } + }.flatMap { $0 } + + controller.setItem(presenter(hierarchicalFacets)) + }.onQueue(.main) + + controller.onClick = interactor.computeSelection(key:) + } + + public func disconnect() { + interactor.onItemChanged.cancelSubscription(for: controller) + controller.onClick = nil + } + + } + +} + +public extension HierarchicalInteractor { + + @discardableResult func connectController(_ controller: Controller, + presenter: @escaping ([HierarchicalFacet]) -> Output) -> Hierarchical.ControllerConnection { + let connection = Hierarchical.ControllerConnection(interactor: self, controller: controller, presenter: presenter) + connection.connect() + return connection + } + +} + +public extension HierarchicalInteractor { + + @discardableResult func connectController(_ controller: Controller, + presenter: @escaping HierarchicalPresenter = DefaultPresenter.Hierarchical.present) -> Hierarchical.ControllerConnection { + let connection = Hierarchical.ControllerConnection(interactor: self, controller: controller, presenter: presenter) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+FilterState.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+FilterState.swift new file mode 100644 index 00000000..2cf17b08 --- /dev/null +++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+FilterState.swift @@ -0,0 +1,64 @@ +// +// HierarchicalInteractor+FilterState.swift +// InstantSearchCore +// +// Created by Guy Daher on 03/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension HierarchicalInteractor { + + struct FilterStateConnection: Connection { + + public let interactor: HierarchicalInteractor + public let filterState: FilterState + + public func connect() { + + let groupName = "_hierarchical" + + filterState[hierarchical: groupName].set(interactor.hierarchicalAttributes) + + interactor.onSelectionsComputed.subscribePast(with: filterState) { [weak interactor] filterState, selections in + + interactor?.selections = selections.map { $0.value.description } + + filterState[hierarchical: groupName].removeAll() + + guard let lastSelectedFilter = selections.last else { + filterState[hierarchical: groupName].set([Filter.Facet]()) + return + } + + filterState[hierarchical: groupName].add(lastSelectedFilter) + filterState[hierarchical: groupName].set(selections) + filterState.notifyChange() + + } + + filterState.onChange.subscribePast(with: interactor) { _, _ in + // TODO + } + + } + + public func disconnect() { + interactor.onSelectionsChanged.cancelSubscription(for: filterState) + filterState.onChange.cancelSubscription(for: interactor) + } + + } + +} + +public extension HierarchicalInteractor { + + @discardableResult func connectFilterState(_ filterState: FilterState) -> FilterStateConnection { + let connection = FilterStateConnection(interactor: self, filterState: filterState) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Searcher.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Searcher.swift new file mode 100644 index 00000000..5642e804 --- /dev/null +++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor+Searcher.swift @@ -0,0 +1,53 @@ +// +// HierarchicalInteractor+Searcher.swift +// InstantSearchCore +// +// Created by Guy Daher on 03/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +public extension HierarchicalInteractor { + + struct SingleIndexSearcherConnection: Connection { + + public let interactor: HierarchicalInteractor + public let searcher: SingleIndexSearcher + + public func connect() { + + for attribute in interactor.hierarchicalAttributes { + searcher.indexQueryState.query.updateQueryFacets(with: attribute) + } + + searcher.onResults.subscribePast(with: interactor) { interactor, searchResults in + + if let hierarchicalFacets = searchResults.hierarchicalFacets { + interactor.item = interactor.hierarchicalAttributes.map { hierarchicalFacets[$0] }.compactMap { $0 } + } else if let firstHierarchicalAttribute = interactor.hierarchicalAttributes.first { + interactor.item = searchResults.facets?[firstHierarchicalAttribute].flatMap { [$0] } ?? [] + } else { + interactor.item = [] + } + } + + } + + public func disconnect() { + searcher.onResults.cancelSubscription(for: interactor) + } + + } + +} + +public extension HierarchicalInteractor { + + @discardableResult func connectSearcher(searcher: SingleIndexSearcher) -> SingleIndexSearcherConnection { + let connection = SingleIndexSearcherConnection(interactor: self, searcher: searcher) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor.swift b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor.swift new file mode 100644 index 00000000..25657d17 --- /dev/null +++ b/Sources/InstantSearchCore/Hierarchical/HierarchicalInteractor.swift @@ -0,0 +1,58 @@ +// +// HiearchicalInteractor.swift +// InstantSearchCore +// +// Created by Guy Daher on 03/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias HierarchicalPath = [Filter.Facet] + +public class HierarchicalInteractor: ItemInteractor<[[Facet]]> { + + let hierarchicalAttributes: [Attribute] + let separator: String + + // Might be a string? + public var selections: [String] { + didSet { + if oldValue != selections { + onSelectionsChanged.fire(selections) + } + } + } + + public let onSelectionsChanged: Observer<[String]> + public let onSelectionsComputed: Observer + + public init(hierarchicalAttributes: [Attribute], separator: String) { + self.hierarchicalAttributes = hierarchicalAttributes + self.separator = separator + self.onSelectionsChanged = .init() + self.onSelectionsComputed = .init() + self.selections = [] + super.init(item: []) + + } + + public func computeSelection(key: String) { + let selections = key.subPaths(withSeparator: separator) + let hierarchicalPath = zip(hierarchicalAttributes, selections).map { Filter.Facet(attribute: $0, stringValue: $1) } + onSelectionsComputed.fire(hierarchicalPath) + } +} + +public enum Hierarchical {} + +extension String { + + func subPaths(withSeparator separator: String) -> [String] { + return components(separatedBy: separator).reduce([]) { (paths, component) in + let newPath = paths.last.flatMap { $0 + separator + component } ?? component + return paths + [newPath] + } + } + +} diff --git a/Sources/InstantSearchCore/Highlighting/NSAttributedString+TaggedString.swift b/Sources/InstantSearchCore/Highlighting/NSAttributedString+TaggedString.swift new file mode 100644 index 00000000..e0ac7588 --- /dev/null +++ b/Sources/InstantSearchCore/Highlighting/NSAttributedString+TaggedString.swift @@ -0,0 +1,79 @@ +// +// NSAttributedString+TaggedString.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 14/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension Hit { + + /// Returns a highlighted string for a string key if highlightResult has a flat dictionary structure + /// If the value for key is missing or it is an embedded structure, returns nil + func hightlightedString(forKey key: String) -> HighlightedString? { + return highlightResult?.value(forKey: key)?.value?.value + } + +} + +extension NSAttributedString { + + public convenience init(taggedString: TaggedString, + inverted: Bool = false, + attributes: [NSAttributedString.Key: Any]) { + let attributedString = NSMutableAttributedString(string: taggedString.output) + let ranges = inverted ? taggedString.untaggedRanges : taggedString.taggedRanges + ranges.forEach { range in + attributedString.addAttributes(attributes, range: NSRange(range, in: taggedString.output)) + } + self.init(attributedString: attributedString) + } + + public convenience init(highlightedString: HighlightedString, + inverted: Bool = false, + attributes: [NSAttributedString.Key: Any]) { + self.init(taggedString: highlightedString.taggedString, inverted: inverted, attributes: attributes) + } + + public convenience init(highlightResult: HighlightResult, + inverted: Bool = false, + attributes: [NSAttributedString.Key: Any]) { + self.init(taggedString: highlightResult.value.taggedString, inverted: inverted, attributes: attributes) + } + + public convenience init(taggedStrings: [TaggedString], + inverted: Bool = false, + separator: NSAttributedString, + attributes: [NSAttributedString.Key: Any]) { + + let resultString = NSMutableAttributedString() + + for (idx, taggedString) in taggedStrings.enumerated() { + + let substring = NSAttributedString(taggedString: taggedString, inverted: inverted, attributes: attributes) + resultString.append(substring) + + // No need to add separator if joined last substring + if idx != taggedStrings.endIndex - 1 { + resultString.append(separator) + } + } + + self.init(attributedString: resultString) + + } + + public convenience init(highlightedResults: [HighlightResult], + inverted: Bool = false, + separator: NSAttributedString, + attributes: [NSAttributedString.Key: Any]) { + let taggedStrings = highlightedResults.map { $0.value.taggedString } + self.init(taggedStrings: taggedStrings, + inverted: inverted, + separator: separator, + attributes: attributes) + } + +} diff --git a/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift b/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift new file mode 100644 index 00000000..feb7c480 --- /dev/null +++ b/Sources/InstantSearchCore/Hits/AnyHitsInteractor.swift @@ -0,0 +1,51 @@ +// +// AnyHitsInteractor.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 15/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// +import Foundation +import AlgoliaSearchClient +/** This is a type-erasure protocol for HitsInteractor which makes possible + to create a collections of hits interactors with different record types. +*/ + +public protocol AnyHitsInteractor: class { + + var onError: Observer { get } + + var pageLoader: PageLoadable? { get set } + + /// Updates search results with a search results with a hit of JSON type. + /// Internally it tries to convert JSON to a record type of hits Interactor + /// - Parameter searchResults: + /// - Throws: HitsInteractor.Error.incompatibleRecordType if the derived record type mismatches the record type of corresponding hits Interactor + + @discardableResult func update(_ searchResults: HitsExtractable & SearchStatsConvertible) -> Operation + + /// Returns a hit for row of a desired type + /// - Throws: HitsInteractor.Error.incompatibleRecordType if the derived record type mismatches the record type of corresponding hits Interactor + + func genericHitAtIndex(_ index: Int) throws -> R? + + /// Returns a hit for row as dictionary + + func rawHitAtIndex(_ index: Int) -> [String: Any]? + + /// Returns number of hits + func numberOfHits() -> Int + + /// Returns currently stored hits of a desired type + /// This method doesn't trigger pages loading for infinite scrolling + /// - Throws: HitsInteractor.Error.incompatibleRecordType if the derived record type mismatches the record type of corresponding hits Interactor + func getCurrentGenericHits() throws -> [R] + + /// Returns currently stored raw hits + /// This method doesn't trigger pages loading for infinite scrolling + func getCurrentRawHits() -> [[String: Any]] + + func notifyQueryChanged() + func process(_ error: Swift.Error, for query: Query) + +} diff --git a/Sources/InstantSearchCore/Hits/GeoHitsController.swift b/Sources/InstantSearchCore/Hits/GeoHitsController.swift new file mode 100644 index 00000000..dd6083a2 --- /dev/null +++ b/Sources/InstantSearchCore/Hits/GeoHitsController.swift @@ -0,0 +1,17 @@ +// +// GeoHitsController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/10/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol GeoHitsController: class, Reloadable { + + associatedtype DataSource: HitsSource where DataSource.Record: Geolocated + + var hitsSource: DataSource? { get set } + +} diff --git a/Sources/InstantSearchCore/Hits/HitsConnector.swift b/Sources/InstantSearchCore/Hits/HitsConnector.swift new file mode 100644 index 00000000..63247036 --- /dev/null +++ b/Sources/InstantSearchCore/Hits/HitsConnector.swift @@ -0,0 +1,94 @@ +// +// HitsConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class HitsConnector: Connection { + + public let searcher: Searcher + public let interactor: HitsInteractor + public let filterState: FilterState? + + public let filterStateConnection: Connection? + public let searcherConnection: Connection + + internal init(searcher: S, + interactor: HitsInteractor, + filterState: FilterState? = .none, + connectSearcher: (S) -> Connection) { + self.searcher = searcher + self.filterState = filterState + self.interactor = interactor + self.filterStateConnection = filterState.flatMap(interactor.connectFilterState) + self.searcherConnection = connectSearcher(searcher) + } + + public func connect() { + filterStateConnection?.connect() + searcherConnection.connect() + } + + public func disconnect() { + filterStateConnection?.disconnect() + searcherConnection.disconnect() + } + +} + +public extension HitsConnector { + + convenience init(searcher: SingleIndexSearcher, + interactor: HitsInteractor, + filterState: FilterState? = .none) { + self.init(searcher: searcher, + interactor: interactor, + filterState: filterState, + connectSearcher: interactor.connectSearcher) + } + + convenience init(appID: ApplicationID, + apiKey: APIKey, + indexName: IndexName, + interactor: HitsInteractor, + filterState: FilterState? = .none) { + let searcher = SingleIndexSearcher(appID: appID, + apiKey: apiKey, + indexName: indexName) + self.init(searcher: searcher, + interactor: interactor, + filterState: filterState, + connectSearcher: interactor.connectSearcher) + } + +} + +public typealias PlaceHit = Hit + +public extension HitsConnector where Hit == PlaceHit { + + convenience init(searcher: PlacesSearcher, + interactor: HitsInteractor, + filterState: FilterState? = .none) { + self.init(searcher: searcher, + interactor: interactor, + filterState: filterState, + connectSearcher: interactor.connectPlacesSearcher) + } + + convenience init(placesAppID: ApplicationID, + apiKey: APIKey, + interactor: HitsInteractor, + filterState: FilterState? = .none) { + let searcher = PlacesSearcher(appID: placesAppID, apiKey: apiKey) + self.init(searcher: searcher, + interactor: interactor, + filterState: filterState, + connectSearcher: interactor.connectPlacesSearcher) + } + +} diff --git a/Sources/InstantSearchCore/Hits/HitsController.swift b/Sources/InstantSearchCore/Hits/HitsController.swift new file mode 100644 index 00000000..df3140f7 --- /dev/null +++ b/Sources/InstantSearchCore/Hits/HitsController.swift @@ -0,0 +1,19 @@ +// +// HitsController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 23/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol HitsController: class, Reloadable { + + associatedtype DataSource: HitsSource + + var hitsSource: DataSource? { get set } + + func scrollToTop() + +} diff --git a/Sources/InstantSearchCore/Hits/HitsExtractable.swift b/Sources/InstantSearchCore/Hits/HitsExtractable.swift new file mode 100644 index 00000000..4d7f096a --- /dev/null +++ b/Sources/InstantSearchCore/Hits/HitsExtractable.swift @@ -0,0 +1,35 @@ +// +// HitsResponse.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 15/04/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation + +public protocol HitsExtractable { + + func extractHits() throws -> [T] + +} + +extension SearchResponse: HitsExtractable {} + +extension PlacesResponse: HitsExtractable { + + public func extractHits() throws -> [T] where T: Decodable { + let hitsData = try JSONEncoder().encode(hits) + return try JSONDecoder().decode([T].self, from: hitsData) + } + +} + +extension FacetSearchResponse: HitsExtractable { + + public func extractHits() throws -> [T] where T: Decodable { + let hitsData = try JSONEncoder().encode(facetHits) + return try JSONDecoder().decode([T].self, from: hitsData) + } + +} diff --git a/Sources/InstantSearchCore/Hits/HitsInteractor+Controller.swift b/Sources/InstantSearchCore/Hits/HitsInteractor+Controller.swift new file mode 100644 index 00000000..b7551a05 --- /dev/null +++ b/Sources/InstantSearchCore/Hits/HitsInteractor+Controller.swift @@ -0,0 +1,66 @@ +// +// HitsInteractor+Controller.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 25/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension HitsInteractor { + + struct ControllerConnection: Connection where Controller.DataSource == HitsInteractor { + + public let interactor: HitsInteractor + public let controller: Controller + public let externalReload: Bool + + public init(interactor: HitsInteractor, + controller: Controller, + externalReload: Bool = false) { + self.interactor = interactor + self.controller = controller + self.externalReload = externalReload + } + + public func connect() { + controller.hitsSource = interactor + + interactor.onRequestChanged.subscribe(with: controller) { controller, _ in + controller.scrollToTop() + }.onQueue(.main) + + if !externalReload { + interactor.onResultsUpdated.subscribePast(with: controller) { controller, _ in + controller.reload() + }.onQueue(.main) + } + } + + public func disconnect() { + if controller.hitsSource === interactor { + controller.hitsSource = nil + } + interactor.onRequestChanged.cancelSubscription(for: controller) + if !externalReload { + interactor.onResultsUpdated.cancelSubscription(for: controller) + } + } + + } + +} + +public extension HitsInteractor { + + @discardableResult func connectController(_ controller: Controller, + externalReload: Bool = false) -> ControllerConnection where Controller.DataSource == HitsInteractor { + let connection = ControllerConnection(interactor: self, + controller: controller, + externalReload: externalReload) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Hits/HitsInteractor+FilterState.swift b/Sources/InstantSearchCore/Hits/HitsInteractor+FilterState.swift new file mode 100644 index 00000000..9ff25dda --- /dev/null +++ b/Sources/InstantSearchCore/Hits/HitsInteractor+FilterState.swift @@ -0,0 +1,36 @@ +// +// HitsInteractor+FilterState.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 25/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public struct HitsInteractorFilterStateConnection: Connection { + + public let interactor: Interactor + public let filterState: FilterState + + public func connect() { + filterState.onChange.subscribePast(with: interactor) { interactor, _ in + interactor.notifyQueryChanged() + } + } + + public func disconnect() { + filterState.onChange.cancelSubscription(for: interactor) + } + +} + +public extension AnyHitsInteractor { + + @discardableResult func connectFilterState(_ filterState: FilterState) -> Connection { + let connection = HitsInteractorFilterStateConnection(interactor: self, filterState: filterState) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Hits/HitsInteractor+GeoHitsController.swift b/Sources/InstantSearchCore/Hits/HitsInteractor+GeoHitsController.swift new file mode 100644 index 00000000..52a7364f --- /dev/null +++ b/Sources/InstantSearchCore/Hits/HitsInteractor+GeoHitsController.swift @@ -0,0 +1,44 @@ +// +// HitsInteractor+GeoHitsController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/10/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension HitsInteractor where Record: Geolocated { + + struct GeoHitsControllerConnection: Connection where Controller.DataSource == HitsInteractor { + + let interactor: HitsInteractor + let controller: Controller + + public func connect() { + controller.hitsSource = interactor + interactor.onResultsUpdated.subscribePast(with: controller) { (controller, _) in + controller.reload() + }.onQueue(.main) + } + + public func disconnect() { + if controller.hitsSource === interactor { + controller.hitsSource = nil + } + interactor.onResultsUpdated.cancelSubscription(for: controller) + } + + } + +} + +public extension HitsInteractor where Record: Geolocated { + + @discardableResult func connectController(_ controller: Controller) -> GeoHitsControllerConnection where Controller.DataSource == HitsInteractor { + let connection = GeoHitsControllerConnection(interactor: self, controller: controller) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Hits/HitsInteractor+PlacesSearcher.swift b/Sources/InstantSearchCore/Hits/HitsInteractor+PlacesSearcher.swift new file mode 100644 index 00000000..4b4fc0a4 --- /dev/null +++ b/Sources/InstantSearchCore/Hits/HitsInteractor+PlacesSearcher.swift @@ -0,0 +1,57 @@ +// +// HitsInteractor+PlacesSearcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension HitsInteractor where Record == Hit { + + struct PlacesSearcherConnection: Connection { + + public let interactor: HitsInteractor + public let searcher: PlacesSearcher + + public func connect() { + + interactor.pageLoader = searcher + + searcher.onResults.subscribePast(with: interactor) { interactor, searchResults in + interactor.update(searchResults) + } + + searcher.onError.subscribe(with: interactor) { _, _ in + //TODO: when pagination added, notify pending query in infinite scrolling controller + } + + searcher.onQueryChanged.subscribe(with: interactor) { (interactor, _) in + interactor.notifyQueryChanged() + } + + } + + public func disconnect() { + if interactor.pageLoader === searcher { + interactor.pageLoader = nil + } + searcher.onResults.cancelSubscription(for: interactor) + searcher.onError.cancelSubscription(for: interactor) + searcher.onQueryChanged.cancelSubscription(for: interactor) + } + + } + +} + +public extension HitsInteractor where Record == Hit { + + @discardableResult func connectPlacesSearcher(_ searcher: PlacesSearcher) -> PlacesSearcherConnection { + let connection = PlacesSearcherConnection(interactor: self, searcher: searcher) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Hits/HitsInteractor+SingleIndexSearcher.swift b/Sources/InstantSearchCore/Hits/HitsInteractor+SingleIndexSearcher.swift new file mode 100644 index 00000000..e5e062c3 --- /dev/null +++ b/Sources/InstantSearchCore/Hits/HitsInteractor+SingleIndexSearcher.swift @@ -0,0 +1,63 @@ +// +// HitsInteractor+Connectors.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension HitsInteractor { + + struct SingleIndexSearcherConnection: Connection { + + public let interactor: HitsInteractor + public let searcher: SingleIndexSearcher + + public func connect() { + + interactor.pageLoader = searcher + + searcher.onResults.subscribePast(with: interactor) { interactor, searchResults in + interactor.update(searchResults) + } + + searcher.onError.subscribe(with: interactor) { interactor, arg in + let (query, error) = arg + interactor.process(error, for: query) + } + + searcher.onIndexChanged.subscribePast(with: interactor) { interactor, _ in + interactor.notifyQueryChanged() + } + + searcher.onQueryChanged.subscribePast(with: interactor) { interactor, _ in + interactor.notifyQueryChanged() + } + + } + + public func disconnect() { + if interactor.pageLoader === searcher { + interactor.pageLoader = nil + } + searcher.onResults.cancelSubscription(for: interactor) + searcher.onError.cancelSubscription(for: interactor) + searcher.onIndexChanged.cancelSubscription(for: interactor) + searcher.onQueryChanged.cancelSubscription(for: interactor) + } + + } + +} + +public extension HitsInteractor { + + @discardableResult func connectSearcher(_ searcher: SingleIndexSearcher) -> SingleIndexSearcherConnection { + let connection = SingleIndexSearcherConnection(interactor: self, searcher: searcher) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Hits/HitsInteractor.swift b/Sources/InstantSearchCore/Hits/HitsInteractor.swift new file mode 100644 index 00000000..0144b25d --- /dev/null +++ b/Sources/InstantSearchCore/Hits/HitsInteractor.swift @@ -0,0 +1,228 @@ +// +// HitsInteractor.swift +// InstantSearch +// +// Created by Guy Daher on 15/02/2019. +// + +import Foundation +import AlgoliaSearchClient + +public class HitsInteractor: AnyHitsInteractor { + + public typealias Result = HitsExtractable & SearchStatsConvertible + + public let settings: Settings + + internal let paginator: Paginator + private var isLastQueryEmpty: Bool = true + private let infiniteScrollingController: InfiniteScrollable + private let mutationQueue: OperationQueue + + public let onRequestChanged: Observer + public let onResultsUpdated: Observer + public let onError: Observer + + public var pageLoader: PageLoadable? { + + get { + return infiniteScrollingController.pageLoader + } + + set { + infiniteScrollingController.pageLoader = newValue + } + + } + + convenience public init(infiniteScrolling: InfiniteScrolling = Constants.Defaults.infiniteScrolling, + showItemsOnEmptyQuery: Bool = Constants.Defaults.showItemsOnEmptyQuery) { + let settings = Settings(infiniteScrolling: infiniteScrolling, + showItemsOnEmptyQuery: showItemsOnEmptyQuery) + self.init(settings: settings) + } + + public convenience init(settings: Settings? = nil) { + self.init(settings: settings, + paginationController: Paginator(), + infiniteScrollingController: InfiniteScrollingController()) + } + + internal init(settings: Settings? = nil, + paginationController: Paginator, + infiniteScrollingController: InfiniteScrollable) { + self.settings = settings ?? Settings() + self.paginator = paginationController + self.infiniteScrollingController = infiniteScrollingController + self.onRequestChanged = .init() + self.onResultsUpdated = .init() + self.onError = .init() + self.mutationQueue = .init() + self.mutationQueue.maxConcurrentOperationCount = 1 + self.mutationQueue.qualityOfService = .userInitiated + } + + public func numberOfHits() -> Int { + guard let hitsPageMap = paginator.pageMap else { return 0 } + + if isLastQueryEmpty && !settings.showItemsOnEmptyQuery { + return 0 + } else { + return hitsPageMap.count + } + } + + public func hit(atIndex index: Int) -> Record? { + guard let hitsPageMap = paginator.pageMap else { return nil } + notifyForInfiniteScrolling(rowNumber: index) + return hitsPageMap[index] + } + + public func rawHitAtIndex(_ row: Int) -> [String: Any]? { + guard let hit = hit(atIndex: row) else { return nil } + return toRaw(hit) + } + + public func genericHitAtIndex(_ index: Int) throws -> R? { + guard let hit = hit(atIndex: index) else { return .none } + return try cast(hit) + } + + public func getCurrentHits() -> [Record] { + guard let pageMap = paginator.pageMap else { return [] } + return pageMap.loadedPages.flatMap { $0.items } + } + + public func getCurrentGenericHits() throws -> [R] where R: Decodable { + guard let pageMap = paginator.pageMap else { return [] } + return try pageMap.loadedPages.flatMap { $0.items }.map(cast) + } + + public func getCurrentRawHits() -> [[String: Any]] { + guard let pageMap = paginator.pageMap else { return [] } + return pageMap.loadedPages.flatMap { $0.items }.compactMap(toRaw) + } + +} + +extension HitsInteractor { + + public enum Error: Swift.Error, LocalizedError { + case incompatibleRecordType + + var localizedDescription: String { + return "Unexpected record type: \(String(describing: Record.self))" + } + + } + +} + +private extension HitsInteractor { + + func notifyForInfiniteScrolling(rowNumber: Int) { + guard + case .on(let pageLoadOffset) = settings.infiniteScrolling, + let hitsPageMap = paginator.pageMap else { return } + + infiniteScrollingController.calculatePagesAndLoad(currentRow: rowNumber, offset: pageLoadOffset, pageMap: hitsPageMap) + } + + func toRaw(_ hit: Record) -> [String: Any]? { + guard let json = try? JSON(hit) else { return nil } + return [String: Any](json) + } + + func cast(_ hit: Record) throws -> R { + if let castedHit = hit as? R { + return castedHit + } else { + throw Error.incompatibleRecordType + } + } + +} + +public extension HitsInteractor where Record == JSON { + + func rawHitForRow(_ row: Int) -> [String: Any]? { + return hit(atIndex: row).flatMap([String: Any].init) + } + +} + +extension HitsInteractor { + + public struct Settings { + + public var infiniteScrolling: InfiniteScrolling + public var showItemsOnEmptyQuery: Bool + + public init(infiniteScrolling: InfiniteScrolling = Constants.Defaults.infiniteScrolling, + showItemsOnEmptyQuery: Bool = Constants.Defaults.showItemsOnEmptyQuery) { + self.infiniteScrolling = infiniteScrolling + self.showItemsOnEmptyQuery = showItemsOnEmptyQuery + } + + } + +} + +public enum InfiniteScrolling { + case on(withOffset: Int) + case off +} + +extension HitsInteractor: ResultUpdatable { + + @discardableResult public func update(_ searchResults: Result) -> Operation { + let stats = searchResults.searchStats + let updateOperation = BlockOperation { [weak self] in + guard let hitsInteractor = self else { return } + if case .on = hitsInteractor.settings.infiniteScrolling { + hitsInteractor.infiniteScrollingController.notifyPending(pageIndex: stats.page) + hitsInteractor.infiniteScrollingController.lastPageIndex = stats.pagesCount - 1 + } + hitsInteractor.isLastQueryEmpty = stats.query.isNilOrEmpty + + do { + let page: HitsPage = try HitsPage(searchResults: searchResults) + hitsInteractor.paginator.process(page) + hitsInteractor.onResultsUpdated.fire(searchResults) + } catch let error { + Logger.HitsDecoding.failure(hitsInteractor: hitsInteractor, error: error) + hitsInteractor.onError.fire(error) + } + } + + mutationQueue.addOperation(updateOperation) + + return updateOperation + + } + + public func notifyQueryChanged() { + + mutationQueue.cancelAllOperations() + + let queryChangedCompletion = { [weak self] in + guard let hitsInteractor = self else { return } + if case .on = hitsInteractor.settings.infiniteScrolling { + hitsInteractor.infiniteScrollingController.notifyPendingAll() + } + + hitsInteractor.paginator.invalidate() + hitsInteractor.onRequestChanged.fire(()) + } + + mutationQueue.addOperation(queryChangedCompletion) + + } + + public func process(_ error: Swift.Error, for query: Query) { + if let pendingPage = query.page { + infiniteScrollingController.notifyPending(pageIndex: Int(pendingPage)) + } + } + +} diff --git a/Sources/InstantSearchCore/Hits/HitsSource.swift b/Sources/InstantSearchCore/Hits/HitsSource.swift new file mode 100644 index 00000000..d76e7515 --- /dev/null +++ b/Sources/InstantSearchCore/Hits/HitsSource.swift @@ -0,0 +1,20 @@ +// +// HitsSource.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 23/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol HitsSource: class { + + associatedtype Record: Codable + + func numberOfHits() -> Int + func hit(atIndex index: Int) -> Record? + +} + +extension HitsInteractor: HitsSource {} diff --git a/Sources/InstantSearchCore/Hits/MultiSourceReloadNotifier.swift b/Sources/InstantSearchCore/Hits/MultiSourceReloadNotifier.swift new file mode 100644 index 00000000..17c8664a --- /dev/null +++ b/Sources/InstantSearchCore/Hits/MultiSourceReloadNotifier.swift @@ -0,0 +1,74 @@ +// +// MultiSourceReloadNotifier.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class MultiSourceReloadNotifier { + + public let target: Reloadable + + private var resultsUpdatableWrappers: [ResultsUpdatableNotificationWrapper] = [] + + public init(target: Reloadable) { + self.target = target + self.resultsUpdatableWrappers = [] + } + + public func register(_ updatable: Updatable) { + let wrapper = ResultsUpdatableNotificationWrapper(updatable: updatable) + wrapper.connect() + resultsUpdatableWrappers.append(wrapper) + } + + public func notifyReload() { + let group = DispatchGroup() + for updatable in resultsUpdatableWrappers { + group.enter() + updatable.onResultsUpdated.subscribeOnce(with: self) { (_, _) in + group.leave() + } + } + group.notify(queue: .main) { [weak self] in + self?.target.reload() + } + } + +} + +private class ResultsUpdatableNotificationWrapper: Connection { + + public var onResultsUpdated: Observer + + private var subscribe: () -> Void + private var unsubscribe: () -> Void + + public init(updatable: Updatable) { + onResultsUpdated = .init() + subscribe = {} + unsubscribe = {} + subscribe = { [weak self] in + guard let wrapper = self else { return } + updatable.onResultsUpdated.subscribe(with: wrapper) { (wrapper, _) in + wrapper.onResultsUpdated.fire(()) + } + } + unsubscribe = { [weak self] in + guard let wrapper = self else { return } + updatable.onResultsUpdated.cancelSubscription(for: wrapper) + } + } + + public func connect() { + subscribe() + } + + public func disconnect() { + unsubscribe() + } + +} diff --git a/Sources/InstantSearchCore/Hits/Related Items/MatchingPattern.swift b/Sources/InstantSearchCore/Hits/Related Items/MatchingPattern.swift new file mode 100644 index 00000000..127a213e --- /dev/null +++ b/Sources/InstantSearchCore/Hits/Related Items/MatchingPattern.swift @@ -0,0 +1,32 @@ +// +// MatchingPattern.swift +// InstantSearchCore +// +// Created by test test on 23/04/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation + +public struct MatchingPattern { + let attribute: Attribute + let score: Int + let oneOrManyElementsInKeyPath: OneOrManyElementsInKeyPath + + public init(attribute: Attribute, score: Int, filterPath: KeyPath) { + self.attribute = attribute + self.score = score + self.oneOrManyElementsInKeyPath = .one(filterPath) + } + + public init(attribute: Attribute, score: Int, filterPath: KeyPath) { + self.attribute = attribute + self.score = score + self.oneOrManyElementsInKeyPath = .many(filterPath) + } + + enum OneOrManyElementsInKeyPath { + case one(KeyPath) + case many(KeyPath) + } +} diff --git a/Sources/InstantSearchCore/Hits/Related Items/RelatedItemsInteractor+SingleIndexSearcher.swift b/Sources/InstantSearchCore/Hits/Related Items/RelatedItemsInteractor+SingleIndexSearcher.swift new file mode 100644 index 00000000..096dc1bb --- /dev/null +++ b/Sources/InstantSearchCore/Hits/Related Items/RelatedItemsInteractor+SingleIndexSearcher.swift @@ -0,0 +1,45 @@ +// +// RelatedItemsInteractor+SingleIndexSearcher.swift +// InstantSearchCore +// +// Created by test test on 23/04/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation + +extension HitsInteractor { + + @discardableResult public func connectSearcher(_ searcher: SingleIndexSearcher, withRelatedItemsTo hit: ObjectWrapper, with matchingPatterns: [MatchingPattern]) -> SingleIndexSearcherConnection { + let connection = SingleIndexSearcherConnection(interactor: self, searcher: searcher) + connection.connect() + + let legacyFilters = generateOptionalFilters(from: matchingPatterns, and: hit) + + searcher.indexQueryState.query.sumOrFiltersScores = true + + searcher.indexQueryState.query.facetFilters = [.and("objectID:-\(hit.objectID)")] + searcher.indexQueryState.query.optionalFilters = legacyFilters + + return connection + } + + func generateOptionalFilters(from matchingPatterns: [MatchingPattern], and hit: ObjectWrapper) -> FiltersStorage? { + let filterState = FilterState() + + for matchingPattern in matchingPatterns { + switch matchingPattern.oneOrManyElementsInKeyPath { + case .one(let keyPath): // // in the case of a single facet associated to a filter -> AND Behaviours + let facetValue = hit.object[keyPath: keyPath] + let facetFilter = Filter.Facet.init(attribute: matchingPattern.attribute, value: .string(facetValue), score: matchingPattern.score) + filterState[and: matchingPattern.attribute.rawValue].add(facetFilter) + case .many(let keyPath): // in the case of multiple facets associated to a filter -> OR Behaviours + let facetFilters = hit.object[keyPath: keyPath].map { Filter.Facet.init(attribute: matchingPattern.attribute, value: .string($0), score: matchingPattern.score) } + filterState[or: matchingPattern.attribute.rawValue].addAll(facetFilters) + } + } + + return FilterGroupConverter().legacy(filterState.toFilterGroups()) + } + +} diff --git a/Sources/InstantSearchCore/InfiniteScrolling/InfiniteScrollable.swift b/Sources/InstantSearchCore/InfiniteScrolling/InfiniteScrollable.swift new file mode 100644 index 00000000..ea0484b2 --- /dev/null +++ b/Sources/InstantSearchCore/InfiniteScrolling/InfiniteScrollable.swift @@ -0,0 +1,20 @@ +// +// InfiniteScrollable.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 07/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +protocol InfiniteScrollable: class { + + var lastPageIndex: Int? { get set } + var pageLoader: PageLoadable? { get set } + + func calculatePagesAndLoad(currentRow: Int, offset: Int, pageMap: PageMap) + func notifyPending(pageIndex: Int) + func notifyPendingAll() + +} diff --git a/Sources/InstantSearchCore/InfiniteScrolling/InfiniteScrollingController.swift b/Sources/InstantSearchCore/InfiniteScrolling/InfiniteScrollingController.swift new file mode 100644 index 00000000..e5e2f196 --- /dev/null +++ b/Sources/InstantSearchCore/InfiniteScrolling/InfiniteScrollingController.swift @@ -0,0 +1,103 @@ +// +// InfiniteScrollingController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 05/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +class InfiniteScrollingController: InfiniteScrollable { + + public var lastPageIndex: Int? + public weak var pageLoader: PageLoadable? + private let pendingPageIndexes: SynchronizedSet + + public init() { + pendingPageIndexes = SynchronizedSet() + } + + public func notifyPending(pageIndex: Int) { + pendingPageIndexes.remove(pageIndex) + } + + public func notifyPendingAll() { + pendingPageIndexes.removeAll() + } + + internal func isLoadedOrPending(pageIndex: PageMap.PageIndex, pageMap: PageMap) -> Bool { + let pageIsLoaded = pageMap.containsPage(atIndex: pageIndex) + let pageIsPending = pendingPageIndexes.contains(pageIndex) + return pageIsLoaded || pageIsPending + } + + private func pagesToLoad(in range: [Int], from pageMap: PageMap) -> Set { + let pagesContainingRequiredRows = Set(range.map(pageMap.pageIndex(for:))) + let pagesToLoad = pagesContainingRequiredRows.filter { !isLoadedOrPending(pageIndex: $0, pageMap: pageMap) } + return pagesToLoad + } + + func calculatePagesAndLoad(currentRow: Int, offset: Int, pageMap: PageMap) { + + guard let pageLoader = pageLoader else { + assertionFailure("Missing Page Loader") + return + } + + let previousPagesToLoad = computePreviousPagesToLoad(currentRow: currentRow, offset: offset, pageMap: pageMap) + + let nextPagesToLoad = computeNextPagesToLoad(currentRow: currentRow, offset: offset, pageMap: pageMap) + + let pagesToLoad = previousPagesToLoad.union(nextPagesToLoad) + + for pageIndex in pagesToLoad { + pendingPageIndexes.insert(pageIndex) + pageLoader.loadPage(atIndex: pageIndex) + } + + } + + func computePreviousPagesToLoad(currentRow: Int, offset: Int, pageMap: PageMap) -> Set.PageIndex> { + + let computedLowerBoundRow = currentRow - offset + let lowerBoundRow: Int + + if computedLowerBoundRow < pageMap.startIndex { + lowerBoundRow = pageMap.startIndex + } else { + lowerBoundRow = computedLowerBoundRow + } + + let requiredRows = Array(lowerBoundRow..(currentRow: Int, offset: Int, pageMap: PageMap) -> Set.PageIndex> { + + let computedUpperBoundRow = currentRow + offset + + let upperBoundRow: Int + + if let lastPageIndex = lastPageIndex { + let lastPageSize = pageMap.page(atIndex: lastPageIndex)?.items.count ?? pageMap.pageSize + let totalPagesButLastCount = lastPageIndex + let lastRowIndex = (totalPagesButLastCount * pageMap.pageSize + lastPageSize) - 1 + upperBoundRow = computedUpperBoundRow > lastRowIndex ? lastRowIndex : computedUpperBoundRow + } else { + upperBoundRow = computedUpperBoundRow + } + + let nextRow = currentRow + 1 + + guard nextRow <= upperBoundRow else { + return [] + } + + let requiredRows = Array(nextRow...upperBoundRow) + return pagesToLoad(in: requiredRows, from: pageMap) + + } + +} diff --git a/Sources/InstantSearchCore/Loading/LoadingConnector.swift b/Sources/InstantSearchCore/Loading/LoadingConnector.swift new file mode 100644 index 00000000..919246d9 --- /dev/null +++ b/Sources/InstantSearchCore/Loading/LoadingConnector.swift @@ -0,0 +1,32 @@ +// +// LoadingConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class LoadingConnector: Connection { + + public let searcher: S + public let interactor: LoadingInteractor + public let searcherConnection: Connection + + public init(searcher: S, + interactor: LoadingInteractor) { + self.searcher = searcher + self.interactor = interactor + self.searcherConnection = interactor.connectSearcher(searcher) + } + + public func connect() { + searcherConnection.connect() + } + + public func disconnect() { + searcherConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/Loading/LoadingController.swift b/Sources/InstantSearchCore/Loading/LoadingController.swift new file mode 100644 index 00000000..70f63989 --- /dev/null +++ b/Sources/InstantSearchCore/Loading/LoadingController.swift @@ -0,0 +1,11 @@ +// +// LoadingController.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol LoadingController: ItemController where Item == Bool {} diff --git a/Sources/InstantSearchCore/Loading/LoadingInteractor+Controller.swift b/Sources/InstantSearchCore/Loading/LoadingInteractor+Controller.swift new file mode 100644 index 00000000..bc86c498 --- /dev/null +++ b/Sources/InstantSearchCore/Loading/LoadingInteractor+Controller.swift @@ -0,0 +1,17 @@ +// +// LoadingInteractor+Controller.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension LoadingInteractor { + + @discardableResult func connectController(_ controller: Controller) -> ControllerConnection { + return connectController(controller) { $0 } + } + +} diff --git a/Sources/InstantSearchCore/Loading/LoadingInteractor+Searcher.swift b/Sources/InstantSearchCore/Loading/LoadingInteractor+Searcher.swift new file mode 100644 index 00000000..a252abab --- /dev/null +++ b/Sources/InstantSearchCore/Loading/LoadingInteractor+Searcher.swift @@ -0,0 +1,40 @@ +// +// LoadingInteractor+Searcher.swift +// InstantSearchCore +// +// Created by Guy Daher on 10/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension LoadingInteractor { + + struct SearcherConnection: Connection { + + public let interactor: LoadingInteractor + public let searcher: S + + public func connect() { + searcher.isLoading.subscribePast(with: interactor) { interactor, isLoading in + interactor.item = isLoading + } + } + + public func disconnect() { + searcher.isLoading.cancelSubscription(for: interactor) + } + + } + +} + +public extension LoadingInteractor { + + @discardableResult func connectSearcher(_ searcher: S) -> SearcherConnection { + let connection = SearcherConnection(interactor: self, searcher: searcher) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Loading/LoadingInteractor.swift b/Sources/InstantSearchCore/Loading/LoadingInteractor.swift new file mode 100644 index 00000000..42b2f018 --- /dev/null +++ b/Sources/InstantSearchCore/Loading/LoadingInteractor.swift @@ -0,0 +1,15 @@ +// +// LoadingInteractor.swift +// InstantSearchCore +// +// Created by Guy Daher on 10/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class LoadingInteractor: ItemInteractor { + public init() { + super.init(item: false) + } +} diff --git a/Sources/InstantSearchCore/Logging/DecodingErrorPrettyPrinter.swift b/Sources/InstantSearchCore/Logging/DecodingErrorPrettyPrinter.swift new file mode 100644 index 00000000..8dc7b091 --- /dev/null +++ b/Sources/InstantSearchCore/Logging/DecodingErrorPrettyPrinter.swift @@ -0,0 +1,69 @@ +// +// DecodingErrorPrettyPrinter.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 12/02/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation + +struct DecodingErrorPrettyPrinter: CustomStringConvertible, CustomDebugStringConvertible { + + let decodingError: DecodingError + + init(decodingError: DecodingError) { + self.decodingError = decodingError + } + + private let prefix = "Decoding error" + + private func codingKeyDescription(_ key: CodingKey) -> String { + if let index = key.intValue { + return "[\(index)]" + } else { + return "'\(key.stringValue)'" + } + } + + private func codingPathDescription(_ path: [CodingKey]) -> String { + return path.map(codingKeyDescription).joined(separator: " -> ") + } + + private func additionalComponents(for error: DecodingError) -> [String] { + switch decodingError { + case .valueNotFound(_, let context): + return [codingPathDescription(context.codingPath), context.debugDescription] + + case .keyNotFound(let key, let context): + return [codingPathDescription(context.codingPath), "Key not found: \(codingKeyDescription(key))"] + + case .typeMismatch(let type, let context): + return [codingPathDescription(context.codingPath), "Type mismatch. Expected: \(type)"] + + case .dataCorrupted(let context): + return [codingPathDescription(context.codingPath), context.debugDescription] + + @unknown default: + return [decodingError.localizedDescription] + } + + } + + var description: String { + return ([prefix] + additionalComponents(for: decodingError)).joined(separator: ": ") + } + + var debugDescription: String { + return description + } + +} + +public extension DecodingError { + + var prettyDescription: String { + return DecodingErrorPrettyPrinter(decodingError: self).description + } + +} diff --git a/Sources/InstantSearchCore/Logging/LogLevel.swift b/Sources/InstantSearchCore/Logging/LogLevel.swift new file mode 100644 index 00000000..47dd5723 --- /dev/null +++ b/Sources/InstantSearchCore/Logging/LogLevel.swift @@ -0,0 +1,12 @@ +// +// LogLevel.swift +// +// +// Created by Vladislav Fitc on 11/06/2020. +// + +import Foundation + +public enum LogLevel { + case trace, debug, info, notice, warning, error, critical +} diff --git a/Sources/InstantSearchCore/Logging/Loggable.swift b/Sources/InstantSearchCore/Logging/Loggable.swift new file mode 100644 index 00000000..ef750536 --- /dev/null +++ b/Sources/InstantSearchCore/Logging/Loggable.swift @@ -0,0 +1,16 @@ +// +// Loggable.swift +// +// +// Created by Vladislav Fitc on 11/06/2020. +// + +import Foundation + +public protocol Loggable { + + var minSeverityLevel: LogLevel { get set } + + func log(level: LogLevel, message: String) + +} diff --git a/Sources/InstantSearchCore/Logging/Logger.swift b/Sources/InstantSearchCore/Logging/Logger.swift new file mode 100644 index 00000000..fb83836d --- /dev/null +++ b/Sources/InstantSearchCore/Logging/Logger.swift @@ -0,0 +1,59 @@ +// +// Logger.swift +// +// +// Created by Vladislav Fitc on 11/06/2020. +// + +import Foundation + +public struct Logger { + + static var loggingService: Loggable = { + var swiftLog = SwiftLog(label: "com.algolia.InstantSearchCore") + print("InstantSearchCore: Default minimal log severity level is info. Change InstantSearchCore.Logger.minLogServerityLevel value if you want to change it.") + swiftLog.logLevel = .info + return swiftLog + }() + + public static var minSeverityLevel: LogLevel { + get { + return loggingService.minSeverityLevel + } + + set { + loggingService.minSeverityLevel = newValue + } + } + + private init() {} + + static func trace(_ message: String) { + loggingService.log(level: .trace, message: message) + } + + static func debug(_ message: String) { + loggingService.log(level: .debug, message: message) + } + + static func info(_ message: String) { + loggingService.log(level: .info, message: message) + } + + static func notice(_ message: String) { + loggingService.log(level: .notice, message: message) + } + + static func warning(_ message: String) { + loggingService.log(level: .warning, message: message) + } + + static func error(_ message: String) { + loggingService.log(level: .error, message: message) + } + + static func critical(_ message: String) { + loggingService.log(level: .critical, message: message) + } + +} diff --git a/Sources/InstantSearchCore/Logging/Logging.swift b/Sources/InstantSearchCore/Logging/Logging.swift new file mode 100644 index 00000000..5fa013f8 --- /dev/null +++ b/Sources/InstantSearchCore/Logging/Logging.swift @@ -0,0 +1,54 @@ +// +// Logging.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 11/02/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation + +extension Logger { + + static func error(prefix: String = "", _ error: Error) { + let errorMessage: String + if let decodingError = error as? DecodingError { + errorMessage = decodingError.prettyDescription + } else { + errorMessage = "\(error)" + } + self.error("\(prefix) \(errorMessage)") + } + +} + +extension Logger { + + enum HitsDecoding { + + static func failure(hitsInteractor: AnyHitsInteractor, error: Error) { + Logger.error(prefix: "\(hitsInteractor): ", error) + } + + } + +} + +extension Logger { + + enum Results { + + static func failure(searcher: Searcher, indexName: IndexName, _ error: Error) { + Logger.error(prefix: "\(searcher): error - index: \(indexName.rawValue)", error) + } + + static func success(searcher: Searcher, indexName: IndexName, results: SearchStatsConvertible) { + let stats = results.searchStats + let query = stats.query ?? "" + let message = "\(searcher): received results - index: \(indexName.rawValue) query: \"\(query)\" hits count: \(stats.totalHitsCount) in \(stats.processingTimeMS)ms" + Logger.info(message) + } + + } + +} diff --git a/Sources/InstantSearchCore/Logging/SwiftLog+Loggable.swift b/Sources/InstantSearchCore/Logging/SwiftLog+Loggable.swift new file mode 100644 index 00000000..cb35a931 --- /dev/null +++ b/Sources/InstantSearchCore/Logging/SwiftLog+Loggable.swift @@ -0,0 +1,56 @@ +// +// SwiftLog+Loggable.swift +// +// +// Created by Vladislav Fitc on 11/06/2020. +// + +import Foundation +import Logging + +typealias SwiftLog = Logging.Logger + +extension SwiftLog: Loggable { + + public var minSeverityLevel: LogLevel { + get { + return LogLevel(swiftLogLevel: logLevel) + } + set { + self.logLevel = newValue.swiftLogLevel + } + } + + public func log(level: LogLevel, message: String) { + self.log(level: level.swiftLogLevel, SwiftLog.Message(stringLiteral: message), metadata: .none) + } + +} + +extension LogLevel { + + init(swiftLogLevel: SwiftLog.Level) { + switch swiftLogLevel { + case .trace: self = .trace + case .debug: self = .debug + case .info: self = .info + case .notice: self = .notice + case .warning: self = .warning + case .error: self = .error + case .critical: self = .critical + } + } + + var swiftLogLevel: SwiftLog.Level { + switch self { + case .trace: return .trace + case .debug: return .debug + case .info: return .info + case .notice: return .notice + case .warning: return .warning + case .error: return .error + case .critical: return .critical + } + } + +} diff --git a/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsConnector.swift b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsConnector.swift new file mode 100644 index 00000000..173bf3a4 --- /dev/null +++ b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsConnector.swift @@ -0,0 +1,97 @@ +// +// MultiIndexHitsConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class MultiIndexHitsConnector: Connection { + + public let searcher: MultiIndexSearcher + public let interactor: MultiIndexHitsInteractor + public let filterStates: [FilterState?] + public let filterStatesConnections: [Connection] + public let searcherConnection: Connection + + public init(searcher: MultiIndexSearcher, + interactor: MultiIndexHitsInteractor, + filterStates: [FilterState?]) { + self.searcher = searcher + self.interactor = interactor + self.filterStates = filterStates + self.searcherConnection = interactor.connectSearcher(searcher) + self.filterStatesConnections = zip(interactor.hitsInteractors, filterStates).compactMap { arg in + let (interactor, filterState) = arg + return filterState.flatMap(interactor.connectFilterState) + } + } + + public func connect() { + searcherConnection.connect() + filterStatesConnections.forEach { $0.connect() } + } + + public func disconnect() { + searcherConnection.disconnect() + filterStatesConnections.forEach { $0.disconnect() } + } + +} + +public extension MultiIndexHitsConnector { + + struct IndexModule { + + public let indexName: IndexName + public let hitsInteractor: AnyHitsInteractor + public let filterState: FilterState? + + public init(indexName: IndexName, + hitsInteractor: HitsInteractor, + filterState: FilterState? = .none) { + self.indexName = indexName + self.hitsInteractor = hitsInteractor + self.filterState = filterState + } + + public init(indexName: IndexName, + infiniteScrolling: InfiniteScrolling = .on(withOffset: 10), + showItemsOnEmptyQuery: Bool = true, + filterState: FilterState? = .none) { + let hitsInteractor = HitsInteractor(infiniteScrolling: infiniteScrolling, + showItemsOnEmptyQuery: showItemsOnEmptyQuery) + self.init(indexName: indexName, + hitsInteractor: hitsInteractor, + filterState: filterState) + } + + } + + convenience init(appID: ApplicationID, + apiKey: APIKey, + indexModules: [IndexModule]) { + let searcher = MultiIndexSearcher(appID: appID, + apiKey: apiKey, + indexNames: indexModules.map { $0.indexName }) + let interactor = MultiIndexHitsInteractor(hitsInteractors: indexModules.map { $0.hitsInteractor }) + self.init(searcher: searcher, + interactor: interactor, + filterStates: indexModules.map { $0.filterState }) + } + +} + +public extension MultiIndexHitsConnector.IndexModule { + + init(suggestionsIndexName: IndexName, + hitsInteractor: HitsInteractor> = .init(infiniteScrolling: .off, showItemsOnEmptyQuery: true), + filterState: FilterState? = .none) { + self.init(indexName: suggestionsIndexName, + hitsInteractor: hitsInteractor, + filterState: filterState) + } + +} diff --git a/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsController.swift b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsController.swift new file mode 100644 index 00000000..d2d73c49 --- /dev/null +++ b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsController.swift @@ -0,0 +1,19 @@ +// +// MultiIndexHitsController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 23/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol MultiIndexHitsController: class, Reloadable { + + var hitsSource: MultiIndexHitsSource? { get set } + + func scrollToTop() + +} + +extension MultiIndexHitsInteractor: MultiIndexHitsSource {} diff --git a/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor+Controller.swift b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor+Controller.swift new file mode 100644 index 00000000..94921698 --- /dev/null +++ b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor+Controller.swift @@ -0,0 +1,52 @@ +// +// MultiIndexHitsInteractor+Controller.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 25/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension MultiIndexHitsInteractor { + + struct ControllerConnection: Connection { + + public let interactor: MultiIndexHitsInteractor + public let controller: Controller + + public func connect() { + controller.hitsSource = interactor + + interactor.onRequestChanged.subscribe(with: controller) { controller, _ in + controller.scrollToTop() + }.onQueue(.main) + + interactor.onResultsUpdated.subscribePast(with: controller) { controller, _ in + controller.reload() + }.onQueue(.main) + + controller.reload() + } + + public func disconnect() { + if controller.hitsSource === interactor { + controller.hitsSource = nil + } + interactor.onRequestChanged.cancelSubscription(for: controller) + interactor.onResultsUpdated.cancelSubscription(for: controller) + } + + } + +} + +public extension MultiIndexHitsInteractor { + + @discardableResult func connectController(_ controller: Controller) -> ControllerConnection { + let connection = ControllerConnection(interactor: self, controller: controller) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor+FilterState.swift b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor+FilterState.swift new file mode 100644 index 00000000..68d3aefd --- /dev/null +++ b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor+FilterState.swift @@ -0,0 +1,40 @@ +// +// MultiIndexHitsInteractor+FilterState.swift +// InstantSearchCore +// +// Created by Guy Daher on 18/09/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension MultiIndexHitsInteractor { + + struct FilterStateConnection: Connection { + + public let interactor: MultiIndexHitsInteractor + public let filterState: FilterState + + public func connect() { + filterState.onChange.subscribePast(with: interactor) { interactor, _ in + interactor.notifyQueryChanged() + } + } + + public func disconnect() { + filterState.onChange.cancelSubscription(for: interactor) + } + + } + +} + +public extension MultiIndexHitsInteractor { + + @discardableResult func connectFilterState(_ filterState: FilterState) -> FilterStateConnection { + let connection = FilterStateConnection(interactor: self, filterState: filterState) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor+Searcher.swift b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor+Searcher.swift new file mode 100644 index 00000000..64943859 --- /dev/null +++ b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor+Searcher.swift @@ -0,0 +1,59 @@ +// +// MultiIndexHitsInteractor+Searcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 07/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension MultiIndexHitsInteractor { + + struct SearcherConnection: Connection { + + public let interactor: MultiIndexHitsInteractor + public let searcher: MultiIndexSearcher + + public func connect() { + zip(interactor.hitsInteractors.indices, searcher.pageLoaders).forEach { + let (index, pageLoader) = $0 + interactor.hitsInteractors[index].pageLoader = pageLoader + } + + searcher.onResults.subscribePast(with: interactor) { interactor, searchResults in + interactor.update(searchResults.results) + } + + searcher.onError.subscribe(with: interactor) { interactor, args in + let (queries, error) = args + interactor.process(error, for: queries) + } + + searcher.onQueryChanged.subscribe(with: interactor) { interactor, _ in + interactor.notifyQueryChanged() + } + } + + public func disconnect() { + for nestedInteractor in interactor.hitsInteractors { + nestedInteractor.pageLoader = nil + } + searcher.onResults.cancelSubscription(for: interactor) + searcher.onError.cancelSubscription(for: interactor) + searcher.onQueryChanged.cancelSubscription(for: interactor) + } + + } + +} + +public extension MultiIndexHitsInteractor { + + @discardableResult func connectSearcher(_ searcher: MultiIndexSearcher) -> SearcherConnection { + let connection = SearcherConnection(interactor: self, searcher: searcher) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor.swift b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor.swift new file mode 100644 index 00000000..4fc3c310 --- /dev/null +++ b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsInteractor.swift @@ -0,0 +1,185 @@ +// +// MultiIndexHitsInteractor.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 15/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +/** + Interactor which constitutes the aggregation of nested hits interactors providing a convenient functions for managing them. + Designed for a joint usage with multi index searcher, but can be used with multiple separate single index searchers as well. + */ + +public class MultiIndexHitsInteractor { + + public let onRequestChanged: Observer + public let onResultsUpdated: Observer<[SearchResponse]> + public let onError: Observer + + private let mutationQueue: OperationQueue + + /// List of nested hits interactors + + let hitsInteractors: [AnyHitsInteractor] + + /// Common initializer + + public init(hitsInteractors: [AnyHitsInteractor]) { + self.hitsInteractors = hitsInteractors + self.onRequestChanged = .init() + self.onResultsUpdated = .init() + self.onError = .init() + self.mutationQueue = .init() + self.mutationQueue.maxConcurrentOperationCount = 1 + self.mutationQueue.qualityOfService = .userInitiated + for interactor in hitsInteractors { + interactor.onError.subscribe(with: self) { multIndexInteractor, error in + Logger.HitsDecoding.failure(hitsInteractor: interactor, error: error) + multIndexInteractor.onError.fire(error) + } + } + } + + /// Returns the index of provided hits interactor. + /// - Parameter hitsInteractor: the interactor to search for + /// - Returns: The index of desired interactor. If no there is no such interactor, returns `nil` + + public func section(of hitsInteractor: HitsInteractor) -> Int? { + return hitsInteractors.firstIndex { ($0 as? HitsInteractor) === hitsInteractor } + } + + /// Returns boolean value indicating if desired hitsInteractor is nested in current multi hits hitsInteractor + /// - Parameter hitsInteractor: the interactor to check + + public func contains(_ hitsInteractor: HitsInteractor) -> Bool { + return section(of: hitsInteractor) != nil + } + + /// Returns a hits interactor at specified index + /// - Parameter section: the section index of nested hits interactor + /// - Throws: HitsInteractor.Error.incompatibleRecordType if the derived record type mismatches the record type of corresponding hits interactor + /// - Returns: The nested interactor at specified index. + + public func hitsInteractor(forSection section: Int) throws -> HitsInteractor { + guard let typedInteractor = hitsInteractors[section] as? HitsInteractor else { + throw HitsInteractor.Error.incompatibleRecordType + } + + return typedInteractor + } + + /// Returns the hit of a desired type + /// - Parameter index: the index of a hit in a nested hits Interactor + /// - Parameter section: the index of a nested hits Interactor + /// - Throws: HitsInteractor.Error.incompatibleRecordType if desired type of record doesn't match with record type of corresponding hits Interactor + /// - Returns: The hit at row for index path or `nil` if there is no element at index in a specified section + + public func hit(atIndex index: Int, inSection section: Int) throws -> R? { + return try hitsInteractors[section].genericHitAtIndex(index) + } + + /// Returns the hit in raw dictionary form + /// - Parameter index: the index of a hit in a nested hits Interactor + /// - Parameter section: the index of a nested hits Interactor + /// - Returns: The hit in raw dictionary form or `nil` if there is no element at index in a specified section + + public func rawHit(atIndex index: Int, inSection section: Int) -> [String: Any]? { + return hitsInteractors[section].rawHitAtIndex(index) + } + + /// Returns number of nested hits Interactors + + public func numberOfSections() -> Int { + return hitsInteractors.count + } + + /// Returns number rows in the nested hits Interactor at section + /// - Parameter section: the index of nested hits Interactor + public func numberOfHits(inSection section: Int) -> Int { + return hitsInteractors[section].numberOfHits() + } + +} + +extension MultiIndexHitsInteractor { + + /// Updates the results of a nested hits Interactor at specified index + /// - Parameter results: list of typed search results. + /// - Parameter section: the section index of nested hits Interactor + /// - Throws: HitsInteractor.Error.incompatibleRecordType if the record type of results mismatches the record type of corresponding hits Interactor + + public func update(_ results: SearchResponse, forInteractorInSection section: Int) { + + let completion = BlockOperation { [weak self] in + self?.onResultsUpdated.fire([results]) + } + + completion.addDependency(hitsInteractors[section].update(results)) + + mutationQueue.addOperation(completion) + + } + + /// Updates the results of all nested hits Interactors. + /// Each search results element will be converted to a corresponding nested hits Interactor search results type. + /// - Parameter results: list of generic search results. Order of results must match the order of nested hits Interactors. + /// - Parameter metadata: the metadata of query corresponding to results + /// - Throws: HitsInteractor.Error.incompatibleRecordType if the conversion of search results for one of a nested hits Interactors is impossible due to a record type mismatch + + public func update(_ results: [SearchResponse]) { + + let completion = BlockOperation { [weak self] in + self?.onResultsUpdated.fire(results) + } + + zip(hitsInteractors, results).map { arg in + let (interactor, results) = arg + return interactor.update(results) + }.forEach(completion.addDependency) + + mutationQueue.addOperation(completion) + + } + + public func process(_ error: Error, for queries: [Query]) { + zip(hitsInteractors, queries).forEach { (hitsInteractor, query) in + hitsInteractor.process(error, for: query) + } + } + + public func notifyQueryChanged() { + hitsInteractors.forEach { + $0.notifyQueryChanged() + } + onRequestChanged.fire(()) + } + +} + +#if os(iOS) || os(tvOS) +import UIKit + +public extension MultiIndexHitsInteractor { + + /// Returns the hit of a desired type + /// - Parameter indexPath: the pointer to a hit, where section points to a nested hits Interactor, and item defines the index of a hit in a Interactor + /// - Throws: HitsInteractor.Error.incompatibleRecordType if desired type of record doesn't match with record type of corresponding hits Interactor + /// - Returns: The hit at row for index path or `nil` if there is no element at index in a specified section + + func hit(at indexPath: IndexPath) throws -> R? { + return try hit(atIndex: indexPath.item, inSection: indexPath.section) + } + + /// Returns the hit in raw dictionary form + /// - Parameter indexPath: the pointer to a hit, where section points to a nested hits Interactor, and item defines the index of a hit in a Interactor + /// - Returns: The hit in raw dictionary form or `nil` if there is no element at index in a specified section + + func rawHit(at indexPath: IndexPath) -> [String: Any]? { + return rawHit(atIndex: indexPath.item, inSection: indexPath.section) + } + +} +#endif diff --git a/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsSource.swift b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsSource.swift new file mode 100644 index 00000000..c514d36e --- /dev/null +++ b/Sources/InstantSearchCore/MultiIndexHits/MultiIndexHitsSource.swift @@ -0,0 +1,17 @@ +// +// MultiIndexHitsSource.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 23/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol MultiIndexHitsSource: class { + + func numberOfSections() -> Int + func numberOfHits(inSection section: Int) -> Int + func hit(atIndex index: Int, inSection section: Int) throws -> R? + +} diff --git a/Sources/InstantSearchCore/Number/Computation.swift b/Sources/InstantSearchCore/Number/Computation.swift new file mode 100644 index 00000000..0f9dee7d --- /dev/null +++ b/Sources/InstantSearchCore/Number/Computation.swift @@ -0,0 +1,39 @@ +// +// Computation.swift +// InstantSearchCore +// +// Created by Guy Daher on 04/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class Computation { + var numeric: N? { + didSet { + onNumericUpdate(numeric) + } + } + var onNumericUpdate: ((N?) -> Void) + + public init(numeric: N?, onNumericUpdate: @escaping ((N?) -> Void)) { + self.numeric = numeric + self.onNumericUpdate = onNumericUpdate + } + + public func increment(step: N = 1, default: N = 0) { + self.numeric = (self.numeric != nil) ? self.numeric! + step : `default` + } + + public func decrement(step: N = 1, default: N = 0) { + self.numeric = (self.numeric != nil) ? self.numeric! - step : `default` + } + + public func multiply(step: N = 1, default: N = 0) { + self.numeric = (self.numeric != nil) ? self.numeric! * step : `default` + } + + public func just(value: N?) { + self.numeric = value + } +} diff --git a/Sources/InstantSearchCore/Number/FilterComparison+Controller.swift b/Sources/InstantSearchCore/Number/FilterComparison+Controller.swift new file mode 100644 index 00000000..2c7f7102 --- /dev/null +++ b/Sources/InstantSearchCore/Number/FilterComparison+Controller.swift @@ -0,0 +1,47 @@ +// +// FilterComparisonConnectView.swift +// InstantSearchCore +// +// Created by Guy Daher on 04/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public struct NumberInteractorControllerConnection: Connection where Controller.Item == Number { + + public let interactor: NumberInteractor + public let controller: Controller + + public func connect() { + let computation = Computation(numeric: interactor.item) { [weak interactor] numeric in + interactor?.computeNumber(number: numeric) + } + + controller.setComputation(computation: computation) + + interactor.onItemChanged.subscribePast(with: controller) { controller, item in + guard let item = item else { + controller.invalidate() + return + } + controller.setItem(item) + }.onQueue(.main) + } + + public func disconnect() { + controller.invalidate() + interactor.onItemChanged.cancelSubscription(for: controller) + } + +} + +public extension NumberInteractor { + + @discardableResult func connectNumberController(_ controller: Controller) -> NumberInteractorControllerConnection where Controller.Item == Number { + let connection = NumberInteractorControllerConnection(interactor: self, controller: controller) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Number/FilterComparison+FilterState.swift b/Sources/InstantSearchCore/Number/FilterComparison+FilterState.swift new file mode 100644 index 00000000..e877cdb6 --- /dev/null +++ b/Sources/InstantSearchCore/Number/FilterComparison+FilterState.swift @@ -0,0 +1,130 @@ +// +// FilterComparisonConnectFilterState.swift +// InstantSearchCore +// +// Created by Guy Daher on 04/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension NumberInteractor { + + struct FilterStateConnection: Connection { + + public let interactor: NumberInteractor + public let filterState: FilterState + public let attribute: Attribute + public let numericOperator: Filter.Numeric.Operator + public let `operator`: RefinementOperator + public let groupName: String? + + public init(interactor: NumberInteractor, + filterState: FilterState, + attribute: Attribute, + numericOperator: Filter.Numeric.Operator, + `operator`: RefinementOperator = .and, + groupName: String? = nil) { + self.interactor = interactor + self.filterState = filterState + self.attribute = attribute + self.numericOperator = numericOperator + self.operator = `operator` + self.groupName = groupName + } + + public func connect() { + + let groupName = self.groupName ?? attribute.rawValue + + switch `operator` { + case .and: + connectFilterState(filterState, to: interactor, attribute: attribute, numericOperator: numericOperator, via: SpecializedAndGroupAccessor(filterState[and: groupName])) + case .or: + connectFilterState(filterState, to: interactor, attribute: attribute, numericOperator: numericOperator, via: filterState[or: groupName] ) + } + + } + + public func disconnect() { + filterState.onChange.cancelSubscription(for: interactor) + interactor.onNumberComputed.cancelSubscription(for: filterState) + } + + private func connectFilterState(_ filterState: FilterState, + to interactor: NumberInteractor, + attribute: Attribute, + numericOperator: Filter.Numeric.Operator, + via accessor: Accessor) where Accessor.Filter == Filter.Numeric { + whenFilterStateChangedUpdateExpression(interactor: interactor, filterState: filterState, attribute: attribute, numericOperator: numericOperator, accessor: accessor) + whenExpressionComputedUpdateFilterState(interactor: interactor, filterState: filterState, attribute: attribute, numericOperator: numericOperator, accessor: accessor) + } + + private func whenFilterStateChangedUpdateExpression(interactor: NumberInteractor, + filterState: FilterState, + attribute: Attribute, + numericOperator: Filter.Numeric.Operator, + accessor: Accessor) where Accessor.Filter == Filter.Numeric { + + func extractValue(from numericFilter: Filter.Numeric) -> Number? { + if case .comparison(numericOperator, let value) = numericFilter.value { + return Number(value) + } else { + return nil + } + } + + filterState.onChange.subscribePast(with: interactor) { interactor, _ in + interactor.item = accessor.filters(for: attribute).compactMap(extractValue).first + } + + } + + private func whenExpressionComputedUpdateFilterState(interactor: NumberInteractor, + filterState: FilterState, + attribute: Attribute, + numericOperator: Filter.Numeric.Operator, + accessor: P) where P.Filter == Filter.Numeric { + + let removeCurrentItem = { [weak interactor] in + guard let item = interactor?.item else { return } + let filter = Filter.Numeric(attribute: attribute, operator: numericOperator, value: item.toDouble()) + accessor.remove(filter) + } + + let addItem: (Number?) -> Void = { value in + guard let value = value else { return } + let filter = Filter.Numeric(attribute: attribute, operator: numericOperator, value: value.toDouble()) + accessor.add(filter) + } + + interactor.onNumberComputed.subscribePast(with: filterState) { filterState, computed in + removeCurrentItem() + addItem(computed) + filterState.notifyChange() + } + + } + + } + +} + +public extension NumberInteractor { + + @discardableResult func connectFilterState(_ filterState: FilterState, + attribute: Attribute, + numericOperator: Filter.Numeric.Operator, + operator: RefinementOperator = .and, + groupName: String? = nil) -> FilterStateConnection { + let connection = FilterStateConnection(interactor: self, + filterState: filterState, + attribute: attribute, + numericOperator: numericOperator, + operator: `operator`, + groupName: groupName) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Number/FilterComparisonComputeBounds.swift b/Sources/InstantSearchCore/Number/FilterComparisonComputeBounds.swift new file mode 100644 index 00000000..7846f612 --- /dev/null +++ b/Sources/InstantSearchCore/Number/FilterComparisonComputeBounds.swift @@ -0,0 +1,47 @@ +// +// FilterComparisonComputeBounds.swift +// InstantSearchCore +// +// Created by Guy Daher on 04/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +public struct BoundableSingleIndexSearcherConnection: Connection { + + public let boundable: B + public let searcher: SingleIndexSearcher + public let attribute: Attribute + + public func connect() { + let attribute = self.attribute + searcher.indexQueryState.query.updateQueryFacets(with: attribute) + searcher.onResults.subscribePastOnce(with: boundable) { boundable, searchResults in + boundable.computeBoundsFromFacetStats(attribute: attribute, facetStats: searchResults.facetStats) + } + } + + public func disconnect() { + searcher.onResults.cancelSubscription(for: searcher) + } + +} + +extension Boundable { + + @discardableResult public func connectSearcher(_ searcher: SingleIndexSearcher, attribute: Attribute) -> BoundableSingleIndexSearcherConnection { + let connection = BoundableSingleIndexSearcherConnection(boundable: self, searcher: searcher, attribute: attribute) + connection.connect() + return connection + } + + func computeBoundsFromFacetStats(attribute: Attribute, facetStats: [Attribute: FacetStats]?) { + guard let facetStats = facetStats, let facetStatsOfAttribute = facetStats[attribute] else { + applyBounds(bounds: nil) + return + } + + applyBounds(bounds: Number(facetStatsOfAttribute.min)...Number(facetStatsOfAttribute.max)) + } +} diff --git a/Sources/InstantSearchCore/Number/FilterComparisonConnector.swift b/Sources/InstantSearchCore/Number/FilterComparisonConnector.swift new file mode 100644 index 00000000..4e354ea9 --- /dev/null +++ b/Sources/InstantSearchCore/Number/FilterComparisonConnector.swift @@ -0,0 +1,51 @@ +// +// FilterComparisonConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class FilterComparisonConnector: Connection { + + public let interactor: NumberInteractor + public let filterState: FilterState + public let attribute: Attribute + public let numericOperator: Filter.Numeric.Operator + public let `operator`: RefinementOperator + public let groupName: String + + public let filterStateConnection: Connection + + public init(interactor: NumberInteractor, + filterState: FilterState, + attribute: Attribute, + numericOperator: Filter.Numeric.Operator, + number: Number, + operator: RefinementOperator, + groupName: String? = nil) { + self.interactor = interactor + self.filterState = filterState + self.attribute = attribute + self.numericOperator = numericOperator + self.operator = `operator` + self.groupName = groupName ?? attribute.rawValue + self.filterStateConnection = interactor.connectFilterState(filterState, + attribute: attribute, + numericOperator: numericOperator, + operator: `operator`, + groupName: groupName) + self.interactor.computeNumber(number: number) + } + + public func connect() { + filterStateConnection.connect() + } + + public func disconnect() { + filterStateConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/Number/NumberController.swift b/Sources/InstantSearchCore/Number/NumberController.swift new file mode 100644 index 00000000..077a936d --- /dev/null +++ b/Sources/InstantSearchCore/Number/NumberController.swift @@ -0,0 +1,15 @@ +// +// NumberView.swift +// InstantSearchCore +// +// Created by Guy Daher on 04/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol NumberController: ItemController where Item: Numeric { + + func setComputation(computation: Computation) + +} diff --git a/Sources/InstantSearchCore/Number/NumberInteractor.swift b/Sources/InstantSearchCore/Number/NumberInteractor.swift new file mode 100644 index 00000000..94f7a62b --- /dev/null +++ b/Sources/InstantSearchCore/Number/NumberInteractor.swift @@ -0,0 +1,50 @@ +// +// NumberInteractor.swift +// InstantSearchCore +// +// Created by Guy Daher on 04/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class NumberInteractor: ItemInteractor, Boundable { + + public let onNumberComputed: Observer + public let onBoundsComputed: Observer?> + + public private(set) var bounds: ClosedRange? + + public convenience init() { + self.init(item: nil) + } + + public override init(item: Number?) { + self.onNumberComputed = .init() + self.onBoundsComputed = .init() + super.init(item: item) + } + + public func applyBounds(bounds: ClosedRange?) { + let coerced = item?.coerce(in: bounds) + self.bounds = bounds + + onBoundsComputed.fire(bounds) + onNumberComputed.fire(coerced) + } + + public func computeNumber(number: Number?) { + let coerced = number?.coerce(in: bounds) + + onNumberComputed.fire(coerced) + } +} + +extension Comparable { + func coerce(in range: ClosedRange?) -> Self { + guard let range = range else { return self } + if self < range.lowerBound { return range.lowerBound } + if self > range.upperBound { return range.upperBound } + return self + } +} diff --git a/Sources/InstantSearchCore/NumberRange/NumberRangeConnector.swift b/Sources/InstantSearchCore/NumberRange/NumberRangeConnector.swift new file mode 100644 index 00000000..ef81188f --- /dev/null +++ b/Sources/InstantSearchCore/NumberRange/NumberRangeConnector.swift @@ -0,0 +1,41 @@ +// +// NumberRangeConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class NumberRangeConnector: Connection { + + public let filterState: FilterState + public let attribute: Attribute + public let filterStateConnection: Connection + + public init(filterState: FilterState, + attribute: Attribute, + operator: RefinementOperator, + groupName: String? = nil, + bounds: ClosedRange, + range: ClosedRange) { + self.filterState = filterState + self.attribute = attribute + let interactor = NumberRangeInteractor(item: range) + interactor.applyBounds(bounds: bounds) + self.filterStateConnection = interactor.connectFilterState(filterState, + attribute: attribute, + operator: `operator`, + groupName: groupName) + } + + public func connect() { + filterStateConnection.connect() + } + + public func disconnect() { + filterStateConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/NumberRange/NumberRangeController.swift b/Sources/InstantSearchCore/NumberRange/NumberRangeController.swift new file mode 100644 index 00000000..67a8b172 --- /dev/null +++ b/Sources/InstantSearchCore/NumberRange/NumberRangeController.swift @@ -0,0 +1,16 @@ +// +// NumberRangeController.swift +// InstantSearchCore +// +// Created by Guy Daher on 14/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol NumberRangeController: ItemController where Item == ClosedRange { + associatedtype Number: Comparable + + var onRangeChanged: ((ClosedRange) -> Void)? { get set } + func setBounds(_ bounds: ClosedRange) +} diff --git a/Sources/InstantSearchCore/NumberRange/NumberRangeInteractor+Controller.swift b/Sources/InstantSearchCore/NumberRange/NumberRangeInteractor+Controller.swift new file mode 100644 index 00000000..5ba0e39f --- /dev/null +++ b/Sources/InstantSearchCore/NumberRange/NumberRangeInteractor+Controller.swift @@ -0,0 +1,53 @@ +// +// NumberRangeInteractor+Controller.swift +// InstantSearchCore +// +// Created by Guy Daher on 14/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension NumberRange { + + struct ControllerConnection: Connection where Controller.Number == Number { + public let interactor: NumberRangeInteractor + public let controller: Controller + + public func connect() { + interactor.onItemChanged.subscribePast(with: controller) { controller, item in + guard let item = item else { + controller.invalidate() + return + } + controller.setItem(item) + }.onQueue(.main) + + controller.onRangeChanged = { [weak interactor] closedRange in + interactor?.computeNumberRange(numberRange: closedRange) + } + + interactor.onBoundsComputed.subscribePast(with: controller) { controller, bounds in + bounds.flatMap(controller.setBounds) + }.onQueue(.main) + } + + public func disconnect() { + interactor.onItemChanged.cancelSubscription(for: controller) + controller.onRangeChanged = nil + interactor.onBoundsComputed.cancelSubscription(for: controller) + } + + } + +} + +public extension NumberRangeInteractor { + + @discardableResult func connectController(_ controller: Controller) -> NumberRange.ControllerConnection { + let connection = NumberRange.ControllerConnection(interactor: self, controller: controller) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/NumberRange/NumberRangeInteractor+FilterState.swift b/Sources/InstantSearchCore/NumberRange/NumberRangeInteractor+FilterState.swift new file mode 100644 index 00000000..ecf80559 --- /dev/null +++ b/Sources/InstantSearchCore/NumberRange/NumberRangeInteractor+FilterState.swift @@ -0,0 +1,121 @@ +// +// NumberRangeInteractor+FilterState.swift +// InstantSearchCore +// +// Created by Guy Daher on 14/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension NumberRange { + + struct FilterStateConnection: Connection { + + public let interactor: NumberRangeInteractor + public let filterState: FilterState + public let attribute: Attribute + public let `operator`: RefinementOperator + public let groupName: String? + + public init(interactor: NumberRangeInteractor, + filterState: FilterState, + attribute: Attribute, + `operator`: RefinementOperator = .and, + groupName: String? = nil) { + self.interactor = interactor + self.filterState = filterState + self.attribute = attribute + self.operator = `operator` + self.groupName = groupName + } + + public func connect() { + + let groupName = self.groupName ?? attribute.rawValue + + switch `operator` { + case .and: + connect(interactor, with: filterState, attribute: attribute, via: SpecializedAndGroupAccessor(filterState[and: groupName])) + case .or: + connect(interactor, with: filterState, attribute: attribute, via: filterState[or: groupName]) + } + } + + public func disconnect() { + filterState.onChange.cancelSubscription(for: interactor) + interactor.onNumberRangeComputed.cancelSubscription(for: filterState) + } + + private func connect(_ interactor: NumberRangeInteractor, + with: FilterState, + attribute: Attribute, + via accessor: Accessor) where Accessor.Filter == Filter.Numeric { + whenFilterStateChangedUpdateRange(interactor: interactor, filterState: with, attribute: attribute, accessor: accessor) + whenRangeComputedUpdateFilterState(interactor: interactor, filterState: with, attribute: attribute, accessor: accessor) + } + + private func whenFilterStateChangedUpdateRange(interactor: NumberRangeInteractor, + filterState: FilterState, + attribute: Attribute, + accessor: Accessor) where Accessor.Filter == Filter.Numeric { + + func extractRange(from numericFilter: Filter.Numeric) -> ClosedRange? { + switch numericFilter.value { + case .range(let closedRange): + return Number(closedRange.lowerBound)...Number(closedRange.upperBound) + case .comparison: + return nil + } + } + + filterState.onChange.subscribePast(with: interactor) { interactor, _ in + interactor.item = accessor.filters(for: attribute).compactMap(extractRange).first + } + + } + + private func whenRangeComputedUpdateFilterState(interactor: NumberRangeInteractor, + filterState: FilterState, + attribute: Attribute, + accessor: Accessor) where Accessor.Filter == Filter.Numeric { + + func numericFilter(with range: ClosedRange) -> Filter.Numeric { + let castedRange: ClosedRange = range.lowerBound.toDouble()...range.upperBound.toDouble() + return .init(attribute: attribute, range: castedRange) + } + + let removeCurrentItem = { [weak interactor] in + guard let item = interactor?.item else { return } + accessor.remove(numericFilter(with: item)) + } + + let addItem: (ClosedRange?) -> Void = { range in + guard let range = range else { return } + accessor.add(numericFilter(with: range)) + } + + interactor.onNumberRangeComputed.subscribePast(with: filterState) { filterState, computedRange in + removeCurrentItem() + addItem(computedRange) + filterState.notifyChange() + } + + } + + } + +} + +public extension NumberRangeInteractor { + + @discardableResult func connectFilterState(_ filterState: FilterState, + attribute: Attribute, + operator: RefinementOperator = .and, + groupName: String? = nil) -> NumberRange.FilterStateConnection { + let connection = NumberRange.FilterStateConnection(interactor: self, filterState: filterState, attribute: attribute, operator: `operator`, groupName: groupName) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/NumberRange/NumberRangeInteractor.swift b/Sources/InstantSearchCore/NumberRange/NumberRangeInteractor.swift new file mode 100644 index 00000000..ca5563fe --- /dev/null +++ b/Sources/InstantSearchCore/NumberRange/NumberRangeInteractor.swift @@ -0,0 +1,56 @@ +// +// NumberRangeInteractor.swift +// InstantSearchCore +// +// Created by Guy Daher on 14/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public enum NumberRange {} + +public class NumberRangeInteractor: ItemInteractor?>, Boundable { + + public let onNumberRangeComputed: Observer?> + // TODO: Need to move that info at the view/controller level. + public let onBoundsComputed: Observer?> + + public private(set) var bounds: ClosedRange? + + public convenience init() { + self.init(item: nil) + } + + public override init(item: ClosedRange?) { + self.onNumberRangeComputed = .init() + self.onBoundsComputed = .init() + super.init(item: item) + } + + public func applyBounds(bounds: ClosedRange?) { + let coerced: ClosedRange? + if let bounds = bounds { + coerced = item?.clamped(to: bounds) + } else { + coerced = item + } + + self.bounds = bounds + + onBoundsComputed.fire(bounds) + onNumberRangeComputed.fire(coerced) + + } + + public func computeNumberRange(numberRange: ClosedRange?) { + let coerced: ClosedRange? + if let bounds = bounds { + coerced = numberRange?.clamped(to: bounds) + } else { + coerced = numberRange + } + + onNumberRangeComputed.fire(coerced) + } +} diff --git a/Sources/InstantSearchCore/Observations/Observable.swift b/Sources/InstantSearchCore/Observations/Observable.swift new file mode 100644 index 00000000..e07f9a61 --- /dev/null +++ b/Sources/InstantSearchCore/Observations/Observable.swift @@ -0,0 +1,66 @@ +// +// Observable.swift +// InstantSearchCore +// +// Created by Guy Daher on 21/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol Observable { + + associatedtype ParameterType + associatedtype Obs: Observation + + typealias ObserverCallback = (O, ParameterType) -> Void + + /// Whether or not the `Signal` should retain a reference to the last data it was fired with. Defaults to false. + var retainLastData: Bool { get set } + + /// The last data that the `Signal` was fired with. In order for the `Signal` to retain the last fired data, its + /// `retainLastFired`-property needs to be set to true + var lastDataFired: ParameterType? { get } + + /// Subscribes an observer to the `Signal`. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke whenever the `Signal` fires. + /// - returns: A `SignalSubscription` that can be used to cancel or filter the subscription. + func subscribe(with observer: O, callback: @escaping ObserverCallback) -> Obs + + /// Subscribes an observer to the `Signal`. The subscription is automatically canceled after the `Signal` has + /// fired once. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke when the signal fires for the first time. + func subscribeOnce(with observer: O, callback: @escaping ObserverCallback) -> Obs + + /// Subscribes an observer to the `Signal` and invokes its callback immediately with the last data fired by the + /// `Signal` if it has fired at least once and if the `retainLastData` property has been set to true. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke whenever the `Signal` fires. + func subscribePast(with observer: O, callback: @escaping ObserverCallback) -> Obs + + /// Subscribes an observer to the `Signal` and invokes its callback immediately with the last data fired by the + /// `Signal` if it has fired at least once and if the `retainLastData` property has been set to true. If it has + /// not been fired yet, it will continue listening until it fires for the first time. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke whenever the signal fires. + func subscribePastOnce(with observer: O, callback: @escaping ObserverCallback) -> Obs + + /// Cancels all subscriptions for an observer. + /// + /// - parameter observer: The observer whose subscriptions to cancel + func cancelSubscription(for observer: AnyObject) + + /// Cancels all subscriptions for the `Signal`. + func cancelAllSubscriptions() + +} diff --git a/Sources/InstantSearchCore/Observations/Observation.swift b/Sources/InstantSearchCore/Observations/Observation.swift new file mode 100644 index 00000000..1fd25976 --- /dev/null +++ b/Sources/InstantSearchCore/Observations/Observation.swift @@ -0,0 +1,50 @@ +// +// Observation.swift +// InstantSearchCore +// +// Created by Guy Daher on 21/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +/// A SignalLister represenents an instance and its association with a `Signal`. +public protocol Observation { + + associatedtype ParameterType + + typealias ObserverFilter = (ParameterType) -> Bool + + /// Whether the observer should be removed once it observes the `Signal` firing once. Defaults to false. + var once: Bool { get set } + + /// Assigns a filter to the `SignalSubscription`. This lets you define conditions under which a observer should actually + /// receive the firing of a `Singal`. The closure that is passed an argument can decide whether the firing of a + /// `Signal` should actually be dispatched to its observer depending on the data fired. + /// + /// If the closeure returns true, the observer is informed of the fire. The default implementation always + /// returns `true`. + /// + /// - parameter predicate: A closure that can decide whether the `Signal` fire should be dispatched to its observer. + /// - returns: Returns self so you can chain calls. + func filter(_ predicate: @escaping ObserverFilter) -> Self + + /// Tells the observer to sample received `Signal` data and only dispatch the latest data once the time interval + /// has elapsed. This is useful if the subscriber wants to throttle the amount of data it receives from the + /// `Signal`. + /// + /// - parameter sampleInterval: The number of seconds to delay dispatch. + /// - returns: Returns self so you can chain calls. + func sample(every sampleInterval: TimeInterval) -> Self + + /// Assigns a dispatch queue to the `SignalSubscription`. The queue is used for scheduling the observer calls. If not + /// nil, the callback is fired asynchronously on the specified queue. Otherwise, the block is run synchronously + /// on the posting thread, which is its default behaviour. + /// + /// - parameter queue: A queue for performing the observer's calls. + /// - returns: Returns self so you can chain calls. + func onQueue(_ queue: DispatchQueue) -> Self + + /// Cancels the observer. This will cancelSubscription the listening object from the `Signal`. + func cancel() +} diff --git a/Sources/InstantSearchCore/Observations/Observer.swift b/Sources/InstantSearchCore/Observations/Observer.swift new file mode 100644 index 00000000..86efcfd0 --- /dev/null +++ b/Sources/InstantSearchCore/Observations/Observer.swift @@ -0,0 +1,112 @@ +// +// Observer.swift +// InstantSearchCore +// +// Created by Guy Daher on 21/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +/// This is where you can change the implementation detail of the observer +/// If you want to change a new one, then you need just implement the Observable protocol +/// and then swap it here. Note that you will also have to implement a Subscription logic +/// that will conform to the Observation protocol (See Signals+Observable for more info) +public class Observer

: Observable { + + public typealias ParameterType = P + public typealias Obs = Subscription

+ + private let signal: Signal

+ + public init(retainLastData: Bool = true) { + self.signal = Signal

() + self.retainLastData = retainLastData + } + + /// Whether or not the `Signal` should retain a reference to the last data it was fired with. Defaults to false. + public var retainLastData: Bool { + get { + return signal.retainLastData + } + set { + signal.retainLastData = newValue + } + } + + /// The last data that the `Signal` was fired with. In order for the `Signal` to retain the last fired data, its + /// `retainLastFired`-property needs to be set to true + public var lastDataFired: ParameterType? { + return signal.lastDataFired + } + + /// Subscribes an observer to the `Signal`. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke whenever the `Signal` fires. + /// - returns: A `SignalSubscription` that can be used to cancel or filter the subscription. + @discardableResult public func subscribe(with observer: O, callback: @escaping ObserverCallback) -> Subscription

{ + return Subscription(signalSubscription: signal.subscribe(with: observer, callback: callback)) + } + + /// Subscribes an observer to the `Signal`. The subscription is automatically canceled after the `Signal` has + /// fired once. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke when the signal fires for the first time. + @discardableResult public func subscribeOnce(with observer: O, callback: @escaping ObserverCallback) -> Subscription

{ + return Subscription(signalSubscription: signal.subscribeOnce(with: observer, callback: callback)) + } + + /// Subscribes an observer to the `Signal` and invokes its callback immediately with the last data fired by the + /// `Signal` if it has fired at least once and if the `retainLastData` property has been set to true. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke whenever the `Signal` fires. + @discardableResult public func subscribePast(with observer: O, callback: @escaping ObserverCallback) -> Subscription

{ + return Subscription(signalSubscription: signal.subscribePast(with: observer, callback: callback)) + } + + /// Subscribes an observer to the `Signal` and invokes its callback immediately with the last data fired by the + /// `Signal` if it has fired at least once and if the `retainLastData` property has been set to true. If it has + /// not been fired yet, it will continue listening until it fires for the first time. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke whenever the signal fires. + @discardableResult public func subscribePastOnce(with observer: O, callback: @escaping ObserverCallback) -> Subscription

{ + return Subscription(signalSubscription: signal.subscribePastOnce(with: observer, callback: callback)) + } + + /// Cancels all subscriptions for an observer. + /// + /// - parameter observer: The observer whose subscriptions to cancel + public func cancelSubscription(for observer: AnyObject) { + signal.cancelSubscription(for: observer) + } + + /// Cancels all subscriptions for the `Signal`. + public func cancelAllSubscriptions() { + signal.cancelAllSubscriptions() + } + +} + +public extension Observer { + + func fire(_ data: P) { + signal.fire(data) + } + + func clearLastData() { + signal.clearLastData() + } + + var observers: [AnyObject] { + return signal.observers + } + +} diff --git a/Sources/InstantSearchCore/Observations/Signals+Observable.swift b/Sources/InstantSearchCore/Observations/Signals+Observable.swift new file mode 100644 index 00000000..738bd31f --- /dev/null +++ b/Sources/InstantSearchCore/Observations/Signals+Observable.swift @@ -0,0 +1,15 @@ +// +// Signals+Observable.swift +// InstantSearchCore +// +// Created by Guy Daher on 21/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +extension Signal: Observable { + public typealias ParameterType = T +} + +extension SignalSubscription: Observation {} diff --git a/Sources/InstantSearchCore/Observations/Subscription.swift b/Sources/InstantSearchCore/Observations/Subscription.swift new file mode 100644 index 00000000..d26d5bd9 --- /dev/null +++ b/Sources/InstantSearchCore/Observations/Subscription.swift @@ -0,0 +1,77 @@ +// +// Subscription.swift +// InstantSearchCore +// +// Created by Guy Daher on 21/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +/// A SignalLister represenents an instance and its association with a `Signal`. +final public class Subscription: Observation { + + public typealias ParameterType = T + + public typealias SignalCallback = (T) -> Void + + public typealias SignalFilter = (T) -> Bool + + private let signalSubscription: SignalSubscription + + init(signalSubscription: SignalSubscription) { + self.signalSubscription = signalSubscription + } + + weak public var observer: AnyObject? { + return signalSubscription.observer + } + + /// Whether the observer should be removed once it observes the `Signal` firing once. Defaults to false. + public var once: Bool { + get { + return signalSubscription.once + } + set { + signalSubscription.once = newValue + } + } + + /// Assigns a filter to the `SignalSubscription`. This lets you define conditions under which a observer should actually + /// receive the firing of a `Singal`. The closure that is passed an argument can decide whether the firing of a + /// `Signal` should actually be dispatched to its observer depending on the data fired. + /// + /// If the closeure returns true, the observer is informed of the fire. The default implementation always + /// returns `true`. + /// + /// - parameter predicate: A closure that can decide whether the `Signal` fire should be dispatched to its observer. + /// - returns: Returns self so you can chain calls. + @discardableResult public func filter(_ predicate: @escaping Subscription.SignalFilter) -> Subscription { + return .init(signalSubscription: signalSubscription.filter(predicate)) + } + + /// Tells the observer to sample received `Signal` data and only dispatch the latest data once the time interval + /// has elapsed. This is useful if the subscriber wants to throttle the amount of data it receives from the + /// `Signal`. + /// + /// - parameter sampleInterval: The number of seconds to delay dispatch. + /// - returns: Returns self so you can chain calls. + @discardableResult public func sample(every sampleInterval: TimeInterval) -> Subscription { + return .init(signalSubscription: signalSubscription.sample(every: sampleInterval)) + } + + /// Assigns a dispatch queue to the `SignalSubscription`. The queue is used for scheduling the observer calls. If not + /// nil, the callback is fired asynchronously on the specified queue. Otherwise, the block is run synchronously + /// on the posting thread, which is its default behaviour. + /// + /// - parameter queue: A queue for performing the observer's calls. + /// - returns: Returns self so you can chain calls. + @discardableResult public func onQueue(_ queue: DispatchQueue) -> Subscription { + return .init(signalSubscription: signalSubscription.onQueue(queue)) + } + + /// Cancels the observer. This will cancelSubscription the listening object from the `Signal`. + public func cancel() { + signalSubscription.cancel() + } +} diff --git a/Sources/InstantSearchCore/Pagination/PageLoadable.swift b/Sources/InstantSearchCore/Pagination/PageLoadable.swift new file mode 100644 index 00000000..b7775cec --- /dev/null +++ b/Sources/InstantSearchCore/Pagination/PageLoadable.swift @@ -0,0 +1,41 @@ +// +// PageLoadable.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol PageLoadable: class { + + func loadPage(atIndex pageIndex: Int) + +} + +extension SingleIndexSearcher: PageLoadable { + + public func loadPage(atIndex pageIndex: Int) { + indexQueryState.query.page = pageIndex + search() + } + +} + +extension FacetSearcher: PageLoadable { + + public func loadPage(atIndex pageIndex: Int) { + indexQueryState.query.page = pageIndex + search() + } + +} + +extension PlacesSearcher: PageLoadable { + + public func loadPage(atIndex pageIndex: Int) { + search() + } + +} diff --git a/Sources/InstantSearchCore/Pagination/PageMap.swift b/Sources/InstantSearchCore/Pagination/PageMap.swift new file mode 100644 index 00000000..55091be4 --- /dev/null +++ b/Sources/InstantSearchCore/Pagination/PageMap.swift @@ -0,0 +1,219 @@ +// +// PageMap.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 13/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +struct PageMap { + + typealias PageIndex = Int + + private var storage: [PageIndex: [Item]] + + var pageSize: Int + + var latestPageIndex: PageIndex? { + return loadedPageIndexes.max() + } + + var loadedPageIndexes: Set = [] + + var loadedPages: [Page] { + return storage + .sorted { $0.key < $1.key } + .map { Page(index: $0.key, items: $0.value) } + } + + var loadedPagesCount: Int { + return storage.count + } + + var totalPagesCount: Int { + guard let latestPageIndex = latestPageIndex else { return 0 } + return latestPageIndex + 1 + } + + mutating func insert(_ page: [Item], withIndex pageIndex: PageIndex) { + storage[pageIndex] = page + loadedPageIndexes = Set(storage.keys) + } + + func page(atIndex pageIndex: PageIndex) -> Page? { + if let items = storage[pageIndex] { + return Page(index: pageIndex, items: items) + } else { + return nil + } + } + + func inserting(_ page: [Item], withIndex pageIndex: PageIndex) -> PageMap { + var mutableCopy = self + mutableCopy.insert(page, withIndex: pageIndex) + return mutableCopy + } + + func pageIndex(for index: Index) -> PageIndex { + return index / pageSize + } + + func containsPage(atIndex pageIndex: PageIndex) -> Bool { + return storage[pageIndex] != nil + } + + func containsItem(atIndex index: Index) -> Bool { + return item(atIndex: index) != nil + } + + func item(atIndex index: Index) -> Item? { + + let pageIndex = self.pageIndex(for: index) + let offset = index % pageSize + + guard + let page = storage[pageIndex], + offset < page.count else { return nil } + + return page[offset] + } + +} + +// MARK: SequenceType +extension PageMap: Sequence { + + public func makeIterator() -> IndexingIterator { + return IndexingIterator(_elements: self) + } + +} + +// MARK: CollectionType +extension PageMap: BidirectionalCollection { + + public typealias Index = Int + + public var startIndex: Index { return 0 } + public var endIndex: Index { + guard let latestPageIndex = latestPageIndex else { return 0 } + // Here we suppose that last items page can be not full + // So there is a need to calculate a number of elements + // On last page and add it to previous pages count * page size + let fullPagesCount = totalPagesCount - 1 + let latestPageItemsCount = storage[latestPageIndex]?.count ?? 0 + return fullPagesCount * pageSize + latestPageItemsCount + } + + public func index(after index: Index) -> Index { + return index+1 + } + + public func index(before index: Index) -> Index { + return index-1 + } + + /// Accesses and sets elements for a given flat index position. + /// Currently, setter can only be used to replace non-optional values. + public subscript (position: Index) -> Item? { + get { + let pageIndex = self.pageIndex(for: position) + let inPageIndex = position % pageSize + if let page = storage[pageIndex], inPageIndex < page.count { + return page[inPageIndex] + } else { + // Return nil for all pages that haven't been set yet + return nil + } + } + + set(newValue) { + guard let newValue = newValue else { return } + + let pageIndex = self.pageIndex(for: position) + var elementPage = storage[pageIndex] + elementPage?[position % pageSize] = newValue + storage[pageIndex] = elementPage + } + } +} + +protocol Pageable { + + associatedtype Item + + var index: Int { get } + var items: [Item] { get } + +} + +extension PageMap { + + struct Page { + + let index: Int + let items: [Item] + + init(index: Int, items: [Item]) { + self.index = index + self.items = items + } + + } + +} + +extension PageMap.Page: Equatable where Item: Hashable {} +extension PageMap.Page: Hashable where Item: Hashable {} + +extension PageMap { + + init?(_ source: T) where T.Item == Item { + guard !source.items.isEmpty else { + return nil + } + storage = [source.index: source.items] + loadedPageIndexes = [source.index] + pageSize = source.items.count + } + + init?(_ items: C) where C.Element == Item { + guard !items.isEmpty else { + return nil + } + let itemsArray = Array(items) + self.storage = [0: itemsArray] + loadedPageIndexes = [0] + pageSize = itemsArray.count + } + + init?(_ dictionary: [Int: [Item]]) { + if dictionary.isEmpty { + return nil + } + + storage = dictionary + loadedPageIndexes = Set(dictionary.keys) + pageSize = dictionary.sorted(by: { $0.key < $1.key }).first?.value.count ?? 0 + } + +} + +extension PageMap { + + mutating func cleanUp(basePageIndex pageIndex: Int, keepingPagesOffset: Int) { + + let leastPageIndex = pageIndex - keepingPagesOffset + let lastPageIndex = pageIndex + keepingPagesOffset + + let pagesToRemove = loadedPageIndexes.filter { $0 < leastPageIndex || $0 > lastPageIndex } + + for pageIndex in pagesToRemove { + storage.removeValue(forKey: pageIndex) + } + + } + +} diff --git a/Sources/InstantSearchCore/Pagination/Paginator.swift b/Sources/InstantSearchCore/Pagination/Paginator.swift new file mode 100644 index 00000000..9a165833 --- /dev/null +++ b/Sources/InstantSearchCore/Pagination/Paginator.swift @@ -0,0 +1,40 @@ +// +// Pagination.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 13/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +class Paginator { + + var pageMap: PageMap? + var pageCleanUpOffset: Int? = 3 + var isInvalidated: Bool = false + + func process(_ page: IP) where IP.Item == Item { + + let updatedPageMap: PageMap? + + if let pageMap = pageMap, !isInvalidated { + updatedPageMap = pageMap.inserting(page.items, withIndex: page.index) + } else { + updatedPageMap = PageMap(page) + isInvalidated = false + } + + pageMap = updatedPageMap + + if let pageCleanUpOffset = pageCleanUpOffset { + pageMap?.cleanUp(basePageIndex: page.index, keepingPagesOffset: pageCleanUpOffset) + } + + } + + public func invalidate() { + isInvalidated = true + } + +} diff --git a/Sources/InstantSearchCore/Pagination/SearchResults+PageMap.swift b/Sources/InstantSearchCore/Pagination/SearchResults+PageMap.swift new file mode 100644 index 00000000..90f16773 --- /dev/null +++ b/Sources/InstantSearchCore/Pagination/SearchResults+PageMap.swift @@ -0,0 +1,35 @@ +// +// SearchResults+PageMap.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 13/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +struct HitsPage: Pageable { + + let index: Int + let items: [Item] + + init() { + self.index = 0 + self.items = [] + } + + init(index: Int, items: [Item]) { + self.index = index + self.items = items + } + +} + +extension HitsPage { + + init(searchResults: HitsExtractable & SearchStatsConvertible) throws { + self.index = searchResults.searchStats.page + self.items = try searchResults.extractHits() + } + +} diff --git a/Sources/InstantSearchCore/Pagination/SynchronizedSet.swift b/Sources/InstantSearchCore/Pagination/SynchronizedSet.swift new file mode 100644 index 00000000..f925ceea --- /dev/null +++ b/Sources/InstantSearchCore/Pagination/SynchronizedSet.swift @@ -0,0 +1,42 @@ +// +// PageCoordinator.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +class SynchronizedSet { + + private let queue = DispatchQueue(label: "thread_safe_access_queue", attributes: .concurrent) + private var storage: Set = [] + + func contains(_ item: T) -> Bool { + var containsItem: Bool! + queue.sync { + containsItem = self.storage.contains(item) + } + return containsItem + } + + func insert(_ item: T) { + queue.async(flags: .barrier) { + self.storage.insert(item) + } + } + + func remove(_ item: T) { + queue.async(flags: .barrier) { + self.storage.remove(item) + } + } + + func removeAll() { + queue.async(flags: .barrier) { + self.storage.removeAll() + } + } + +} diff --git a/Sources/InstantSearchCore/Presenters/DefaultPresenter.swift b/Sources/InstantSearchCore/Presenters/DefaultPresenter.swift new file mode 100644 index 00000000..28b1bf6e --- /dev/null +++ b/Sources/InstantSearchCore/Presenters/DefaultPresenter.swift @@ -0,0 +1,11 @@ +// +// DefaultPresenter.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public enum DefaultPresenter {} diff --git a/Sources/InstantSearchCore/Presenters/FacetPresenter.swift b/Sources/InstantSearchCore/Presenters/FacetPresenter.swift new file mode 100644 index 00000000..648e9bbc --- /dev/null +++ b/Sources/InstantSearchCore/Presenters/FacetPresenter.swift @@ -0,0 +1,22 @@ +// +// FacetPresenter.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias FacetPresenter = (Facet) -> String + +public extension DefaultPresenter { + + enum Facet { + + public static let present: FacetPresenter = { facet in + return "\(facet.value) \(facet.count)" + } + + } +} diff --git a/Sources/InstantSearchCore/Presenters/FilterPresenter.swift b/Sources/InstantSearchCore/Presenters/FilterPresenter.swift new file mode 100644 index 00000000..2e705ca5 --- /dev/null +++ b/Sources/InstantSearchCore/Presenters/FilterPresenter.swift @@ -0,0 +1,50 @@ +// +// FilterPresenter.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 17/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias FilterPresenter = (Filter) -> String + +public extension DefaultPresenter { + + enum Filter { + + public static let present: FilterPresenter = { filter in + let attributeName = filter.filter.attribute.rawValue + + switch filter { + case .facet(let facetFilter): + switch facetFilter.value { + case .bool: + return filter.filter.attribute.rawValue + + case .float(let floatValue): + return "\(attributeName): \(floatValue)" + + case .string(let stringValue): + return stringValue + } + + case .numeric(let numericFilter): + + switch numericFilter.value { + case .comparison(let compOperator, let value): + return "\(attributeName) \(compOperator) \(value)" + + case .range(let range): + return "\(attributeName): \(range.lowerBound) to \(range.upperBound)" + } + + case .tag(let tagFilter): + return tagFilter.value + } + } + + } + +} diff --git a/Sources/InstantSearchCore/Presenters/HierarchicalPresenter.swift b/Sources/InstantSearchCore/Presenters/HierarchicalPresenter.swift new file mode 100644 index 00000000..4ae00151 --- /dev/null +++ b/Sources/InstantSearchCore/Presenters/HierarchicalPresenter.swift @@ -0,0 +1,40 @@ +// +// HierarchicalPresenter.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias HierarchicalPresenter = ([HierarchicalFacet]) -> [HierarchicalFacet] + +public extension DefaultPresenter { + + enum Hierarchical { + + public static let present: HierarchicalPresenter = { facets in + let levels = Set(facets.map { $0.level }).sorted() + + guard !levels.isEmpty else { return facets } + + var output: [HierarchicalFacet] = [] + + output.reserveCapacity(facets.count) + + levels.forEach { level in + let facetsForLevel = facets + .filter { $0.level == level } + .sorted { $0.facet.value < $1.facet.value } + let indexToInsert = output + .lastIndex { $0.isSelected } + .flatMap { output.index(after: $0) } ?? output.endIndex + output.insert(contentsOf: facetsForLevel, at: indexToInsert) + } + + return output + } + + } +} diff --git a/Sources/InstantSearchCore/Presenters/IndexPresenter.swift b/Sources/InstantSearchCore/Presenters/IndexPresenter.swift new file mode 100644 index 00000000..5864ea65 --- /dev/null +++ b/Sources/InstantSearchCore/Presenters/IndexPresenter.swift @@ -0,0 +1,22 @@ +// +// IndexPresenter.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +public typealias IndexPresenter = (Index) -> String + +public extension DefaultPresenter { + + enum Index { + + public static let present: IndexPresenter = { index in + return index.name.rawValue + } + + } +} diff --git a/Sources/InstantSearchCore/Presenters/StatsPresenter.swift b/Sources/InstantSearchCore/Presenters/StatsPresenter.swift new file mode 100644 index 00000000..8f19cba5 --- /dev/null +++ b/Sources/InstantSearchCore/Presenters/StatsPresenter.swift @@ -0,0 +1,22 @@ +// +// StatsPresneter.swift +// InstantSearchCore +// +// Created by Guy Daher on 12/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias StatsPresenter = Presenter + +public extension DefaultPresenter { + + enum Stats { + + public static let present: StatsPresenter = { stats in + return (stats?.totalHitsCount).flatMap { "\($0) results" } + } + + } +} diff --git a/Sources/InstantSearchCore/QueryBuilder/QueryBuilder+DisjunctiveFaceting.swift b/Sources/InstantSearchCore/QueryBuilder/QueryBuilder+DisjunctiveFaceting.swift new file mode 100644 index 00000000..7d265e39 --- /dev/null +++ b/Sources/InstantSearchCore/QueryBuilder/QueryBuilder+DisjunctiveFaceting.swift @@ -0,0 +1,177 @@ +// +// QueryBuilder+DisjunctiveFaceting.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 26/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +/// Provides convenient method for building disjuncitve faceting queries and parsing disjunctive faceting + +extension QueryBuilder { + + /// Constructs dictionary of raw facets for attribute with filters and set of disjunctive faceting attributes + /// - parameter disjunctiveFacets: set of disjuncitve faceting attributes + /// - parameter filters: list of filters containing disjunctive facets + /// - returns: dictionary mapping disjunctive faceting attributes to list of raw facets + + private func facetDictionary(with disjunctiveFacets: Set, filters: [FilterType]) -> [Attribute: [String]] { + return disjunctiveFacets.map { attribute -> (Attribute, [String]) in + let values = filters + .compactMap { $0 as? Filter.Facet } + .filter { $0.attribute == attribute } + .map { $0.value.description } + return (attribute, values) + }.reduce([:]) { dict, val in + return dict.merging([val.0: val.1]) { value, _ in value } + } + } + + /// Constructs dictionary of facets for attribute with filters and set of disjunctive faceting attributes + /// Each generated facet is zero-count + /// - parameter disjunctiveFacets: set of disjuncitve faceting attributes + /// - parameter filters: list of filters containing disjunctive facets + /// - returns: dictionary mapping disjunctive faceting attributes to list of facets + + private func typedFacetDictionary(with dict: [Attribute: [String]]) -> [Attribute: [Facet]] { + return dict + .map { (attribute, facetValues) -> (Attribute, [Facet]) in + let facets = facetValues.map { Facet(value: $0, count: 0, highlighted: .none) } + return (attribute, facets) + } + .reduce([:]) { dict, arg in + return dict.merging([arg.0: arg.1]) { value, _ in value } + } + } + + /// Completes disjunctive faceting result with currently selected facets with empty results + /// - parameter results: base disjuncitve faceting results + /// - parameter facets: dictionary of current facets + /// - returns: disjuncitve faceting results enriched with selected but empty facets + + private func completeMissingFacets(in results: SearchResponse, with facets: [Attribute: [String]]) -> SearchResponse { + + var output = results + + func complete(lhs: [Facet], withFacetValues facetValues: Set) -> [Facet] { + let existingValues = lhs.map { $0.value } + return lhs + facetValues.subtracting(existingValues).map { Facet(value: $0, count: 0, highlighted: .none) } + } + + guard let currentDisjunctiveFacets = results.disjunctiveFacets else { + output.disjunctiveFacets = typedFacetDictionary(with: facets) + return output + } + + facets.forEach { attribute, values in + let facets = currentDisjunctiveFacets[attribute] ?? [] + let completedFacets = complete(lhs: facets, withFacetValues: Set(values)) + output.disjunctiveFacets?[attribute] = completedFacets + } + + return output + + } + + /// Completes disjunctive faceting result with currently selected facets with empty results + /// - parameter results: base disjuncitve faceting results + /// - parameter facets: set of attribute of facets + /// - returns: disjuncitve faceting results enriched with selected but empty facets + + func completeMissingFacets(in results: SearchResponse, disjunctiveFacets: Set, filters: [FilterType]) -> SearchResponse { + let facetDictionary = self.facetDictionary(with: disjunctiveFacets, filters: filters) + return completeMissingFacets(in: results, with: facetDictionary) + } + + /// Builds disjunctive faceting queries for each facet + /// - parameter query: source query + /// - parameter filterGroups: + /// - parameter disjunctiveFacets: attributes of disjunctive facets + /// - returns: list of "or" queries for disjunctive faceting + + func buildDisjunctiveFacetingQueries(query: Query, filterGroups: [FilterGroupType], disjunctiveFacets: Set) -> [Query] { + + return disjunctiveFacets.map { attribute in + + let groups = filterGroups.map { (group) -> FilterGroupType in + guard let disjunctiveFacetGroup = group as? FilterGroup.Or else { + return group + } + let filtersMinusDisjunctiveFacet = disjunctiveFacetGroup.typedFilters.filter { $0.attribute != attribute } + return FilterGroup.Or(filters: filtersMinusDisjunctiveFacet, name: group.name) + }.filter { !$0.filters.isEmpty } + + var query = query + query.facets = [attribute] + query.requestOnlyFacets() + query.filters = FilterGroupConverter().sql(groups) + return query + + } + + } + +} + +extension Query { + + mutating func requestOnlyFacets() { + attributesToRetrieve = [] + attributesToHighlight = [] + hitsPerPage = 0 + analytics = false + } + +} + +extension Collection { + + func partition(by predicate: (Element) -> Bool) -> (satisfying: [Element], rest: [Element]) { + var satisfying: [Element] = [] + var rest: [Element] = [] + for element in self { + if predicate(element) { + satisfying.append(element) + } else { + rest.append(element) + } + } + return (satisfying, rest) + } + +} + +extension Collection { + + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript (safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } + +} + +extension Collection { + + func anySatisfy(_ predicate: (Element) -> Bool) -> Bool { + return reduce(false) { $0 || predicate($1) } + } + +} + +extension Collection where Element == SearchResponse { + + func aggregateFacets() -> [Attribute: [Facet]] { + return compactMap { $0.facets }.reduce([:]) { (aggregatedFacets, facets) in + aggregatedFacets.merging(facets) { (_, new) in new } + } + } + + func aggregateFacetStats() -> [Attribute: FacetStats] { + return compactMap { $0.facetStats }.reduce([:]) { (aggregatedFacetStats, facetStats) in + aggregatedFacetStats.merging(facetStats) { (_, new) in new } + } + } + +} diff --git a/Sources/InstantSearchCore/QueryBuilder/QueryBuilder+HierarchicalFaceting.swift b/Sources/InstantSearchCore/QueryBuilder/QueryBuilder+HierarchicalFaceting.swift new file mode 100644 index 00000000..b0904a5b --- /dev/null +++ b/Sources/InstantSearchCore/QueryBuilder/QueryBuilder+HierarchicalFaceting.swift @@ -0,0 +1,60 @@ +// +// ComplexQueryBuilder+HierarchicalFaceting.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient + +extension QueryBuilder { + + func buildHierarchicalQueries(with query: Query, + filterGroups: [FilterGroupType], + hierarchicalAttributes: [Attribute], + hierachicalFilters: [Filter.Facet]) -> [Query] { + + // An empty hierarchical offset in the beggining is added to create + // The first request in the list returning search results + + guard !hierachicalFilters.isEmpty else { + return [] + } + + let offsetHierachicalFilters: [Filter.Facet?] = [.none] + hierachicalFilters + + let queriesForHierarchicalFacets = zip(hierarchicalAttributes, offsetHierachicalFilters) + .map { arguments -> Query in + let (attribute, hierarchicalFilter) = arguments + + var outputFilterGroups = filterGroups + + if let appliedHierachicalFacet = hierachicalFilters.last { + outputFilterGroups = outputFilterGroups.map { group in + guard let andGroup = group as? FilterGroup.And else { + return group + } + let filtersMinusHierarchicalFacet = andGroup.filters.filter { ($0 as? Filter.Facet) != appliedHierachicalFacet } + return FilterGroup.And(filters: filtersMinusHierarchicalFacet, name: andGroup.name) + } + } + + if let currentHierarchicalFilter = hierarchicalFilter { + outputFilterGroups.append(FilterGroup.And(filters: [currentHierarchicalFilter], name: "_hierarchical")) + } + + var query = query + query.requestOnlyFacets() + query.facets = [attribute] + query.filters = FilterGroupConverter().sql(outputFilterGroups) + return query + + } + + return queriesForHierarchicalFacets + + } + +} diff --git a/Sources/InstantSearchCore/QueryBuilder/QueryBuilder.swift b/Sources/InstantSearchCore/QueryBuilder/QueryBuilder.swift new file mode 100644 index 00000000..7afb079f --- /dev/null +++ b/Sources/InstantSearchCore/QueryBuilder/QueryBuilder.swift @@ -0,0 +1,123 @@ +// +// QueryBuilder.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +public struct QueryBuilder { + + public let query: Query + public let filterGroups: [FilterGroupType] + + public var keepSelectedEmptyFacets: Bool + + private let disjunctiveFacets: Set + + public let hierarchicalAttributes: [Attribute] + public let hierachicalFilters: [Filter.Facet] + + public let resultQueriesCount: Int = 1 + + public var disjunctiveFacetingQueriesCount: Int { + return disjunctiveFacets.count + } + + public var hierarchicalFacetingQueriesCount: Int { + if hierarchicalAttributes.count == hierachicalFilters.count { + return hierarchicalAttributes.count + } + return hierachicalFilters.isEmpty ? 0 : hierachicalFilters.count + 1 + } + + public var totalQueriesCount: Int { + return resultQueriesCount + disjunctiveFacetingQueriesCount + hierarchicalFacetingQueriesCount + } + + public init(query: Query, + filterGroups: [FilterGroupType] = [], + hierarchicalAttributes: [Attribute] = [], + hierachicalFilters: [Filter.Facet] = []) { + self.query = query + self.keepSelectedEmptyFacets = false + self.filterGroups = filterGroups + self.disjunctiveFacets = Set(filterGroups + .compactMap { $0 as? FilterGroup.Or } + .map { $0.filters } + .flatMap { $0 } + .map { $0.attribute }) + self.hierarchicalAttributes = hierarchicalAttributes + self.hierachicalFilters = hierachicalFilters + } + + public func build() -> [Query] { + + var queryForResults = query + queryForResults.filters = FilterGroupConverter().sql(filterGroups) + + let disjunctiveFacetingQueries = buildDisjunctiveFacetingQueries(query: query, + filterGroups: filterGroups, + disjunctiveFacets: disjunctiveFacets) + + let hierarchicalFacetingQueries = buildHierarchicalQueries(with: query, + filterGroups: filterGroups, + hierarchicalAttributes: hierarchicalAttributes, + hierachicalFilters: hierachicalFilters) + + return [queryForResults] + disjunctiveFacetingQueries + hierarchicalFacetingQueries + } + + public func aggregate(_ results: [SearchResponse]) throws -> SearchResponse { + + guard var aggregatedResult = results.first else { + throw Error.emptyResults + } + + if results.count != totalQueriesCount { + throw Error.queriesResultsCountMismatch(totalQueriesCount, results.count) + } + + let resultsForFaceting = results.dropFirst() + let resultsForDisjuncitveFaceting = resultsForFaceting[...disjunctiveFacetingQueriesCount] + let resultsForHierarchicalFaceting = resultsForFaceting.dropFirst(disjunctiveFacetingQueriesCount)[..(_ results: SearchResponse, withResultsForDisjuncitveFaceting resultsForDisjuncitveFaceting: C) -> SearchResponse where C.Element == SearchResponse { + var output = results + output.disjunctiveFacets = resultsForDisjuncitveFaceting.aggregateFacets() + output.exhaustiveFacetsCount = resultsForDisjuncitveFaceting.allSatisfy { $0.exhaustiveFacetsCount == true } + return output + } + + func update(_ results: SearchResponse, withResultsForHierarchicalFaceting resultsForHierarchicalFaceting: C) -> SearchResponse where C.Element == SearchResponse { + var output = results + let hierarchicalFacets = resultsForHierarchicalFaceting.aggregateFacets() + output.hierarchicalFacets = hierarchicalFacets.isEmpty ? nil : hierarchicalFacets + return output + } + + public enum Error: Swift.Error { + case emptyResults + case queriesResultsCountMismatch(Int, Int) + } + +} + +extension SearchResponse: Builder {} diff --git a/Sources/InstantSearchCore/QueryInput/QueryInputConnector.swift b/Sources/InstantSearchCore/QueryInput/QueryInputConnector.swift new file mode 100644 index 00000000..7b5b7414 --- /dev/null +++ b/Sources/InstantSearchCore/QueryInput/QueryInputConnector.swift @@ -0,0 +1,36 @@ +// +// QueryInputConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class QueryInputConnector: Connection { + + public let searcher: S + public let interactor: QueryInputInteractor + public let searchTriggeringMode: SearchTriggeringMode + + public let searcherConnection: Connection + + public init(searcher: S, + interactor: QueryInputInteractor = .init(), + searchTriggeringMode: SearchTriggeringMode = .searchAsYouType) { + self.searcher = searcher + self.interactor = interactor + self.searchTriggeringMode = searchTriggeringMode + self.searcherConnection = interactor.connectSearcher(searcher) + } + + public func connect() { + searcherConnection.connect() + } + + public func disconnect() { + searcherConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/QueryInput/QueryInputController.swift b/Sources/InstantSearchCore/QueryInput/QueryInputController.swift new file mode 100644 index 00000000..afa62874 --- /dev/null +++ b/Sources/InstantSearchCore/QueryInput/QueryInputController.swift @@ -0,0 +1,15 @@ +// +// QueryInputController.swift +// InstantSearchCore +// +// Created by Guy Daher on 22/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol QueryInputController: class { + var onQueryChanged: ((String?) -> Void)? { get set } + var onQuerySubmitted: ((String?) -> Void)? { get set } + func setQuery(_ query: String?) +} diff --git a/Sources/InstantSearchCore/QueryInput/QueryInputInteractor+Controller.swift b/Sources/InstantSearchCore/QueryInput/QueryInputInteractor+Controller.swift new file mode 100644 index 00000000..97e9fb57 --- /dev/null +++ b/Sources/InstantSearchCore/QueryInput/QueryInputInteractor+Controller.swift @@ -0,0 +1,53 @@ +// +// QueryInputInteractor+Controller.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension QueryInputInteractor { + + struct ControllerConnection: Connection { + + public let interactor: QueryInputInteractor + public let controller: Controller + + public func connect() { + + interactor.onQueryChanged.subscribePast(with: controller) { controller, query in + controller.setQuery(query) + }.onQueue(.main) + + controller.onQueryChanged = { [weak interactor] in + interactor?.query = $0 + } + + controller.onQuerySubmitted = { [weak interactor] in + interactor?.query = $0 + interactor?.submitQuery() + } + + } + + public func disconnect() { + interactor.onQueryChanged.cancelSubscription(for: controller) + controller.onQueryChanged = nil + controller.onQuerySubmitted = nil + } + + } + +} + +public extension QueryInputInteractor { + + @discardableResult func connectController(_ controller: Controller) -> ControllerConnection { + let connection = ControllerConnection(interactor: self, controller: controller) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/QueryInput/QueryInputInteractor+Searcher.swift b/Sources/InstantSearchCore/QueryInput/QueryInputInteractor+Searcher.swift new file mode 100644 index 00000000..5acf0c6e --- /dev/null +++ b/Sources/InstantSearchCore/QueryInput/QueryInputInteractor+Searcher.swift @@ -0,0 +1,78 @@ +// +// QueryInputInteractor+Searcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public enum SearchTriggeringMode { + case searchAsYouType + case searchOnSubmit +} + +public extension QueryInputInteractor { + + struct SearcherConnection: Connection { + + public let interactor: QueryInputInteractor + public let searcher: S + public let searchTriggeringMode: SearchTriggeringMode + + public init(interactor: QueryInputInteractor, + searcher: S, + searchTriggeringMode: SearchTriggeringMode = .searchAsYouType) { + self.interactor = interactor + self.searcher = searcher + self.searchTriggeringMode = searchTriggeringMode + } + + public func connect() { + + interactor.query = searcher.query + + switch searchTriggeringMode { + case .searchAsYouType: + interactor.onQueryChanged.subscribe(with: searcher) { searcher, query in + searcher.query = query + searcher.search() + } + + case .searchOnSubmit: + interactor.onQuerySubmitted.subscribe(with: searcher) { searcher, query in + searcher.query = query + searcher.search() + } + } + } + + public func disconnect() { + + interactor.query = nil + + switch searchTriggeringMode { + case .searchAsYouType: + interactor.onQueryChanged.cancelSubscription(for: searcher) + + case .searchOnSubmit: + interactor.onQuerySubmitted.cancelSubscription(for: searcher) + } + + } + + } + +} + +public extension QueryInputInteractor { + + @discardableResult func connectSearcher(_ searcher: S, + searchTriggeringMode: SearchTriggeringMode = .searchAsYouType) -> SearcherConnection { + let connection = SearcherConnection(interactor: self, searcher: searcher, searchTriggeringMode: searchTriggeringMode) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/QueryInput/QueryInputInteractor.swift b/Sources/InstantSearchCore/QueryInput/QueryInputInteractor.swift new file mode 100644 index 00000000..864de88d --- /dev/null +++ b/Sources/InstantSearchCore/QueryInput/QueryInputInteractor.swift @@ -0,0 +1,32 @@ +// +// QueryInputInteractor.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class QueryInputInteractor { + + public var query: String? { + didSet { + guard oldValue != query else { return } + onQueryChanged.fire(query) + } + } + + public let onQueryChanged: Observer + public let onQuerySubmitted: Observer + + public init() { + onQueryChanged = .init() + onQuerySubmitted = .init() + } + + public func submitQuery() { + onQuerySubmitted.fire(query) + } + +} diff --git a/Sources/InstantSearchCore/Searcher/DisjunctiveFacetingDelegate.swift b/Sources/InstantSearchCore/Searcher/DisjunctiveFacetingDelegate.swift new file mode 100644 index 00000000..74b89b44 --- /dev/null +++ b/Sources/InstantSearchCore/Searcher/DisjunctiveFacetingDelegate.swift @@ -0,0 +1,15 @@ +// +// DisjunctiveFacetingDelegate.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol DisjunctiveFacetingDelegate: class, FilterGroupsConvertible { + + var disjunctiveFacetsAttributes: Set { get } + +} diff --git a/Sources/InstantSearchCore/Searcher/Facet/FacetSearcher+FilterState.swift b/Sources/InstantSearchCore/Searcher/Facet/FacetSearcher+FilterState.swift new file mode 100644 index 00000000..8c328549 --- /dev/null +++ b/Sources/InstantSearchCore/Searcher/Facet/FacetSearcher+FilterState.swift @@ -0,0 +1,51 @@ +// +// FacetSearcher+FilterState.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 05/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension FacetSearcher { + + /** + Establishes connection between searcher and filterState + - Updates filters parameter of Searcher's `Query` according to a new `FilterState` content and relaunches search once `FilterState` changed + - Parameter filterState: filter state to connect + */ + + struct FilterStateConnection: Connection { + + public let facetSearcher: FacetSearcher + public let filterState: FilterState + public let triggerSearchOnFilterStateChange: Bool + + public func connect() { + let shouldTriggerSearch = triggerSearchOnFilterStateChange + filterState.onChange.subscribePast(with: facetSearcher) { searcher, filterState in + searcher.indexQueryState.query.filters = FilterGroupConverter().sql(filterState.toFilterGroups()) + if shouldTriggerSearch { + searcher.search() + } + } + } + + public func disconnect() { + filterState.onChange.cancelSubscription(for: facetSearcher) + } + + } + +} + +public extension FacetSearcher { + + @discardableResult func connectFilterState(_ filterState: FilterState, triggerSearchOnFilterStateChange: Bool = true) -> FilterStateConnection { + let connection = FilterStateConnection(facetSearcher: self, filterState: filterState, triggerSearchOnFilterStateChange: triggerSearchOnFilterStateChange) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Searcher/Facet/FacetSearcher.swift b/Sources/InstantSearchCore/Searcher/Facet/FacetSearcher.swift new file mode 100644 index 00000000..be3a75f6 --- /dev/null +++ b/Sources/InstantSearchCore/Searcher/Facet/FacetSearcher.swift @@ -0,0 +1,133 @@ +// +// FacetSearcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +/** An entity performing search for facet values + */ + +public class FacetSearcher: Searcher, SequencerDelegate, SearchResultObservable { + + public typealias SearchResult = FacetSearchResponse + + public var query: String? { + didSet { + guard oldValue != query else { return } + onQueryChanged.fire(query) + } + } + + public let client: SearchClient + + /// Current tuple of index and query + public var indexQueryState: IndexQueryState + + public var onQueryChanged: Observer + + public let isLoading: Observer + + public let onResults: Observer + + /// Triggered when an error occured during search query execution + /// - Parameter: a tuple of query text and error + public let onError: Observer<(String, Error)> + + /// Name of facet attribute for which the values will be searched + public var facetName: String + + /// Custom request options + public var requestOptions: RequestOptions? + + /// Sequencer which orders and debounce redundant search operations + internal let sequencer: Sequencer + + private let processingQueue: OperationQueue + + /** + - Parameters: + - appID: Application ID + - apiKey: API Key + - indexName: Name of the index in which search will be performed + - facetName: Name of facet attribute for which the values will be searched + - query: Instance of Query. By default a new empty instant of Query will be created. + - requestOptions: Custom request options. Default is `nil`. + */ + public convenience init(appID: ApplicationID, + apiKey: APIKey, + indexName: IndexName, + facetName: String, + query: Query = .init(), + requestOptions: RequestOptions? = nil) { + let client = SearchClient(appID: appID, apiKey: apiKey) + self.init(client: client, + indexName: indexName, + facetName: facetName, + query: query, + requestOptions: requestOptions) + updateClientUserAgents() + } + + /** + - Parameters: + - index: Index value in which search will be performed + - facetName: Name of facet attribute for which the values will be searched + - query: Instance of Query. By default a new empty instant of Query will be created. + - requestOptions: Custom request options. Default is `nil`. + */ + public init(client: SearchClient, + indexName: IndexName, + facetName: String, + query: Query = .init(), + requestOptions: RequestOptions? = nil) { + self.client = client + self.indexQueryState = IndexQueryState(indexName: indexName, query: query) + self.isLoading = .init() + self.onQueryChanged = .init() + self.onResults = .init() + self.onError = .init() + self.facetName = facetName + self.sequencer = .init() + self.processingQueue = .init() + self.requestOptions = requestOptions + sequencer.delegate = self + onResults.retainLastData = true + isLoading.retainLastData = true + processingQueue.maxConcurrentOperationCount = 1 + processingQueue.qualityOfService = .userInitiated + } + + public func search() { + + let query = self.query ?? "" + let indexName = indexQueryState.indexName + + let operation = client.index(withName: indexName).searchForFacetValues(of: Attribute(rawValue: facetName), matching: query, applicableFor: indexQueryState.query) { [weak self] result in + guard let searcher = self else { return } + + searcher.processingQueue.addOperation { + switch result { + case .success(let results): + Logger.Results.success(searcher: searcher, indexName: indexName, results: results) + searcher.onResults.fire(results) + + case .failure(let error): + Logger.Results.failure(searcher: searcher, indexName: indexName, error) + searcher.onError.fire((query, error)) + } + } + } + + sequencer.orderOperation(operationLauncher: { return operation }) + + } + + public func cancel() { + sequencer.cancelPendingOperations() + } + +} diff --git a/Sources/InstantSearchCore/Searcher/HierarchicalFacetingDelegate.swift b/Sources/InstantSearchCore/Searcher/HierarchicalFacetingDelegate.swift new file mode 100644 index 00000000..88a7b58c --- /dev/null +++ b/Sources/InstantSearchCore/Searcher/HierarchicalFacetingDelegate.swift @@ -0,0 +1,14 @@ +// +// HierarchicalFacetingDelegate.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol HierarchicalFacetingDelegate: class { + var hierarchicalAttributes: [Attribute] { get set } + var hierarchicalFilters: [Filter.Facet] { get set } +} diff --git a/Sources/InstantSearchCore/Searcher/MultiIndex/MultiIndexSearcher+FilterState.swift b/Sources/InstantSearchCore/Searcher/MultiIndex/MultiIndexSearcher+FilterState.swift new file mode 100644 index 00000000..12ed6e8a --- /dev/null +++ b/Sources/InstantSearchCore/Searcher/MultiIndex/MultiIndexSearcher+FilterState.swift @@ -0,0 +1,51 @@ +// +// MultiIndexSearcher+FilterState.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension MultiIndexSearcher { + + /** + Establishes connection between searcher and filterState + - Updates filters parameter of Searcher's `Query` at specified index according to a new `FilterState` content and relaunches search once `FilterState` changed + - Parameter filterState: filter state to connect + - Parameter index: index of query to attach to filter state + */ + + struct FilterStateConnection: Connection { + + public let multiIndexSearcher: MultiIndexSearcher + public let filterState: FilterState + public let queryIndex: Int + + public func connect() { + let queryIndex = self.queryIndex + filterState.onChange.subscribe(with: multiIndexSearcher) { searcher, filterState in + searcher.indexQueryStates[self.queryIndex].query.filters = FilterGroupConverter().sql(filterState.toFilterGroups()) + searcher.indexQueryStates[queryIndex].query.page = 0 + searcher.search() + } + } + + public func disconnect() { + filterState.onChange.cancelSubscription(for: multiIndexSearcher) + } + + } + +} + +public extension MultiIndexSearcher { + + @discardableResult func connectFilterState(_ filterState: FilterState, withQueryAtIndex index: Int) -> FilterStateConnection { + let connection = FilterStateConnection(multiIndexSearcher: self, filterState: filterState, queryIndex: index) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Searcher/MultiIndex/MultiIndexSearcher.swift b/Sources/InstantSearchCore/Searcher/MultiIndex/MultiIndexSearcher.swift new file mode 100644 index 00000000..f0b78e60 --- /dev/null +++ b/Sources/InstantSearchCore/Searcher/MultiIndex/MultiIndexSearcher.swift @@ -0,0 +1,184 @@ +// +// MultiIndexSearcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +/** An entity performing search queries targeting multiple indices. +*/ + +public class MultiIndexSearcher: Searcher, SequencerDelegate, SearchResultObservable { + + public var query: String? { + + set { + let oldValue = indexQueryStates.first?.query.query + guard oldValue != newValue else { return } + self.indexQueryStates = indexQueryStates.map { indexQueryState in + let query = indexQueryState.query.set(\.query, to: newValue).set(\.page, to: 0) + return indexQueryState.set(\.query, to: query) + } + onQueryChanged.fire(newValue) + } + + get { + return indexQueryStates.first?.query.query + } + + } + + /// `Client` instance containing indices in which search will be performed + public let client: SearchClient + + /// List of index & query tuples + public internal(set) var indexQueryStates: [IndexQueryState] + + public let isLoading: Observer + + public let onQueryChanged: Observer + + public let onResults: Observer + + /// Triggered when an error occured during search query execution + /// - Parameter: a tuple of query and error + public let onError: Observer<([Query], Error)> + + /// Custom request options + public var requestOptions: RequestOptions? + + /// Sequencer which orders and debounce redundant search operations + internal let sequencer: Sequencer + + /// Helpers for separate pagination management + internal var pageLoaders: [PageLoaderProxy] + + private let processingQueue: OperationQueue + + /** + - Parameters: + - appID: Application ID + - apiKey: API Key + - indexNames: List of the indices names in which search will be performed + - requestOptions: Custom request options. Default is nil. + */ + public convenience init(appID: ApplicationID, + apiKey: APIKey, + indexNames: [IndexName], + requestOptions: RequestOptions? = nil) { + let client = SearchClient(appID: appID, apiKey: apiKey) + let indexQueryStates = indexNames.map { IndexQueryState(indexName: $0, query: .init()) } + self.init(client: client, + indexQueryStates: indexQueryStates, + requestOptions: requestOptions) + } + + /** + - Parameters: + - appID: Application ID + - apiKey: API Key + - indexNames: List of the indices names in which search will be performed + - requestOptions: Custom request options. Default is `nil`. + */ + + public convenience init(client: SearchClient, + indices: [Index], + requestOptions: RequestOptions? = nil) { + let indexQueryStates = indices.map { IndexQueryState(indexName: $0.name, query: .init()) } + self.init(client: client, + indexQueryStates: indexQueryStates, + requestOptions: requestOptions) + } + + /** + - Parameters: + - appID: Application ID + - apiKey: API Key + - indexQueryStates: List of the instances of IndexQueryStates encapsulating index value in which search will be performed and a correspondant Query instance + - requestOptions: Custom request options. Default is nil. + */ + + public init(client: SearchClient, + indexQueryStates: [IndexQueryState], + requestOptions: RequestOptions? = nil) { + + self.client = client + self.indexQueryStates = indexQueryStates + self.requestOptions = requestOptions + self.pageLoaders = [] + + processingQueue = .init() + sequencer = .init() + onQueryChanged = .init() + isLoading = .init() + onResults = .init() + onError = .init() + + sequencer.delegate = self + onResults.retainLastData = true + isLoading.retainLastData = true + updateClientUserAgents() + processingQueue.maxConcurrentOperationCount = 1 + processingQueue.qualityOfService = .userInitiated + + self.pageLoaders = indexQueryStates.enumerated().map { (index, _) in + return PageLoaderProxy(setPage: { self.indexQueryStates[index].query.page = $0 }, launchSearch: self.search) + } + + } + + public func search() { + + let queries = indexQueryStates.map { IndexedQuery(indexName: $0.indexName, query: $0.query) } + + let operation = client.multipleQueries(queries: queries) { [weak self] result in + guard let searcher = self else { return } + searcher.processingQueue.addOperation { + switch result { + case .success(let response): + zip(queries, response.results) + .forEach { (query, searchResults) in + Logger.Results.success(searcher: searcher, indexName: query.indexName, results: searchResults) + } + searcher.onResults.fire(response) + + case .failure(let error): + let indicesDescriptor = "[\(queries.map { $0.indexName.rawValue }.joined(separator: ", "))]" + Logger.Results.failure(searcher: searcher, indexName: IndexName(rawValue: indicesDescriptor), error) + searcher.onError.fire((queries.map { $0.query }, error)) + } + } + } + + sequencer.orderOperation(operationLauncher: { return operation }) + } + + public func cancel() { + sequencer.cancelPendingOperations() + } + +} + +internal extension MultiIndexSearcher { + + class PageLoaderProxy: PageLoadable { + + let setPage: (Int) -> Void + let launchSearch: () -> Void + + init(setPage: @escaping (Int) -> Void, launchSearch: @escaping () -> Void) { + self.setPage = setPage + self.launchSearch = launchSearch + } + + func loadPage(atIndex pageIndex: Int) { + setPage(pageIndex) + launchSearch() + } + + } + +} diff --git a/Sources/InstantSearchCore/Searcher/Places/PlacesSearcher.swift b/Sources/InstantSearchCore/Searcher/Places/PlacesSearcher.swift new file mode 100644 index 00000000..68963ddd --- /dev/null +++ b/Sources/InstantSearchCore/Searcher/Places/PlacesSearcher.swift @@ -0,0 +1,94 @@ +// +// PlacesSearcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient + +public class PlacesSearcher: Searcher, SequencerDelegate, SearchResultObservable { + + public var query: String? { + + get { + return placesQuery.query + } + + set { + let oldValue = placesQuery.query + guard oldValue != newValue else { return } + placesQuery.query = newValue + onQueryChanged.fire(newValue) + } + + } + + public var placesQuery: PlacesQuery + + public var onQueryChanged: Observer + + public let isLoading: Observer + + public let onResults: Observer + + /// Triggered when an error occured during search query execution + /// - Parameter: a tuple of query text and error + public let onError: Observer<(String, Error)> + + /// Sequencer which orders and debounce redundant search operations + internal let sequencer: Sequencer + + internal let placesClient: PlacesClient + + public convenience init(appID: ApplicationID, + apiKey: APIKey, + query: PlacesQuery = .init()) { + let client = PlacesClient(appID: appID, apiKey: apiKey) + self.init(client: client, query: query) + } + + public init(client: PlacesClient, query: PlacesQuery = .init()) { + self.placesClient = client + self.placesQuery = query + self.isLoading = .init() + self.onQueryChanged = .init() + self.onResults = .init() + self.onError = .init() + self.sequencer = .init() + updateClientUserAgents() + sequencer.delegate = self + onResults.retainLastData = true + isLoading.retainLastData = true + } + + public func search() { + + let operation = placesClient.search(query: placesQuery, language: .english) { [weak self] result in + guard let searcher = self else { return } + + switch result { + case .success(let results): + Logger.Results.success(searcher: searcher, indexName: "Algolia Places", results: results) + searcher.onResults.fire(results) + + case .failure(let error): + let query = searcher.placesQuery.query ?? "" + Logger.Results.failure(searcher: searcher, indexName: "Algolia Places", error) + searcher.onError.fire((query, error)) + } + } + + sequencer.orderOperation { + return operation + } + + } + + public func cancel() { + sequencer.cancelPendingOperations() + } + +} diff --git a/Sources/InstantSearchCore/Searcher/Searcher.swift b/Sources/InstantSearchCore/Searcher/Searcher.swift new file mode 100644 index 00000000..d91ea544 --- /dev/null +++ b/Sources/InstantSearchCore/Searcher/Searcher.swift @@ -0,0 +1,59 @@ +// +// Searcher.swift +// InstantSearchCore +// +// Created by Guy Daher on 05/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +/// Protocol describing an entity capable to perform search requests +public protocol Searcher: class { + + /// Current query string + var query: String? { get set } + + /// Triggered when query execution started or stopped + /// - Parameter: boolean value equal to true if query execution started, false otherwise + var isLoading: Observer { get } + + /// Triggered when query text changed + /// - Parameter: a new query text value + var onQueryChanged: Observer { get } + + /// Launches search query execution with current query text value + func search() + + /// Stops search query execution + func cancel() + +} + +/// Protocol describing an entity capable to receive search result +public protocol SearchResultObservable { + + /// Search result type + associatedtype SearchResult + + /// Triggered when a new search result received + var onResults: Observer { get } + +} + +extension Searcher { + + /// Add the library's version to the client's user agents, if not already present. + func updateClientUserAgents() { + _ = UserAgentSetter.set + } + +} + +extension Searcher where Self: SequencerDelegate { + + func didChangeOperationsState(hasPendingOperations: Bool) { + isLoading.fire(hasPendingOperations) + } + +} diff --git a/Sources/InstantSearchCore/Searcher/SingleIndex/SingleIndexSearcher+FilterState.swift b/Sources/InstantSearchCore/Searcher/SingleIndex/SingleIndexSearcher+FilterState.swift new file mode 100644 index 00000000..04284ba6 --- /dev/null +++ b/Sources/InstantSearchCore/Searcher/SingleIndex/SingleIndexSearcher+FilterState.swift @@ -0,0 +1,54 @@ +// +// SingleIndexSearcher+FilterState.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension SingleIndexSearcher { + + /** + Establishes connection between searcher and filterState + - Sets `FilterState` as a disjunctive and hierarchical faceting delegate + - Updates filters parameter of Searcher's `Query` according to a new `FilterState` content and relaunches search once `FilterState` changed + - Parameter filterState: filter state to connect + */ + + struct FilterStateConnection: Connection { + + public let singleIndexSearcher: SingleIndexSearcher + public let filterState: FilterState + + public func connect() { + singleIndexSearcher.disjunctiveFacetingDelegate = filterState + singleIndexSearcher.hierarchicalFacetingDelegate = filterState + + filterState.onChange.subscribePast(with: singleIndexSearcher) { searcher, filterState in + searcher.indexQueryState.query.filters = FilterGroupConverter().sql(filterState.toFilterGroups()) + searcher.indexQueryState.query.page = 0 + searcher.search() + } + } + + public func disconnect() { + singleIndexSearcher.disjunctiveFacetingDelegate = nil + singleIndexSearcher.hierarchicalFacetingDelegate = nil + filterState.onChange.cancelSubscription(for: singleIndexSearcher) + } + + } + +} + +public extension SingleIndexSearcher { + + @discardableResult func connectFilterState(_ filterState: FilterState) -> FilterStateConnection { + let connection = FilterStateConnection(singleIndexSearcher: self, filterState: filterState) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Searcher/SingleIndex/SingleIndexSearcher.swift b/Sources/InstantSearchCore/Searcher/SingleIndex/SingleIndexSearcher.swift new file mode 100644 index 00000000..c0f4461c --- /dev/null +++ b/Sources/InstantSearchCore/Searcher/SingleIndex/SingleIndexSearcher.swift @@ -0,0 +1,202 @@ +// +// SingleIndexSearcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// +// swiftlint:disable function_body_length + +import Foundation +import AlgoliaSearchClient +/** An entity performing search queries targeting one index +*/ + +public class SingleIndexSearcher: Searcher, SequencerDelegate, SearchResultObservable { + + public var query: String? { + + set { + let oldValue = indexQueryState.query.query + guard oldValue != newValue else { return } + cancel() + indexQueryState.query.query = newValue + indexQueryState.query.page = 0 + onQueryChanged.fire(newValue) + } + + get { + return indexQueryState.query.query + } + + } + + public let client: SearchClient + + /// Current index & query tuple + public var indexQueryState: IndexQueryState { + didSet { + if oldValue.indexName != indexQueryState.indexName { + onIndexChanged.fire(indexQueryState.indexName) + } + } + } + + public let isLoading: Observer + + public let onResults: Observer + + /// Triggered when an error occured during search query execution + /// - Parameter: a tuple of query and error + public let onError: Observer<(Query, Error)> + + public let onQueryChanged: Observer + + /// Triggered when an index of Searcher changed + /// - Parameter: equals to a new index value + public let onIndexChanged: Observer + + /// Custom request options + public var requestOptions: RequestOptions? + + /// Delegate providing a necessary information for disjuncitve faceting + public weak var disjunctiveFacetingDelegate: DisjunctiveFacetingDelegate? + + /// Delegate providing a necessary information for hierarchical faceting + public weak var hierarchicalFacetingDelegate: HierarchicalFacetingDelegate? + + /// Flag defining if disjunctive faceting is enabled + /// - Default value: true + public var isDisjunctiveFacetingEnabled = true + + /// Sequencer which orders and debounce redundant search operations + internal let sequencer: Sequencer + + private let processingQueue: OperationQueue + + /** + - Parameters: + - appID: Application ID + - apiKey: API Key + - indexName: Name of the index in which search will be performed + - query: Instance of Query. By default a new empty instant of Query will be created. + - requestOptions: Custom request options. Default is `nil`. + */ + public convenience init(appID: ApplicationID, + apiKey: APIKey, + indexName: IndexName, + query: Query = .init(), + requestOptions: RequestOptions? = nil) { + let client = SearchClient(appID: appID, apiKey: apiKey) + self.init(client: client, indexName: indexName, query: query, requestOptions: requestOptions) + } + + /** + - Parameters: + - index: Index value in which search will be performed + - query: Instance of Query. By default a new empty instant of Query will be created. + - requestOptions: Custom request options. Default is nil. + */ + public init(client: SearchClient, + indexName: IndexName, + query: Query = .init(), + requestOptions: RequestOptions? = nil) { + self.client = client + indexQueryState = .init(indexName: indexName, query: query) + self.requestOptions = requestOptions + sequencer = .init() + isLoading = .init() + onResults = .init() + onError = .init() + onQueryChanged = .init() + onIndexChanged = .init() + processingQueue = .init() + sequencer.delegate = self + onResults.retainLastData = true + onError.retainLastData = false + isLoading.retainLastData = true + updateClientUserAgents() + processingQueue.maxConcurrentOperationCount = 1 + processingQueue.qualityOfService = .userInitiated + } + + /** + - Parameters: + - indexQueryState: Instance of `IndexQueryState` encapsulating index value in which search will be performed and a `Query` instance. + - requestOptions: Custom request options. Default is nil. + */ + public convenience init(client: SearchClient, + indexQueryState: IndexQueryState, + requestOptions: RequestOptions? = nil) { + self.init(client: client, + indexName: indexQueryState.indexName, + query: indexQueryState.query, + requestOptions: requestOptions) + } + + public func search() { + + let query = indexQueryState.query + + let operation: Operation + + if isDisjunctiveFacetingEnabled { + let filterGroups = disjunctiveFacetingDelegate?.toFilterGroups() ?? [] + let hierarchicalAttributes = hierarchicalFacetingDelegate?.hierarchicalAttributes ?? [] + let hierarchicalFilters = hierarchicalFacetingDelegate?.hierarchicalFilters ?? [] + var queriesBuilder = QueryBuilder(query: query, + filterGroups: filterGroups, + hierarchicalAttributes: hierarchicalAttributes, + hierachicalFilters: hierarchicalFilters) + queriesBuilder.keepSelectedEmptyFacets = true + let queries = queriesBuilder.build().map { IndexedQuery(indexName: indexQueryState.indexName, query: $0) } + operation = client.multipleQueries(queries: queries) { [weak self] response in + guard let searcher = self else { return } + + searcher.processingQueue.addOperation { + let indexName = searcher.indexQueryState.indexName + + switch response { + case .failure(let error): + Logger.Results.failure(searcher: searcher, indexName: indexName, error) + searcher.onError.fire((queriesBuilder.query, error)) + + case .success(let results): + do { + let result = try queriesBuilder.aggregate(results.results) + Logger.Results.success(searcher: searcher, indexName: indexName, results: result) + searcher.onResults.fire(result) + } catch let error { + Logger.Results.failure(searcher: searcher, indexName: indexName, error) + searcher.onError.fire((queriesBuilder.query, error)) + } + } + } + } + } else { + operation = client.index(withName: indexQueryState.indexName).search(query: query, requestOptions: requestOptions) { [weak self] result in + guard let searcher = self, searcher.query == query.query else { return } + searcher.processingQueue.addOperation { + let indexName = searcher.indexQueryState.indexName + + switch result { + case .failure(let error): + Logger.Results.failure(searcher: searcher, indexName: indexName, error) + searcher.onError.fire((query, error)) + + case .success(let results): + Logger.Results.success(searcher: searcher, indexName: indexName, results: results) + searcher.onResults.fire(results) + } + } + } + } + + sequencer.orderOperation(operationLauncher: { return operation }) + } + + public func cancel() { + sequencer.cancelPendingOperations() + } + +} diff --git a/Sources/InstantSearchCore/Segmented/SelectableFilterConnector.swift b/Sources/InstantSearchCore/Segmented/SelectableFilterConnector.swift new file mode 100644 index 00000000..bb10aac0 --- /dev/null +++ b/Sources/InstantSearchCore/Segmented/SelectableFilterConnector.swift @@ -0,0 +1,55 @@ +// +// SelectableFilterConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class SelectableFilterConnector: Connection { + + public let searcher: SingleIndexSearcher + public let filterState: FilterState + public let interactor: SelectableSegmentInteractor + public let attribute: Attribute + public let `operator`: RefinementOperator + public let groupName: String + + public let searcherConnection: SelectableFilterInteractorSearcherConnection + public let filterStateConnection: SelectableFilterInteractorFilterStateConnection + + public init(searcher: SingleIndexSearcher, + filterState: FilterState, + items: [Int: Filter], + selected: Int, + attribute: Attribute, + `operator`: RefinementOperator, + groupName: String? = nil) { + self.searcher = searcher + self.filterState = filterState + self.interactor = .init(items: items) + self.attribute = attribute + self.operator = `operator` + self.groupName = groupName ?? attribute.rawValue + self.searcherConnection = self.interactor.connectSearcher(searcher, + attribute: attribute) + self.filterStateConnection = self.interactor.connectFilterState(filterState, + attribute: attribute, + operator: `operator`, + groupName: groupName) + self.interactor.selected = selected + } + + public func connect() { + searcherConnection.connect() + filterStateConnection.connect() + } + + public func disconnect() { + searcherConnection.disconnect() + filterStateConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor+Controller.swift b/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor+Controller.swift new file mode 100644 index 00000000..b78d040b --- /dev/null +++ b/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor+Controller.swift @@ -0,0 +1,62 @@ +// +// SelectableSegmentInteractor+Filter+Controller.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// +// swiftlint:disable type_name + +import Foundation + +public struct SelectableFilterInteractorControllerConnection: Connection where Controller.SegmentKey == Int { + + public typealias Interactor = SelectableSegmentInteractor + + public let interactor: Interactor + public let controller: Controller + public let presenter: FilterPresenter + + public init(interactor: Interactor, + controller: Controller, + presenter: @escaping FilterPresenter = DefaultPresenter.Filter.present) { + self.interactor = interactor + self.controller = controller + self.presenter = presenter + } + + public func connect() { + func setControllerItems(controller: Controller, with items: [Int: Filter]) { + let itemsToPresent = items + .map { ($0.key, presenter(.init($0.value))) } + .reduce(into: [:]) { $0[$1.0] = $1.1 } + controller.setItems(items: itemsToPresent) + } + + controller.setSelected(interactor.selected) + controller.onClick = interactor.computeSelected(selecting:) + interactor.onSelectedChanged.subscribePast(with: controller) { controller, selectedItem in + controller.setSelected(selectedItem) + }.onQueue(.main) + interactor.onItemsChanged.subscribePast(with: controller, callback: setControllerItems).onQueue(.main) + + } + + public func disconnect() { + controller.onClick = nil + interactor.onSelectedChanged.cancelSubscription(for: controller) + interactor.onItemsChanged.cancelSubscription(for: controller) + } + +} + +public extension SelectableSegmentInteractor where Segment: FilterType, SegmentKey == Int { + + @discardableResult func connectController(_ controller: Controller, + presenter: @escaping FilterPresenter = DefaultPresenter.Filter.present) -> SelectableFilterInteractorControllerConnection where Controller.SegmentKey == Int { + let connection = SelectableFilterInteractorControllerConnection(interactor: self, controller: controller, presenter: presenter) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor+FilterState.swift b/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor+FilterState.swift new file mode 100644 index 00000000..17ef65d1 --- /dev/null +++ b/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor+FilterState.swift @@ -0,0 +1,102 @@ +// +// SelectableSegmentInteractor+Filter+FilterState.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// +// swiftlint:disable type_name + +import Foundation + +public struct SelectableFilterInteractorFilterStateConnection: Connection { + + public typealias Interactor = SelectableSegmentInteractor + + public let interactor: Interactor + public let filterState: FilterState + public let attribute: Attribute + public let `operator`: RefinementOperator + public let groupName: String + + public init(interactor: Interactor, + filterState: FilterState, + attribute: Attribute, + `operator`: RefinementOperator, + groupName: String? = nil) { + self.interactor = interactor + self.filterState = filterState + self.attribute = attribute + self.operator = `operator` + self.groupName = groupName ?? attribute.rawValue + } + + public func connect() { + switch `operator` { + case .and: + connectFilterState(filterState, to: interactor, via: SpecializedAndGroupAccessor(filterState[and: groupName])) + case .or: + connectFilterState(filterState, to: interactor, via: filterState[or: groupName]) + } + } + + public func disconnect() { + interactor.onSelectedComputed.cancelSubscription(for: filterState) + filterState.onChange.cancelSubscription(for: interactor) + } + + private func connectFilterState(_ filterState: FilterState, + to interactor: Interactor, + + via accessor: Accessor) where Accessor.Filter == Filter { + whenSelectedComputedThenUpdateFilterState(interactor: interactor, filterState: filterState, via: accessor) + whenFilterStateChangedThenUpdateSelected(interactor: interactor, filterState: filterState, via: accessor) + } + + private func whenSelectedComputedThenUpdateFilterState(interactor: Interactor, + filterState: FilterState, + + via accessor: Accessor) where Accessor.Filter == Filter { + + let removeSelectedItem = { [weak interactor] in + interactor?.selected.flatMap { interactor?.items[$0] }.flatMap(accessor.remove) + } + + let addItem: (Int?) -> Void = { [weak interactor] itemKey in + itemKey.flatMap { interactor?.items[$0] }.flatMap { accessor.add($0) } + } + + interactor.onSelectedComputed.subscribePast(with: filterState) { filterState, computedSelectionKey in + removeSelectedItem() + addItem(computedSelectionKey) + filterState.notifyChange() + } + + } + + private func whenFilterStateChangedThenUpdateSelected(interactor: Interactor, + filterState: FilterState, + via accessor: Accessor) where Accessor.Filter == Filter { + let onChange: (Interactor, ReadOnlyFiltersContainer) -> Void = { interactor, _ in + interactor.selected = interactor.items.first(where: { accessor.contains($0.value) })?.key + } + + onChange(interactor, ReadOnlyFiltersContainer(filterState: filterState)) + + filterState.onChange.subscribePast(with: interactor, callback: onChange) + } + +} + +public extension SelectableSegmentInteractor where SegmentKey == Int, Segment: FilterType { + + @discardableResult func connectFilterState(_ filterState: FilterState, + attribute: Attribute, + operator: RefinementOperator, + groupName: String? = nil) -> SelectableFilterInteractorFilterStateConnection { + let connection = SelectableFilterInteractorFilterStateConnection(interactor: self, filterState: filterState, attribute: attribute, operator: `operator`, groupName: groupName) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor+Searcher.swift b/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor+Searcher.swift new file mode 100644 index 00000000..22444023 --- /dev/null +++ b/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor+Searcher.swift @@ -0,0 +1,36 @@ +// +// SelectableSegmentInteractor+Filter.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 13/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// +// swiftlint:disable type_name + +import Foundation +import AlgoliaSearchClient +public struct SelectableFilterInteractorSearcherConnection: Connection { + + public let interactor: SelectableSegmentInteractor + public let searcher: SingleIndexSearcher + public let attribute: Attribute + + public func connect() { + searcher.indexQueryState.query.updateQueryFacets(with: attribute) + } + + public func disconnect() { + + } + +} + +public extension SelectableSegmentInteractor where SegmentKey == Int, Segment: FilterType { + + @discardableResult func connectSearcher(_ searcher: SingleIndexSearcher, attribute: Attribute) -> SelectableFilterInteractorSearcherConnection { + let connection = SelectableFilterInteractorSearcherConnection(interactor: self, searcher: searcher, attribute: attribute) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor.swift b/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor.swift new file mode 100644 index 00000000..ffabc8c1 --- /dev/null +++ b/Sources/InstantSearchCore/Segmented/SelectableFilterInteractor.swift @@ -0,0 +1,11 @@ +// +// SelectableFilterInteractor.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public typealias SelectableFilterInteractor = SelectableSegmentInteractor diff --git a/Sources/InstantSearchCore/Sequencer/Sequencer.swift b/Sources/InstantSearchCore/Sequencer/Sequencer.swift new file mode 100644 index 00000000..58bc235d --- /dev/null +++ b/Sources/InstantSearchCore/Sequencer/Sequencer.swift @@ -0,0 +1,187 @@ +// +// Copyright (c) 2016 Algolia +// http://www.algolia.com/ +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Manages a sequence of operations. +/// A `Sequencer` keeps track of the order in which operations have been issued, and cancels obsolete requests whenever a +/// response to a more recent operation is received. This ensures that responses are always received in the right order, +/// or discarded. +/// + +protocol Sequencable { + + typealias OperationLauncher = () -> Operation + + func orderOperation(operationLauncher: OperationLauncher) + func cancelPendingOperations() +} + +protocol SequencerDelegate: class { + func didChangeOperationsState(hasPendingOperations: Bool) +} + +class Sequencer: Sequencable { + + // MARK: Properties + + /// Sequence number for the next operation. + private var nextSeqNo: Int = 0 + + /// Queue used to serialize accesses to `nextSeqNo`. + private let incrementSequenceQueue = DispatchQueue(label: "Sequencer.lock") + + /// Sequence number of the last completed operation. + private var lastReceivedSeqNo: Int? + + /// All currently ongoing operations. + private var pendingOperations: [Int: Operation] = [:] { + didSet { + delegate?.didChangeOperationsState(hasPendingOperations: hasPendingOperations) + } + } + + /// Maximum number of pending operations allowed. + /// If many operations are made in a short time, this will keep only the N most recent and cancel the older ones. + /// This helps to avoid filling up the operation queue when the network is slow. + /// Default value: 3 + var maxPendingOperationsCount: Int = 3 + + /// Maximum number of concurrent sequencer completion operations allowed + /// Default value: 5 + var maxConcurrentCompletionOperationsCount: Int = 5 + + /// Indicates whether there are any pending operations. + var hasPendingOperations: Bool { + return !pendingOperations.filter { !($0.value.isCancelled || $0.value.isFinished) }.isEmpty + } + + weak var delegate: SequencerDelegate? + + /// Queue containing SequencerCompletion operations + private let sequencerQueue: OperationQueue + + init() { + self.sequencerQueue = OperationQueue() + self.sequencerQueue.maxConcurrentOperationCount = maxConcurrentCompletionOperationsCount + } + + // MARK: - Sequencing logic + + /// Launch next operation. + func orderOperation(operationLauncher: Sequencable.OperationLauncher) { + // Increase sequence number. + let currentSeqNo: Int = incrementSequenceQueue.sync { + nextSeqNo += 1 + return nextSeqNo + } + + // Cancel obsolete operations. + pendingOperations + .filter { $0.0 <= currentSeqNo - maxPendingOperationsCount }.keys + .forEach(cancelOperation) + + let operation = operationLauncher() + let sequencingOperation = SequencerCompletionOperation(sequenceNo: currentSeqNo, sequencer: self, correspondingOperation: operation) + sequencingOperation.addDependency(operation) + + sequencerQueue.addOperation(sequencingOperation) + + pendingOperations[currentSeqNo] = operation + } + + // MARK: - Manage operations + + /// Cancel all pending operations. + func cancelPendingOperations() { + for seqNo in pendingOperations.keys { + cancelOperation(withSeqNo: seqNo) + } + assert(pendingOperations.isEmpty) + } + + /// Cancel a specific operation. + /// + /// - parameter seqNo: The operation's sequence number. + /// + private func cancelOperation(withSeqNo seqNo: Int) { + if let operation = pendingOperations[seqNo] { + operation.cancel() + pendingOperations.removeValue(forKey: seqNo) + } + } + + /// Clean-up after a succesful completion of a sequenced operation + /// + /// - parameter seqNo: The operation's sequence number. + /// + private func dismissOperation(withSeqNo seqNo: Int) { + + guard let operationToDismiss = pendingOperations[seqNo], !operationToDismiss.isCancelled else { + return + } + + // Remove the current operation. + pendingOperations.removeValue(forKey: seqNo) + + // Obsolete operations should not happen since they have been cancelled by more recent operations (see above). + // WARNING: Only works if the current queue is serial! + assert(lastReceivedSeqNo == nil || lastReceivedSeqNo! < seqNo) + + // Update last received response. + lastReceivedSeqNo = seqNo + } + +} + +private extension Sequencer { + + class SequencerCompletionOperation: Operation { + + let sequenceNo: Int + weak var sequencer: Sequencer? + var correspondingOperation: Operation + + init(sequenceNo: Int, sequencer: Sequencer, correspondingOperation: Operation) { + self.sequenceNo = sequenceNo + self.sequencer = sequencer + self.correspondingOperation = correspondingOperation + } + + override func main() { + guard + let sequencer = sequencer, + !correspondingOperation.isCancelled else { return } + + // Cancel all previous operations (as this one is deemed more recent). + sequencer.pendingOperations + .filter { $0.0 < sequenceNo }.keys + .forEach(sequencer.cancelOperation) + + sequencer.dismissOperation(withSeqNo: sequenceNo) + + } + + } + +} diff --git a/Sources/InstantSearchCore/Signal.swift b/Sources/InstantSearchCore/Signal.swift new file mode 100755 index 00000000..5794e985 --- /dev/null +++ b/Sources/InstantSearchCore/Signal.swift @@ -0,0 +1,320 @@ +// +// Copyright (c) 2014 - 2017 Tuomas Artman. All rights reserved. +// + +import Foundation +#if os(Linux) +import Dispatch +#endif + +/// Create instances of `Signal` and assign them to public constants on your class for each event type that your +/// class fires. +final public class Signal { + + public typealias SignalCallback = (O, T) -> Void + + /// The number of times the `Signal` has fired. + public private(set) var fireCount: Int = 0 + + /// Whether or not the `Signal` should retain a reference to the last data it was fired with. Defaults to false. + public var retainLastData: Bool = false { + didSet { + if !retainLastData { + lastDataFired = nil + } + } + } + + /// The last data that the `Signal` was fired with. In order for the `Signal` to retain the last fired data, its + /// `retainLastFired`-property needs to be set to true + public private(set) var lastDataFired: T? + + /// All the observers of to the `Signal`. + public var observers: [AnyObject] { + get { + return signalListeners.filter { + return $0.observer != nil + }.map { signal -> AnyObject in + return signal.observer! + } + } + } + + private var signalListeners = [SignalSubscription]() + + /// Initializer. + /// + /// - parameter retainLastData: Whether or not the Signal should retain a reference to the last data it was fired + /// with. Defaults to false. + public init(retainLastData: Bool = false) { + self.retainLastData = retainLastData + } + + /// Subscribes an observer to the `Signal`. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke whenever the `Signal` fires. + /// - returns: A `SignalSubscription` that can be used to cancel or filter the subscription. + @discardableResult + public func subscribe(with observer: O, callback: @escaping SignalCallback) -> SignalSubscription { + flushCancelledListeners() + let signalListener = SignalSubscription(observer: observer, callback: callback) + signalListeners.append(signalListener) + return signalListener + } + + /// Subscribes an observer to the `Signal`. The subscription is automatically canceled after the `Signal` has + /// fired once. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke when the signal fires for the first time. + @discardableResult + public func subscribeOnce(with observer: O, callback: @escaping SignalCallback) -> SignalSubscription { + let signalListener = self.subscribe(with: observer, callback: callback) + signalListener.once = true + return signalListener + } + + /// Subscribes an observer to the `Signal` and invokes its callback immediately with the last data fired by the + /// `Signal` if it has fired at least once and if the `retainLastData` property has been set to true. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke whenever the `Signal` fires. + @discardableResult + public func subscribePast(with observer: O, callback: @escaping SignalCallback) -> SignalSubscription { + #if DEBUG + signalsAssert(retainLastData, "can't subscribe to past events on Signal with retainLastData set to false") + #endif + let signalListener = self.subscribe(with: observer, callback: callback) + if let lastDataFired = lastDataFired { + signalListener.callback(lastDataFired) + } + return signalListener + } + + /// Subscribes an observer to the `Signal` and invokes its callback immediately with the last data fired by the + /// `Signal` if it has fired at least once and if the `retainLastData` property has been set to true. If it has + /// not been fired yet, it will continue listening until it fires for the first time. + /// + /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the + /// subscription is automatically cancelled. + /// - parameter callback: The closure to invoke whenever the signal fires. + @discardableResult + public func subscribePastOnce(with observer: O, callback: @escaping SignalCallback) -> SignalSubscription { + #if DEBUG + signalsAssert(retainLastData, "can't subscribe to past events on Signal with retainLastData set to false") + #endif + let signalListener = self.subscribe(with: observer, callback: callback) + if let lastDataFired = lastDataFired { + signalListener.callback(lastDataFired) + signalListener.cancel() + } else { + signalListener.once = true + } + return signalListener + } + + /// Fires the `Singal`. + /// + /// - parameter data: The data to fire the `Signal` with. + public func fire(_ data: T) { + fireCount += 1 + lastDataFired = retainLastData ? data : nil + flushCancelledListeners() + + for signalListener in signalListeners where (signalListener.filter?(data)).isNil(or: true) { + _ = signalListener.dispatch(data: data) + } + } + + /// Cancels all subscriptions for an observer. + /// + /// - parameter observer: The observer whose subscriptions to cancel + public func cancelSubscription(for observer: AnyObject) { + signalListeners = signalListeners.filter { + if let definiteListener: AnyObject = $0.observer { + return definiteListener !== observer + } + return false + } + } + + /// Cancels all subscriptions for the `Signal`. + public func cancelAllSubscriptions() { + signalListeners.removeAll(keepingCapacity: false) + } + + /// Clears the last fired data from the `Signal` and resets the fire count. + public func clearLastData() { + lastDataFired = nil + } + + // MARK: - Private Interface + + private func flushCancelledListeners() { + var removeListeners = false + for signalListener in signalListeners where signalListener.observer == nil { + removeListeners = true + } + if removeListeners { + signalListeners = signalListeners.filter { + return $0.observer != nil + } + } + } +} + +public extension Signal where T == Void { + func fire() { + fire(()) + } +} + +/// A SignalLister represenents an instance and its association with a `Signal`. +final public class SignalSubscription { + + public typealias ParameterType = T + public typealias SignalCallback = (T) -> Void + public typealias SignalFilter = (T) -> Bool + + // The observer. + weak public var observer: AnyObject? + + /// Whether the observer should be removed once it observes the `Signal` firing once. Defaults to false. + public var once = false + + fileprivate var queuedData: T? + fileprivate var filter: (SignalFilter)? + fileprivate var callback: SignalCallback + fileprivate var dispatchQueue: DispatchQueue? + private var sampleInterval: TimeInterval? + + fileprivate init(observer: O, callback: @escaping (O, T) -> Void) { + self.observer = observer + self.callback = { [weak observer] value in + guard let observer = observer else { return } + callback(observer, value) + } + } + + /// Assigns a filter to the `SignalSubscription`. This lets you define conditions under which a observer should actually + /// receive the firing of a `Singal`. The closure that is passed an argument can decide whether the firing of a + /// `Signal` should actually be dispatched to its observer depending on the data fired. + /// + /// If the closeure returns true, the observer is informed of the fire. The default implementation always + /// returns `true`. + /// + /// - parameter predicate: A closure that can decide whether the `Signal` fire should be dispatched to its observer. + /// - returns: Returns self so you can chain calls. + @discardableResult + public func filter(_ predicate: @escaping SignalFilter) -> SignalSubscription { + self.filter = predicate + return self + } + + /// Tells the observer to sample received `Signal` data and only dispatch the latest data once the time interval + /// has elapsed. This is useful if the subscriber wants to throttle the amount of data it receives from the + /// `Signal`. + /// + /// - parameter sampleInterval: The number of seconds to delay dispatch. + /// - returns: Returns self so you can chain calls. + @discardableResult + public func sample(every sampleInterval: TimeInterval) -> SignalSubscription { + self.sampleInterval = sampleInterval + return self + } + + /// Assigns a dispatch queue to the `SignalSubscription`. The queue is used for scheduling the observer calls. If not + /// nil, the callback is fired asynchronously on the specified queue. Otherwise, the block is run synchronously + /// on the posting thread, which is its default behaviour. + /// + /// - parameter queue: A queue for performing the observer's calls. + /// - returns: Returns self so you can chain calls. + @discardableResult + public func onQueue(_ queue: DispatchQueue) -> SignalSubscription { + self.dispatchQueue = queue + return self + } + + /// Cancels the observer. This will cancelSubscription the listening object from the `Signal`. + public func cancel() { + self.observer = nil + } + + // MARK: - Internal Interface + + func dispatch(data: T) -> Bool { + guard observer != nil else { + return false + } + + if once { + observer = nil + } + + if let sampleInterval = sampleInterval { + if queuedData != nil { + queuedData = data + } else { + queuedData = data + let block = { [weak self] () -> Void in + if let definiteSelf = self { + let data = definiteSelf.queuedData! + definiteSelf.queuedData = nil + definiteSelf.callback(data) + } + } + let dispatchQueue = self.dispatchQueue ?? DispatchQueue.main + let deadline = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(sampleInterval * 1000)) + dispatchQueue.asyncAfter(deadline: deadline, execute: block) + } + } else { + if let queue = dispatchQueue { + queue.async { [weak self] in + self?.callback(data) + } + } else { + callback(data) + } + } + + return observer != nil + } +} + +infix operator => : AssignmentPrecedence + +/// Helper operator to fire signal data. +public func => (signal: Signal, data: T) { + signal.fire(data) +} + +private func signalsAssert(_ condition: Bool, _ message: String) { + #if DEBUG + if let assertionHandlerOverride = assertionHandlerOverride { + assertionHandlerOverride(condition, message) + return + } + #endif + assert(condition, message) +} + +#if DEBUG +var assertionHandlerOverride:((_ condition: Bool, _ message: String) -> Void)? +#endif + +extension Optional where Wrapped == Bool { + + func isNil(or expectedValue: Bool) -> Bool { + switch self { + case .none: + return true + case .some(let value): + return value == expectedValue + } + } + +} diff --git a/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor+Controller.swift b/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor+Controller.swift new file mode 100644 index 00000000..b2e0312f --- /dev/null +++ b/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor+Controller.swift @@ -0,0 +1,58 @@ +// +// IndexSegmentInteractor+Controller.swift +// InstantSearchCore +// +// Created by Guy Daher on 06/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension IndexSegment { + + struct ControllerConnection: Connection where Controller.SegmentKey == Int { + + public let interactor: IndexSegmentInteractor + public let controller: Controller + public let presenter: IndexPresenter + + public init(interactor: IndexSegmentInteractor, + controller: Controller, + presenter: @escaping IndexPresenter = DefaultPresenter.Index.present) { + self.interactor = interactor + self.controller = controller + self.presenter = presenter + } + + public func connect() { + controller.setItems(items: interactor.items.mapValues(presenter)) + controller.onClick = interactor.computeSelected(selecting:) + interactor.onSelectedChanged.subscribePast(with: controller) { controller, selectedItem in + controller.setSelected(selectedItem) + }.onQueue(.main) + interactor.onItemsChanged.subscribePast(with: controller) { controller, newItems in + controller.setItems(items: newItems.mapValues(self.presenter)) + }.onQueue(.main) + } + + public func disconnect() { + controller.setItems(items: [:]) + controller.onClick = nil + interactor.onSelectedChanged.cancelSubscription(for: controller) + interactor.onItemsChanged.cancelSubscription(for: controller) + } + + } + +} + +public extension IndexSegmentInteractor { + + @discardableResult func connectController(_ controller: Controller, + presenter: @escaping IndexPresenter = DefaultPresenter.Index.present) -> IndexSegment.ControllerConnection where Controller.SegmentKey == SegmentKey { + let connection = IndexSegment.ControllerConnection(interactor: self, controller: controller, presenter: presenter) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor+MultiIndexSearcher.swift b/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor+MultiIndexSearcher.swift new file mode 100644 index 00000000..aa02f81a --- /dev/null +++ b/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor+MultiIndexSearcher.swift @@ -0,0 +1,60 @@ +// +// IndexSegmentInteractor+MultiIndexSearcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 12/09/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension IndexSegment { + + struct MultiIndexSearcherConnection: Connection { + + let interactor: IndexSegmentInteractor + let searcher: MultiIndexSearcher + let queryIndex: Int + + public func connect() { + + if + let selected = interactor.selected, + let index = interactor.items[selected] + { + searcher.indexQueryStates[queryIndex].indexName = index.name + searcher.indexQueryStates[queryIndex].query.page = 0 + } + + let queryIndex = self.queryIndex + interactor.onSelectedComputed.subscribePast(with: searcher) { [weak interactor] searcher, computed in + if + let selected = computed, + let index = interactor?.items[selected] + { + interactor?.selected = selected + searcher.indexQueryStates[queryIndex].indexName = index.name + searcher.indexQueryStates[queryIndex].query.page = 0 + searcher.search() + } + } + + } + + public func disconnect() { + interactor.onSelectedComputed.cancelSubscription(for: searcher) + } + + } + +} + +public extension IndexSegmentInteractor { + + @discardableResult func connectSearcher(searcher: MultiIndexSearcher, toQueryAtIndex queryIndex: Int) -> IndexSegment.MultiIndexSearcherConnection { + let connection = IndexSegment.MultiIndexSearcherConnection(interactor: self, searcher: searcher, queryIndex: queryIndex) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor+SingleIndexSearcher.swift b/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor+SingleIndexSearcher.swift new file mode 100644 index 00000000..058ceb47 --- /dev/null +++ b/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor+SingleIndexSearcher.swift @@ -0,0 +1,53 @@ +// +// IndexSegmentInteractor+SingleIndexSearcher.swift +// InstantSearchCore +// +// Created by Guy Daher on 06/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension IndexSegment { + + struct SingleIndexSearcherConnection: Connection { + + let interactor: IndexSegmentInteractor + let searcher: SingleIndexSearcher + + public func connect() { + if let selected = interactor.selected, let index = interactor.items[selected] { + searcher.indexQueryState.indexName = index.name + searcher.indexQueryState.query.page = 0 + } + + interactor.onSelectedComputed.subscribePast(with: searcher) { [weak interactor] searcher, computed in + if + let selected = computed, + let index = interactor?.items[selected] + { + interactor?.selected = selected + searcher.indexQueryState.indexName = index.name + searcher.indexQueryState.query.page = 0 + searcher.search() + } + } + } + + public func disconnect() { + interactor.onSelectedComputed.cancelSubscription(for: searcher) + } + + } + +} + +public extension IndexSegmentInteractor { + + @discardableResult func connectSearcher(searcher: SingleIndexSearcher) -> IndexSegment.SingleIndexSearcherConnection { + let connection = IndexSegment.SingleIndexSearcherConnection(interactor: self, searcher: searcher) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor.swift b/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor.swift new file mode 100644 index 00000000..82aaa772 --- /dev/null +++ b/Sources/InstantSearchCore/SortBy/IndexSegmentInteractor.swift @@ -0,0 +1,13 @@ +// +// IndexSegmentInteractor.swift +// InstantSearchCore +// +// Created by Guy Daher on 06/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +public typealias IndexSegmentInteractor = SelectableSegmentInteractor + +public enum IndexSegment {} diff --git a/Sources/InstantSearchCore/SortBy/SortByConnector.swift b/Sources/InstantSearchCore/SortBy/SortByConnector.swift new file mode 100644 index 00000000..793fd86c --- /dev/null +++ b/Sources/InstantSearchCore/SortBy/SortByConnector.swift @@ -0,0 +1,36 @@ +// +// SortByConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient + +public class SortByConnector: Connection { + + public let searcher: SingleIndexSearcher + public let interactor: IndexSegmentInteractor + + public let searcherConnection: Connection + + init(searcher: SingleIndexSearcher, + indices: [Int: Index], + selected: Int? = nil) { + self.searcher = searcher + self.interactor = .init(items: indices) + self.searcherConnection = interactor.connectSearcher(searcher: searcher) + self.interactor.selected = selected + } + + public func connect() { + searcherConnection.connect() + } + + public func disconnect() { + searcherConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/Stats/SearchStats.swift b/Sources/InstantSearchCore/Stats/SearchStats.swift new file mode 100644 index 00000000..9b13949a --- /dev/null +++ b/Sources/InstantSearchCore/Stats/SearchStats.swift @@ -0,0 +1,99 @@ +// +// SearchStats.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 14/04/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation + +public struct SearchStats: Codable { + + enum CodingKeys: String, CodingKey { + case totalHitsCount = "nbHits" + case page + case pagesCount = "nbPages" + case hitsPerPage + case processingTimeMS + case query + case queryID + } + + /// Number of hits per page. + public let hitsPerPage: Int + + /// Total number of hits. + public let totalHitsCount: Int + + /// Total number of pages. + public let pagesCount: Int + + /// Last returned page. + public let page: Int + + /// Processing time of the last query (in ms). + public let processingTimeMS: Int + + /// Query text that produced these results. + public let query: String? + + /// Query ID that produced these results. + /// Mandatory when reporting click and conversion events + /// Only reported when `clickAnalytics=true` in the `Query` + /// + public let queryID: QueryID? + + init() { + self.hitsPerPage = 0 + self.totalHitsCount = 0 + self.pagesCount = 0 + self.page = 0 + self.processingTimeMS = 0 + self.query = nil + self.queryID = nil + } + + init(totalHitsCount: Int, + hitsPerPage: Int? = nil, + pagesCount: Int = 1, + page: Int = 0, + processingTimeMS: Int, + query: String? = nil, + queryID: QueryID? = nil) { + self.totalHitsCount = totalHitsCount + self.hitsPerPage = hitsPerPage ?? totalHitsCount + self.pagesCount = pagesCount + self.page = page + self.processingTimeMS = processingTimeMS + self.query = query + self.queryID = queryID + } + + public init(from decoder: Decoder) throws { + + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.totalHitsCount = try container.decode(Int.self, forKey: .totalHitsCount) + self.page = try container.decodeIfPresent(Int.self, forKey: .page) ?? 0 + self.pagesCount = try container.decodeIfPresent(Int.self, forKey: .pagesCount) ?? 1 + self.hitsPerPage = try container.decodeIfPresent(Int.self, forKey: .hitsPerPage) ?? 20 + self.processingTimeMS = try container.decode(Int.self, forKey: .processingTimeMS) + self.query = try container.decodeIfPresent(String.self, forKey: .query) + self.queryID = try container.decodeIfPresent(QueryID.self, forKey: .queryID) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(totalHitsCount, forKey: .totalHitsCount) + try container.encode(page, forKey: .page) + try container.encode(pagesCount, forKey: .pagesCount) + try container.encode(hitsPerPage, forKey: .hitsPerPage) + try container.encode(processingTimeMS, forKey: .processingTimeMS) + try container.encodeIfPresent(query, forKey: .query) + try container.encodeIfPresent(queryID, forKey: .queryID) + + } + +} diff --git a/Sources/InstantSearchCore/Stats/SearchStatsConvertible.swift b/Sources/InstantSearchCore/Stats/SearchStatsConvertible.swift new file mode 100644 index 00000000..4d90494c --- /dev/null +++ b/Sources/InstantSearchCore/Stats/SearchStatsConvertible.swift @@ -0,0 +1,57 @@ +// +// SearchStatsConvertible.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 15/04/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation + +public protocol SearchStatsConvertible { + + var searchStats: SearchStats { get } + +} + +extension SearchResponse: SearchStatsConvertible { + + public var searchStats: SearchStats { + return .init(totalHitsCount: nbHits ?? 0, + hitsPerPage: hitsPerPage ?? 20, + pagesCount: nbPages ?? 1, + page: page ?? 0, + processingTimeMS: processingTimeMS.flatMap(Int.init) ?? 0, + query: query, + queryID: queryID) + } + +} + +extension PlacesResponse: SearchStatsConvertible { + + public var searchStats: SearchStats { + return .init(totalHitsCount: nbHits, + hitsPerPage: nbHits, + pagesCount: 1, + page: 0, + processingTimeMS: Int(processingTimeMS), + query: query, + queryID: nil) + } + +} + +extension FacetSearchResponse: SearchStatsConvertible { + + public var searchStats: SearchStats { + return .init(totalHitsCount: facetHits.count, + hitsPerPage: facetHits.count, + pagesCount: 1, + page: 0, + processingTimeMS: Int(processingTimeMS), + query: nil, + queryID: nil) + } + +} diff --git a/Sources/InstantSearchCore/Stats/StatsConnector.swift b/Sources/InstantSearchCore/Stats/StatsConnector.swift new file mode 100644 index 00000000..999e9b68 --- /dev/null +++ b/Sources/InstantSearchCore/Stats/StatsConnector.swift @@ -0,0 +1,33 @@ +// +// StatsConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class StatsConnector: Connection { + + public let searcher: SingleIndexSearcher + public let interactor: StatsInteractor + + public let searcherConnection: Connection + + public init(searcher: SingleIndexSearcher, + interactor: StatsInteractor = .init()) { + self.searcher = searcher + self.interactor = interactor + searcherConnection = interactor.connectSearcher(searcher) + } + + public func connect() { + searcherConnection.connect() + } + + public func disconnect() { + searcherConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/Stats/StatsController.swift b/Sources/InstantSearchCore/Stats/StatsController.swift new file mode 100644 index 00000000..bc311d83 --- /dev/null +++ b/Sources/InstantSearchCore/Stats/StatsController.swift @@ -0,0 +1,15 @@ +// +// StatsController.swift +// InstantSearchCore +// +// Created by Guy Daher on 23/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public protocol ItemTextController: ItemController where Item == String? {} + +public protocol ItemAttributedTextController: ItemController where Item == NSAttributedString? {} + +public typealias StatsTextController = ItemTextController diff --git a/Sources/InstantSearchCore/Stats/StatsInteractor+Controller.swift b/Sources/InstantSearchCore/Stats/StatsInteractor+Controller.swift new file mode 100644 index 00000000..e2138831 --- /dev/null +++ b/Sources/InstantSearchCore/Stats/StatsInteractor+Controller.swift @@ -0,0 +1,17 @@ +// +// StatsInteractor+Controller.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension StatsInteractor { + + @discardableResult func connectController(_ controller: Controller) -> ControllerConnection { + return connectController(controller, presenter: DefaultPresenter.Stats.present) + } + +} diff --git a/Sources/InstantSearchCore/Stats/StatsInteractor+SingleIndexSearcher.swift b/Sources/InstantSearchCore/Stats/StatsInteractor+SingleIndexSearcher.swift new file mode 100644 index 00000000..5d19bb0a --- /dev/null +++ b/Sources/InstantSearchCore/Stats/StatsInteractor+SingleIndexSearcher.swift @@ -0,0 +1,44 @@ +// +// StatsInteractor+SingleIndexSearcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension StatsInteractor { + + struct SingleIndexSearcherConnection: Connection { + + let interactor: StatsInteractor + let searcher: SingleIndexSearcher + + public func connect() { + searcher.onResults.subscribePast(with: interactor) { interactor, searchResults in + interactor.item = searchResults.searchStats + } + searcher.onError.subscribe(with: interactor) { interactor, _ in + interactor.item = .none + } + } + + public func disconnect() { + searcher.onResults.cancelSubscription(for: interactor) + searcher.onError.cancelSubscription(for: interactor) + } + + } + +} + +public extension StatsInteractor { + + @discardableResult func connectSearcher(_ searcher: SingleIndexSearcher) -> SingleIndexSearcherConnection { + let connection = SingleIndexSearcherConnection(interactor: self, searcher: searcher) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Stats/StatsInteractor.swift b/Sources/InstantSearchCore/Stats/StatsInteractor.swift new file mode 100644 index 00000000..b82e1ab6 --- /dev/null +++ b/Sources/InstantSearchCore/Stats/StatsInteractor.swift @@ -0,0 +1,15 @@ +// +// StatsInteractor.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 31/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class StatsInteractor: ItemInteractor { + public init() { + super.init(item: .none) + } +} diff --git a/Sources/InstantSearchCore/Toggle/ToggleFilter+Controller.swift b/Sources/InstantSearchCore/Toggle/ToggleFilter+Controller.swift new file mode 100644 index 00000000..55cf0360 --- /dev/null +++ b/Sources/InstantSearchCore/Toggle/ToggleFilter+Controller.swift @@ -0,0 +1,54 @@ +// +// SelectableInteractor+Filter+Controller.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/08/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public extension ToggleFilter { + + struct ControllerConnection: Connection where Controller.Item == Filter { + + public let interactor: SelectableInteractor + public let controller: Controller + + public init(interactor: SelectableInteractor, + controller: Controller) { + self.interactor = interactor + self.controller = controller + } + + public func connect() { + controller.setItem(interactor.item) + controller.setSelected(interactor.isSelected) + controller.onClick = interactor.computeIsSelected(selecting:) + interactor.onSelectedChanged.subscribePast(with: controller) { controller, isSelected in + controller.setSelected(isSelected) + }.onQueue(.main) + interactor.onItemChanged.subscribePast(with: controller) { controller, item in + controller.setItem(item) + }.onQueue(.main) + } + + public func disconnect() { + controller.onClick = nil + interactor.onSelectedChanged.cancelSubscription(for: controller) + interactor.onItemChanged.cancelSubscription(for: controller) + } + + } + +} + +public extension SelectableInteractor where Item: FilterType { + + @discardableResult func connectController(_ controller: Controller) -> ToggleFilter.ControllerConnection where Controller.Item == (Item) { + let connection = ToggleFilter.ControllerConnection(interactor: self, controller: controller) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Toggle/ToggleFilter+FilterState.swift b/Sources/InstantSearchCore/Toggle/ToggleFilter+FilterState.swift new file mode 100644 index 00000000..c410ebd8 --- /dev/null +++ b/Sources/InstantSearchCore/Toggle/ToggleFilter+FilterState.swift @@ -0,0 +1,121 @@ +// +// SelectableInteractor+Filter.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 06/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public enum ToggleFilter {} + +public extension ToggleFilter { + + struct FilterStateConnection: Connection { + + public let interactor: SelectableInteractor + public let filterState: FilterState + public let `operator`: RefinementOperator + public let groupName: String + + public init(interactor: SelectableInteractor, + filterState: FilterState, + operator: RefinementOperator, + groupName: String? = nil) { + self.interactor = interactor + self.filterState = filterState + self.operator = `operator` + self.groupName = groupName ?? interactor.item.attribute.rawValue + } + + public func connect() { + switch `operator` { + case .and: + connectFilterState(via: SpecializedAndGroupAccessor(filterState[and: groupName])) + case .or: + connectFilterState(via: filterState[or: groupName]) + } + } + + public func disconnect() { + filterState.onChange.cancelSubscription(for: interactor) + interactor.onSelectedComputed.cancelSubscription(for: filterState) + } + + private func connectFilterState(via accessor: GroupAccessor) where GroupAccessor.Filter == Filter { + whenFilterStateChangedThenUpdateSelections(via: accessor) + whenSelectionsComputedThenUpdateFilterState(attribute: interactor.item.attribute, via: accessor) + } + + func whenFilterStateChangedThenUpdateSelections(via accessor: GroupAccessor) where GroupAccessor.Filter == Filter { + + let onChange: (SelectableInteractor, ReadOnlyFiltersContainer) -> Void = { interactor, _ in + interactor.isSelected = accessor.contains(interactor.item) + } + + onChange(interactor, ReadOnlyFiltersContainer(filterState: filterState)) + + filterState.onChange.subscribePast(with: interactor, callback: onChange) + } + + func whenSelectionsComputedThenUpdateFilterState(attribute: Attribute, + via accessor: GroupAccessor) where GroupAccessor.Filter == Filter { + + interactor.onSelectedComputed.subscribePast(with: filterState) { [weak interactor] filterState, computedSelected in + + guard + let interactor = interactor + else { return } + + if computedSelected { + accessor.add(interactor.item) + } else { + accessor.remove(interactor.item) + } + + filterState.notifyChange() + + } + + } + + func whenSelectionsComputedThenUpdateFilterState(attribute: Attribute, + groupID: FilterGroup.ID, + default: F) { + + interactor.onSelectedComputed.subscribePast(with: filterState) { [weak interactor] filterState, computedSelected in + + guard let interactor = interactor else { return } + + if computedSelected { + filterState.filters.remove(`default`, fromGroupWithID: groupID) + filterState.filters.add(interactor.item, toGroupWithID: groupID) + } else { + filterState.filters.remove(interactor.item, fromGroupWithID: groupID) + filterState.filters.add(`default`, toGroupWithID: groupID) + } + + filterState.notifyChange() + + } + + } + + } + +} + +public extension SelectableInteractor where Item: FilterType { + + @discardableResult func connectFilterState(_ filterState: FilterState, + + operator: RefinementOperator = .or, + + groupName: String? = nil) -> ToggleFilter.FilterStateConnection { + let connection = ToggleFilter.FilterStateConnection(interactor: self, filterState: filterState, operator: `operator`, groupName: groupName) + connection.connect() + return connection + } + +} diff --git a/Sources/InstantSearchCore/Toggle/ToggleFilterConnector.swift b/Sources/InstantSearchCore/Toggle/ToggleFilterConnector.swift new file mode 100644 index 00000000..15445c0f --- /dev/null +++ b/Sources/InstantSearchCore/Toggle/ToggleFilterConnector.swift @@ -0,0 +1,41 @@ +// +// ToggleFilterConnector.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 29/11/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +public class ToggleFilterConnector: Connection { + + public let filterState: FilterState + public let filter: Filter + public let interactor: SelectableInteractor + + public let filterStateConnection: Connection + + public init(filterState: FilterState, + filter: Filter, + isSelected: Bool, + refinementOperator: RefinementOperator = .and, + groupName: String? = nil) { + self.filterState = filterState + self.filter = filter + self.interactor = SelectableInteractor(item: filter) + self.interactor.isSelected = isSelected + self.filterStateConnection = interactor.connectFilterState(filterState, + operator: refinementOperator, + groupName: groupName ?? filter.attribute.rawValue) + } + + public func connect() { + filterStateConnection.connect() + } + + public func disconnect() { + filterStateConnection.disconnect() + } + +} diff --git a/Sources/InstantSearchCore/Tracker/FilterTrackable.swift b/Sources/InstantSearchCore/Tracker/FilterTrackable.swift new file mode 100644 index 00000000..e5924c1f --- /dev/null +++ b/Sources/InstantSearchCore/Tracker/FilterTrackable.swift @@ -0,0 +1,44 @@ +// +// FilterTrackable.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 19/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import InstantSearchInsights + +protocol FilterTrackable { + + func viewed(eventName: EventName, + indexName: IndexName, + filters: [String], + userToken: UserToken?) + + func clicked(eventName: EventName, + indexName: IndexName, + filters: [String], + userToken: UserToken?) + + func converted(eventName: EventName, + indexName: IndexName, + filters: [String], + userToken: UserToken?) + +} + +extension Insights: FilterTrackable { + + func viewed(eventName: EventName, indexName: IndexName, filters: [String], userToken: UserToken?) { + self.viewed(eventName: eventName.rawValue, indexName: indexName.rawValue, filters: filters, userToken: userToken?.rawValue) + } + + func clicked(eventName: EventName, indexName: IndexName, filters: [String], userToken: UserToken?) { + self.clicked(eventName: eventName.rawValue, indexName: indexName.rawValue, filters: filters, userToken: userToken?.rawValue) + } + + func converted(eventName: EventName, indexName: IndexName, filters: [String], userToken: UserToken?) { + self.converted(eventName: eventName.rawValue, indexName: indexName.rawValue, filters: filters, userToken: userToken?.rawValue) + } +} diff --git a/Sources/InstantSearchCore/Tracker/FilterTracker.swift b/Sources/InstantSearchCore/Tracker/FilterTracker.swift new file mode 100644 index 00000000..052dd402 --- /dev/null +++ b/Sources/InstantSearchCore/Tracker/FilterTracker.swift @@ -0,0 +1,86 @@ +// +// FilterTracker.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 18/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import InstantSearchInsights + +public class FilterTracker: InsightsTracker { + + public let eventName: EventName + internal let searcher: TrackableSearcher + internal let tracker: FilterTrackable + + public required convenience init(eventName: EventName, + searcher: TrackableSearcher, + insights: Insights) { + self.init(eventName: eventName, + searcher: searcher, + tracker: insights) + } + + init(eventName: EventName, + searcher: TrackableSearcher, + tracker: FilterTrackable) { + self.eventName = eventName + self.searcher = searcher + self.tracker = tracker + } + +} + +// MARK: - Filter tracking methods + +public extension FilterTracker { + + func trackClick(for filter: F, + eventName customEventName: EventName? = nil) { + guard let sqlForm = (filter as? SQLSyntaxConvertible)?.sqlForm else { return } + tracker.clicked(eventName: customEventName ?? eventName, indexName: searcher.indexName, filters: [sqlForm], userToken: .none) + } + + func trackView(for filter: F, + eventName customEventName: EventName? = nil) { + guard let sqlForm = (filter as? SQLSyntaxConvertible)?.sqlForm else { return } + tracker.viewed(eventName: customEventName ?? eventName, indexName: searcher.indexName, filters: [sqlForm], userToken: .none) + } + + func trackConversion(for filter: F, + eventName customEventName: EventName? = nil) { + guard let sqlForm = (filter as? SQLSyntaxConvertible)?.sqlForm else { return } + tracker.converted(eventName: customEventName ?? eventName, indexName: searcher.indexName, filters: [sqlForm], userToken: .none) + } + +} + +// MARK: - Facet tracking methods + +public extension FilterTracker { + + private func filter(for facet: Facet, with attribute: Attribute) -> Filter.Facet { + return Filter.Facet(attribute: attribute, stringValue: facet.value) + } + + func trackClick(for facet: Facet, + attribute: Attribute, + eventName customEventName: EventName? = nil) { + trackClick(for: filter(for: facet, with: attribute), eventName: customEventName ?? eventName) + } + + func trackView(for facet: Facet, + attribute: Attribute, + eventName customEventName: EventName? = nil) { + trackView(for: filter(for: facet, with: attribute), eventName: customEventName ?? eventName) + } + + func trackConversion(for facet: Facet, + attribute: Attribute, + eventName customEventName: EventName? = nil) { + trackConversion(for: filter(for: facet, with: attribute), eventName: customEventName ?? eventName) + } + +} diff --git a/Sources/InstantSearchCore/Tracker/HitsAfterSearchTrackable.swift b/Sources/InstantSearchCore/Tracker/HitsAfterSearchTrackable.swift new file mode 100644 index 00000000..af5e3265 --- /dev/null +++ b/Sources/InstantSearchCore/Tracker/HitsAfterSearchTrackable.swift @@ -0,0 +1,47 @@ +// +// HitsAfterSearchTrackable.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 19/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import InstantSearchInsights + +protocol HitsAfterSearchTrackable { + + func clickedAfterSearch(eventName: EventName, + indexName: IndexName, + objectIDsWithPositions: [(ObjectID, Int)], + queryID: QueryID, + userToken: UserToken?) + + func convertedAfterSearch(eventName: EventName, + indexName: IndexName, + objectIDs: [ObjectID], + queryID: QueryID, + userToken: UserToken?) + + func viewed(eventName: EventName, + indexName: IndexName, + objectIDs: [ObjectID], + userToken: UserToken?) + +} + +extension Insights: HitsAfterSearchTrackable { + + func clickedAfterSearch(eventName: EventName, indexName: IndexName, objectIDsWithPositions: [(ObjectID, Int)], queryID: QueryID, userToken: UserToken?) { + self.clickedAfterSearch(eventName: eventName.rawValue, indexName: indexName.rawValue, objectIDsWithPositions: objectIDsWithPositions.map { ($0.rawValue, $1) }, queryID: queryID.rawValue, userToken: userToken?.rawValue) + } + + func convertedAfterSearch(eventName: EventName, indexName: IndexName, objectIDs: [ObjectID], queryID: QueryID, userToken: UserToken?) { + self.convertedAfterSearch(eventName: eventName.rawValue, indexName: indexName.rawValue, objectIDs: objectIDs.map(\.rawValue), queryID: queryID.rawValue, userToken: userToken?.rawValue) + } + + func viewed(eventName: EventName, indexName: IndexName, objectIDs: [ObjectID], userToken: UserToken?) { + self.viewed(eventName: eventName.rawValue, indexName: indexName.rawValue, objectIDs: objectIDs.map(\.rawValue), userToken: userToken?.rawValue) + } + +} diff --git a/Sources/InstantSearchCore/Tracker/HitsTracker.swift b/Sources/InstantSearchCore/Tracker/HitsTracker.swift new file mode 100644 index 00000000..dc95d64e --- /dev/null +++ b/Sources/InstantSearchCore/Tracker/HitsTracker.swift @@ -0,0 +1,82 @@ +// +// HitsInteractor+Tracker.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 18/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import InstantSearchInsights + +public class HitsTracker: InsightsTracker { + + public let eventName: EventName + internal let searcher: TrackableSearcher + internal let tracker: HitsAfterSearchTrackable + internal var queryID: QueryID? + + public required convenience init(eventName: EventName, + searcher: TrackableSearcher, + insights: Insights) { + self.init(eventName: eventName, + searcher: searcher, + tracker: insights) + } + + init(eventName: EventName, + searcher: TrackableSearcher, + tracker: HitsAfterSearchTrackable) { + self.eventName = eventName + self.searcher = searcher + self.tracker = tracker + + searcher.setClickAnalyticsOn(true) + searcher.subscribeForQueryIDChange(self) + } + + deinit { + switch searcher { + case .singleIndex(let searcher): + searcher.onResults.cancelSubscription(for: self) + case .multiIndex(let searcher, _): + searcher.onResults.cancelSubscription(for: self) + } + } + +} + +// MARK: - Hits tracking methods + +public extension HitsTracker { + + func trackClick(for hit: Hit, + position: Int, + eventName customEventName: EventName? = nil) { + guard let queryID = queryID else { return } + tracker.clickedAfterSearch(eventName: customEventName ?? self.eventName, + indexName: searcher.indexName, + objectIDsWithPositions: [(hit.objectID, position)], + queryID: queryID, + userToken: .none) + } + + func trackConvert(for hit: Hit, + eventName customEventName: EventName? = nil) { + guard let queryID = queryID else { return } + tracker.convertedAfterSearch(eventName: customEventName ?? self.eventName, + indexName: searcher.indexName, + objectIDs: [hit.objectID], + queryID: queryID, + userToken: .none) + } + + func trackView(for hit: Hit, + eventName customEventName: EventName? = nil) { + tracker.viewed(eventName: customEventName ?? self.eventName, + indexName: searcher.indexName, + objectIDs: [hit.objectID], + userToken: .none) + } + +} diff --git a/Sources/InstantSearchCore/Tracker/InsightsTracker.swift b/Sources/InstantSearchCore/Tracker/InsightsTracker.swift new file mode 100644 index 00000000..a2ea6f7c --- /dev/null +++ b/Sources/InstantSearchCore/Tracker/InsightsTracker.swift @@ -0,0 +1,58 @@ +// +// InsightsTracker.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 20/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import InstantSearchInsights + +public protocol InsightsTracker: class { + + init(eventName: EventName, searcher: TrackableSearcher, insights: Insights) + +} + +extension InsightsTracker { + + public init(eventName: EventName, + searcher: SingleIndexSearcher, + userToken: String? = .none) { + let credentials: AlgoliaSearchClient.Credentials = searcher.client + let insights = Insights.register(appId: credentials.applicationID.rawValue, apiKey: credentials.apiKey.rawValue, userToken: userToken) + self.init(eventName: eventName, + searcher: .singleIndex(searcher), + insights: insights) + } + + public init(eventName: EventName, + searcher: SingleIndexSearcher, + insights: Insights) { + self.init(eventName: eventName, + searcher: .singleIndex(searcher), + insights: insights) + } + + public init(eventName: EventName, + searcher: MultiIndexSearcher, + pointer: Int, + userToken: String? = .none) { + let credentials: AlgoliaSearchClient.Credentials = searcher.client + let insights = Insights.register(appId: credentials.applicationID.rawValue, apiKey: credentials.apiKey.rawValue, userToken: userToken) + self.init(eventName: eventName, + searcher: .multiIndex(searcher, pointer: pointer), + insights: insights) + } + + public init(eventName: EventName, + searcher: MultiIndexSearcher, + pointer: Int, + insights: Insights) { + self.init(eventName: eventName, + searcher: .multiIndex(searcher, pointer: pointer), + insights: insights) + } + +} diff --git a/Sources/InstantSearchCore/Tracker/TrackableSearcher.swift b/Sources/InstantSearchCore/Tracker/TrackableSearcher.swift new file mode 100644 index 00000000..0aa66088 --- /dev/null +++ b/Sources/InstantSearchCore/Tracker/TrackableSearcher.swift @@ -0,0 +1,56 @@ +// +// TrackableSearcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 19/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation + +protocol QueryIDContainer: class { + var queryID: QueryID? { get set } +} + +extension HitsTracker: QueryIDContainer {} + +public enum TrackableSearcher { + + case singleIndex(SingleIndexSearcher) + case multiIndex(MultiIndexSearcher, pointer: Int) + + var indexName: IndexName { + switch self { + case .singleIndex(let searcher): + return searcher.indexQueryState.indexName + + case .multiIndex(let searcher, pointer: let index): + return searcher.indexQueryStates[index].indexName + } + } + + func setClickAnalyticsOn(_ on: Bool) { + switch self { + case .singleIndex(let searcher): + return searcher.indexQueryState.query.clickAnalytics = on + + case .multiIndex(let searcher, pointer: let index): + return searcher.indexQueryStates[index].query.clickAnalytics = on + } + } + + func subscribeForQueryIDChange(_ subscriber: S) { + switch self { + case .singleIndex(let searcher): + searcher.onResults.subscribe(with: subscriber) { (subscriber, results) in + subscriber.queryID = results.queryID + } + case .multiIndex(let searcher, pointer: let index): + searcher.onResults.subscribe(with: subscriber) { (subscriber, results) in + subscriber.queryID = results.results[index].queryID + } + } + + } + +} diff --git a/Sources/README.md b/Sources/README.md deleted file mode 100644 index 38bcf639..00000000 --- a/Sources/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# InstantSearch iOS - -**InstantSearch iOS** is a declarative UI library providing widgets and helpers for building native, component-driven UIs with Algolia. -It is built on top of Algolia's [iOS API Client](https://github.com/algolia/algoliasearch-client-swift) and [iOS InstantSearch Core Client](https://github.com/algolia/instantsearch-core-swift) to provide you a high-level solution to quickly build various search interfaces. - -## Widgets - -The core part of InstantSearch iOS are the widgets, which are search-aware components that are binded to search events coming from Algolia. - -Widgets binds `UIKit UIViews`, whether it is an advanced `UICollectionView`, or a simple `UISlider`. -They are also customisable by exposing `IBInspectable` parameters that can be set right through Interface Builder. - -The nice thing about InstantSearch iOS is that you don't have to rewrite your existing `UIViews` to start using the library. -In fact, the architecture of the library is mostly protocol oriented, making it extendible and compatible with your existing UI. -It follows Plugin Architecture conventions by providing most of the business logic mostly through protocols, and sometimes through base `UIViewController` classes. - -In that way, it is also very easy to create your own search-aware custom widgets. All it takes is implementing one or more protocols depending on the purpose of the widget, and then writing the business logic using the provided properties and methods coming from the protocols. - -## Implementation Notes - -### Architecture of InstantSearch - - InstantSearch is inspired by both MVVM architecture. - - This is an overview of the architecture: - - ``` - View <--> ViewModel <--> Binder <--> Interactor/Model - ``` - - Widgets can mean two things, depending on how modular you want your components to be: - - - 1. It can be the View - - ``` - WidgetV <--> WidgetVM <--> Binder <--> Searcher - ``` - - In this first case, we offer a better modular architecture where a WidgetVM can be reused - for different kind of widgets, for example: a collectionView and tableView can share - the same VM since the business logic is exactly the same, only the layout changes. - In that case, the Widget is independent of InstantSearchCore and WidgetVM is independent of UIKit. - - - 2. It can be the View and the ViewModel - - ``` - WidgetVVM <--> Binder <--> Searcher - ``` - - In this second case, we offer an easier way to create new widgets since the widget has access - to the searcher and all of its method. The downside here is that we can't reuse the business logic - through a VM. The upside is that it's easy for 3rd party devs to create their own widgets and plug into IS. - In that case, the Widget is dependent on both InstantSearchCore and UIKit. - We note that the View and the ViewModel depend on abstract delegates, which makes them reusable and testable. - - Finally, the Binder plays a role of exposing all possible search events, whether from the Searcher or other widgets, - and making them available for ViewModels or Views so that they can tune in. - In a way, it is like an observable that knows about all search events, and it will send the search events to - the observers that decided to tune in. We decided to go with delegation to offer a clean safe interface. - -### InstantSearch Notes - - InstantSearch does mainly 3 things: - - 1. Scans the View to find Algolia Widgets - 2. Knows about all search events, whether coming from the Searcher or other widgets - 3. Binds Searcher - Widgets through delegation - -For the 3rd point, InstantSearch binds the following: - - - Searcher and WidgetV through a ViewModelFetcher that creates the appropriate WidgetVM - - Searcher and WidgetVVM \ No newline at end of file diff --git a/Tests/CollectionViewHitsControllerTests.swift b/Tests/CollectionViewHitsControllerTests.swift deleted file mode 100644 index d3e76f37..00000000 --- a/Tests/CollectionViewHitsControllerTests.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// CollectionViewHitsControllerTests.swift -// InstantSearchTests -// -// Created by Vladislav Fitc on 04/09/2019. -// - -@testable import InstantSearch -import Foundation -import XCTest - - -class TestTemplateCollectionViewCell: UICollectionViewCell {} - -class CollectionViewHitsControllerTests: XCTestCase { - - func testMissingDataSource() { - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - - let dataSource = HitsCollectionViewDataSource { (_, hit, _) in - let cell = TestCollectionViewCell() - cell.content = hit - return cell - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = dataSource.collectionView(collectionView, cellForItemAt: .init()) - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = dataSource.collectionView(collectionView, numberOfItemsInSection: .init()) - } - - let delegate = HitsCollectionViewDelegate { _,_,_ in } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = delegate.collectionView(collectionView, didSelectItemAt: .init()) - } - - } - - func testTemplate() { - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - - let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) - - let dataSource = HitsCollectionViewDataSource { (_, hit, _) -> UICollectionViewCell in - let cell = TestCollectionViewCell() - cell.content = hit - return cell - } - - dataSource.hitsSource = hitsDataSource - - dataSource.templateCellProvider = { return TestTemplateCollectionViewCell() } - - XCTAssert(dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 4, section: 0)) is TestTemplateCollectionViewCell, "") - - } - - func testDataSource() { - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - - let dataSource = HitsCollectionViewDataSource { (_, hit, _) -> UICollectionViewCell in - let cell = TestCollectionViewCell() - cell.content = hit - return cell - } - - let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) - - dataSource.hitsSource = hitsDataSource - - XCTAssertEqual(dataSource.collectionView(collectionView, numberOfItemsInSection: 0), 3) - XCTAssertEqual((dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 1, section: 0)) as? TestCollectionViewCell)?.content, "t2") - - } - - func testDelegate() { - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - - let itemToSelect = 2 - - let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) - - let exp = expectation(description: "Hit selection") - - let delegate = HitsCollectionViewDelegate { (_, hit, _) in - XCTAssertEqual(hit, hitsDataSource.hits[itemToSelect]) - exp.fulfill() - } - - delegate.hitsSource = hitsDataSource - - delegate.collectionView(collectionView, didSelectItemAt: IndexPath(item: itemToSelect, section: 0)) - - waitForExpectations(timeout: 1, handler: .none) - - } - -// func testWidget() { -// -// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) -// -// let vm = HitsInteractor() -// -// let dataSource = HitsCollectionViewDataSource> { (_, hit, _) -> UICollectionViewCell in -// let cell = TestCollectionViewCell() -// cell.content = hit -// return cell -// } -// -// dataSource.hitsSource = vm -// -// let delegate = HitsCollectionViewDelegate> { (_, _, _) in } -// -// delegate.hitsSource = vm -// -// let widget = HitsCollectionController>(collectionView: collectionView) -// -// widget.dataSource = dataSource -// widget.delegate = delegate - -// XCTAssertTrue(collectionView.delegate === delegate) -// XCTAssertTrue(collectionView.dataSource === dataSource) - -// } - -} diff --git a/Tests/CollectionViewMultiIndexHitsControllerTests.swift b/Tests/CollectionViewMultiIndexHitsControllerTests.swift deleted file mode 100644 index 8875b94c..00000000 --- a/Tests/CollectionViewMultiIndexHitsControllerTests.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// CollectionViewMultiIndexHitsControllerTests.swift -// InstantSearch -// -// Created by Vladislav Fitc on 04/09/2019. -// - -import Foundation - -@testable import InstantSearch -import InstantSearchCore -import Foundation -import XCTest - -class TestCollectionViewCell: UICollectionViewCell { - var content: String? -} - -class CollectionViewMultiIndexHitsControllerTests: XCTestCase { - - func testDataSource() { - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - - let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) - - let dataSource = MultiIndexHitsCollectionViewDataSource() - - dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) in - let cell = TestCollectionViewCell() - cell.content = h - return cell - } - - dataSource.hitsSource = hitsSource - - XCTAssertEqual(dataSource.numberOfSections(in: collectionView), 2) - XCTAssertEqual(dataSource.collectionView(collectionView, numberOfItemsInSection: 0), 2) - - } - - func testDelegate() { - - let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) - - let delegate = MultiIndexHitsTableViewDelegate() - delegate.hitsSource = hitsSource - - } - - func testMissingHitsSource() { - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - - let dataSource = MultiIndexHitsCollectionViewDataSource() - - dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) in - let cell = TestCollectionViewCell() - cell.content = h - return cell - } - - let delegate = MultiIndexHitsCollectionViewDelegate() - - delegate.setClickHandler(forSection: 0) { (_, h: String, _) in - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = dataSource.numberOfSections(in: collectionView) - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = dataSource.collectionView(collectionView, numberOfItemsInSection: 0) - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 0, section: 0)) - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - delegate.collectionView(collectionView, didSelectItemAt: IndexPath(item: 0, section: 0)) - } - - } - - - func testMissingCellHandler() { - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - - let dataSource = MultiIndexHitsCollectionViewDataSource() - - let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) - - dataSource.hitsSource = hitsSource - - expectLog(expectedMessage: "No cell configurator found for section 0", expectedLevel: .warning) { - _ = dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 0, section: 0)) - } - - } - - func testMissingClickHandler() { - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - - let delegate = MultiIndexHitsCollectionViewDelegate() - - let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) - - delegate.hitsSource = hitsSource - - expectLog(expectedMessage: "No click handler found for section 0", expectedLevel: .warning) { - _ = delegate.collectionView(collectionView, didSelectItemAt: IndexPath(row: 0, section: 0)) - } - - } - -} diff --git a/Tests/FatalErrorTest.swift b/Tests/FatalErrorTest.swift deleted file mode 100644 index b0ac9401..00000000 --- a/Tests/FatalErrorTest.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// FatalErrorTest.swift -// InstantSearch -// -// Created by Vladislav Fitc on 04/09/2019. -// - -@testable import InstantSearch -import Foundation -import XCTest - -extension XCTestCase { - - func expectFatalError(expectedMessage: String, testcase: @escaping () -> Void) { - - // arrange - let expectation = self.expectation(description: "expectingFatalError") - var assertionMessage: String? = nil - - // override fatalError. This will pause forever when fatalError is called. - FatalErrorUtil.replaceFatalError { message, _, _ in - assertionMessage = message - expectation.fulfill() - unreachable() - } - - // act, perform on separate thead because a call to fatalError pauses forever - DispatchQueue.global(qos: .userInitiated).async(execute: testcase) - - waitForExpectations(timeout: 10) { _ in - // assert - XCTAssertEqual(assertionMessage, expectedMessage) - - // clean up - FatalErrorUtil.restoreFatalError() - } - } -} diff --git a/Tests/Info.plist b/Tests/Info.plist deleted file mode 100644 index 6041ed27..00000000 --- a/Tests/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 5.2.3 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/Tests/InstantSearch-Tests-Bridging-Header.h b/Tests/InstantSearch-Tests-Bridging-Header.h deleted file mode 100644 index 402f13d1..00000000 --- a/Tests/InstantSearch-Tests-Bridging-Header.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// InstantSearch-Tests-Bridging-Header.h -// InstantSearch -// -// Created by Guy Daher on 12/05/2017. -// -// - -#ifndef InstantSearch_Tests_Bridging_Header_h -#define InstantSearch_Tests_Bridging_Header_h - -#endif /* InstantSearch_Tests_Bridging_Header_h */ - diff --git a/Tests/InstantSearchCoreTests/InstantSearchCoreTests.swift b/Tests/InstantSearchCoreTests/InstantSearchCoreTests.swift new file mode 100644 index 00000000..c84b394d --- /dev/null +++ b/Tests/InstantSearchCoreTests/InstantSearchCoreTests.swift @@ -0,0 +1,11 @@ +import XCTest +@testable import InstantSearchCore + +final class InstantSearchCoreTests: XCTestCase { + func testExample() { + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/Tests/InstantSearchCoreTests/Integration/DisjuncitveAndHierarchicalIntegrationTests.swift b/Tests/InstantSearchCoreTests/Integration/DisjuncitveAndHierarchicalIntegrationTests.swift new file mode 100644 index 00000000..d828432d --- /dev/null +++ b/Tests/InstantSearchCoreTests/Integration/DisjuncitveAndHierarchicalIntegrationTests.swift @@ -0,0 +1,135 @@ +// +// DisjuncitveAndHierarchicalIntegrationTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore +import AlgoliaSearchClient +class DisjuncitveAndHierarchicalIntegrationTests: OnlineTestCase { + + struct Item: Codable { + let objectID: String = UUID().uuidString + let name: String + let color: String? + let hierarchicalCategories: [String: String] + } + + static func attribute(for level: Int) -> Attribute { + return .init(rawValue: "hierarchicalCategories.lvl\(level)") + } + + let lvl0 = { attribute(for: 0) }() + let lvl1 = { attribute(for: 1) }() + let lvl2 = { attribute(for: 2) }() + + var hierarchicalAttributes: [Attribute] { + return [lvl0, lvl1, lvl2] + } + + let colorAttribute: Attribute = "color" + + let cat1 = "Category1" + let cat2 = "Category2" + let cat3 = "Category3" + + let cat2_1 = "Category2 > SubCategory1" + let cat2_2 = "Category2 > SubCategory2" + let cat3_1 = "Category3 > SubCategory1" + let cat3_2 = "Category3 > SubCategory2" + + let cat3_1_1 = "Category3 > SubCategory1 > SubSubCategory1" + let cat3_1_2 = "Category3 > SubCategory1 > SubSubCategory2" + let cat3_2_1 = "Category3 > SubCategory2 > SubSubCategory1" + let cat3_2_2 = "Category3 > SubCategory2 > SubSubCategory2" + + var facetAttributes: [Attribute] { + return hierarchicalAttributes + [colorAttribute] + } + + override func setUpWithError() throws { + try super.setUpWithError() + let settings = Settings().set(\.attributesForFaceting, to: facetAttributes.map { .default($0) }) + let items = try [Item](jsonFilename: "disjunctiveHierarchical.json") + try fillIndex(withItems: items, settings: settings) + } + + func testDisjuncitiveHierarchical() { + + let expectedHierarchicalFacets: [(Attribute, [Facet])] = [ + (lvl0, [ + .init(value: cat3, count: 2, highlighted: nil), + .init(value: cat2, count: 1, highlighted: nil) + ]), + (lvl1, [ + .init(value: cat3_2, count: 2, highlighted: nil) + ]), + (lvl2, [ + .init(value: cat3_2_1, count: 1, highlighted: nil), + .init(value: cat3_2_2, count: 1, highlighted: nil) + ]) + ] + + let expectedDisjunctiveFacets: [(Attribute, [Facet])] = [ + (colorAttribute, [ + Facet(value: "red", count: 2, highlighted: nil), + Facet(value: "blue", count: 1, highlighted: nil) + ]) + ] + + let colorFilter = Filter.Facet(attribute: colorAttribute, stringValue: "red") + + let hierarchicalFilter = Filter.Facet(attribute: lvl1, stringValue: cat3_2) + + let filterGroups: [FilterGroupType] = [ + FilterGroup.And(filters: [hierarchicalFilter], name: "_hierarchical"), + FilterGroup.Or(filters: [colorFilter], name: "color") + ] + + let hierarchicalFilters = [ + Filter.Facet(attribute: lvl0, stringValue: cat3), + Filter.Facet(attribute: lvl1, stringValue: cat3_2) + ] + + let query = Query("").set(\.facets, to: Set(facetAttributes)) + + let queryBuilder = QueryBuilder(query: query, + filterGroups: filterGroups, + hierarchicalAttributes: hierarchicalAttributes, + hierachicalFilters: hierarchicalFilters) + + let queries = queryBuilder.build() + + XCTAssertEqual(queryBuilder.disjunctiveFacetingQueriesCount, 1) + XCTAssertEqual(queryBuilder.hierarchicalFacetingQueriesCount, 3) + + let exp = expectation(description: "results") + + let indexQueries = queries.map { IndexedQuery(indexName: self.index.name, query: $0) } + + client!.multipleQueries(queries: indexQueries) { result in + switch result { + case .failure(let error): + XCTFail("\(error)") + case .success(let response): + let finalResult = try! queryBuilder.aggregate(response.results) + expectedDisjunctiveFacets.forEach { (attribute, facets) in + XCTAssertTrue(finalResult.disjunctiveFacets?[attribute]?.equalContents(to: facets) == true) + } + expectedHierarchicalFacets.forEach { (attribute, facets) in + XCTAssertTrue(finalResult.hierarchicalFacets?[attribute]?.equalContents(to: facets) == true) + } + + } + exp.fulfill() + } + + waitForExpectations(timeout: 15, handler: .none) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Integration/DisjunctiveFacetingIntegrationTests.swift b/Tests/InstantSearchCoreTests/Integration/DisjunctiveFacetingIntegrationTests.swift new file mode 100644 index 00000000..6c300b45 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Integration/DisjunctiveFacetingIntegrationTests.swift @@ -0,0 +1,143 @@ +// +// DisjunctiveFacetingIntegrationTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore +import AlgoliaSearchClient +class DisjunctiveFacetingIntegrationTests: OnlineTestCase { + + struct Item: Codable { + let objectID: String = UUID().uuidString + let category: String + let color: String? + let promotions: TreeModel? + } + + let disjunctiveAttributes: [Attribute] = [ + "category", + "color", + "promotions" + ] + + override func setUpWithError() throws { + try super.setUpWithError() + let settings = Settings().set(\.attributesForFaceting, to: disjunctiveAttributes.map { .default($0) }) + let items = try [Item](jsonFilename: "disjunctive.json") + try fillIndex(withItems: items, settings: settings) + } + + func testDisjunctive() { + + let expectedFacets: [(Attribute, [Facet])] = [ + ("category", [ + .init(value: "shirt", count: 2, highlighted: nil), + .init(value: "hat", count: 1, highlighted: nil) + ]), + ("promotions", [ + .init(value: "free return", count: 2, highlighted: nil), + .init(value: "coupon", count: 1, highlighted: nil), + .init(value: "on sale", count: 1, highlighted: nil) + ]) + ] + + let expectedDisjucntiveFacets: [(Attribute, [Facet])] = [ + ("color", [ + .init(value: "blue", count: 3, highlighted: nil), + .init(value: "green", count: 2, highlighted: nil), + .init(value: "orange", count: 2, highlighted: nil), + .init(value: "yellow", count: 2, highlighted: nil), + .init(value: "red", count: 1, highlighted: nil) + ]) + ] + + let query = Query().set(\.facets, to: Set(disjunctiveAttributes)) + let colorFilter = Filter.Facet(attribute: "color", stringValue: "blue") + let disjunctiveGroup = FilterGroup.Or(filters: [colorFilter], name: "colors") + let queryBuilder = QueryBuilder(query: query, filterGroups: [disjunctiveGroup]) + + let queries = queryBuilder.build().map { IndexedQuery(indexName: index.name, query: $0) } + + XCTAssertEqual(queries.count, 2) + XCTAssertEqual(queryBuilder.disjunctiveFacetingQueriesCount, 1) + + let exp = expectation(description: "results") + + client.multipleQueries(queries: queries) { result in + do { + let searchesResponse = try result.get() + let finalResult = try! queryBuilder.aggregate(searchesResponse.results) + expectedFacets.forEach { (attribute, facets) in + XCTAssertTrue(finalResult.facets?[attribute]?.equalContents(to: facets) == true) + } + expectedDisjucntiveFacets.forEach { (attribute, facets) in + XCTAssertTrue(finalResult.disjunctiveFacets?[attribute]?.equalContents(to: facets) == true) + } + exp.fulfill() + + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 15, handler: .none) + + } + + func testMultiDisjunctive() { + + let expectedFacets: [(Attribute, [Facet])] = [ + ("category", [ + .init(value: "shirt", count: 1, highlighted: nil) + ]), + ("promotions", [ + .init(value: "coupon", count: 1, highlighted: nil) + ]) + ] + + let expectedDisjucntiveFacets: [(Attribute, [Facet])] = [ + ("color", [ + .init(value: "blue", count: 1, highlighted: nil) + ]) + ] + + let query = Query().set(\.facets, to: Set(disjunctiveAttributes)) + let colorFilter = Filter.Facet(attribute: "color", stringValue: "blue") + let disjunctiveGroup = FilterGroup.Or(filters: [colorFilter], name: "colors") + let promotionsFilter = Filter.Facet(attribute: "promotions", stringValue: "coupon") + let conjunctiveGroup = FilterGroup.And(filters: [promotionsFilter], name: "promotions") + let queryBuilder = QueryBuilder(query: query, filterGroups: [disjunctiveGroup, conjunctiveGroup]) + + let queries = queryBuilder.build().map { IndexedQuery(indexName: index.name, query: $0) } + + XCTAssertEqual(queries.count, 2) + XCTAssertEqual(queryBuilder.disjunctiveFacetingQueriesCount, 1) + + let exp = expectation(description: "results") + + client.multipleQueries(queries: queries) { result in + do { + let searchesResponse = try result.get() + let finalResult = try queryBuilder.aggregate(searchesResponse.results) + expectedFacets.forEach { (attribute, facets) in + XCTAssertTrue(finalResult.facets?[attribute]?.equalContents(to: facets) == true) + } + expectedDisjucntiveFacets.forEach { (attribute, facets) in + XCTAssertTrue(finalResult.disjunctiveFacets?[attribute]?.equalContents(to: facets) == true) + } + exp.fulfill() + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 15, handler: .none) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Integration/HierarchicalIntegrationTests.swift b/Tests/InstantSearchCoreTests/Integration/HierarchicalIntegrationTests.swift new file mode 100644 index 00000000..6563aece --- /dev/null +++ b/Tests/InstantSearchCoreTests/Integration/HierarchicalIntegrationTests.swift @@ -0,0 +1,210 @@ +// +// HierarchicalIntegrationTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore +import AlgoliaSearchClient +class HierarchicalTests: OnlineTestCase { + + struct Item: Codable { + let objectID: String = UUID().uuidString + let name: String + let hierarchicalCategories: [String: String] + } + + static func attribute(for level: Int) -> Attribute { + return .init(rawValue: "hierarchicalCategories.lvl\(level)") + } + + let lvl0 = { attribute(for: 0) }() + let lvl1 = { attribute(for: 1) }() + let lvl2 = { attribute(for: 2) }() + var hierarchicalAttributes: [Attribute] { + return [lvl0, lvl1, lvl2] + } + + let clothing = "Clothing" + let book = "Book" + let furniture = "Furniture" + + let clothing_men = "Clothing > Men" + let clothing_women = "Clothing > Women" + + let clothing_men_hats = "Clothing > Men > Hats" + let clothing_men_shirt = "Clothing > Men > Shirt" + + override func setUpWithError() throws { + try super.setUpWithError() + let settings = Settings().set(\.attributesForFaceting, to: hierarchicalAttributes.map { .default($0) }) + let items = try [Item](jsonFilename: "hierarchical.json") + try fillIndex(withItems: items, settings: settings) + } + + func testHierachical() { + + let filter = Filter.Facet(attribute: lvl1, stringValue: clothing_men) + + let filterGroups = [FilterGroup.And(filters: [filter], name: "_hierarchical")] + + let hierarchicalFilters = [ + Filter.Facet(attribute: lvl0, stringValue: clothing), + Filter.Facet(attribute: lvl1, stringValue: clothing_men) + ] + + let expectedHierarchicalFacets: [(Attribute, [Facet])] = [ + (lvl0, [ + .init(value: clothing, count: 4, highlighted: nil), + .init(value: book, count: 2, highlighted: nil), + .init(value: furniture, count: 1, highlighted: nil) + ]), + (lvl1, [ + .init(value: clothing_men, count: 2, highlighted: nil), + .init(value: clothing_women, count: 2, highlighted: nil) + ]), + (lvl2, [ + .init(value: clothing_men_hats, count: 1, highlighted: nil), + .init(value: clothing_men_shirt, count: 1, highlighted: nil) + ]) + ] + + let query = Query("").set(\.facets, to: Set(hierarchicalAttributes)) + + let queryBuilder = QueryBuilder(query: query, + filterGroups: filterGroups, + hierarchicalAttributes: hierarchicalAttributes, + hierachicalFilters: hierarchicalFilters) + let queries = queryBuilder.build().map { IndexedQuery(indexName: self.index.name, query: $0) } + + XCTAssertEqual(queryBuilder.hierarchicalFacetingQueriesCount, 3) + + let exp = expectation(description: "results") + + client.multipleQueries(queries: queries) { result in + do { + let searchesResponse = try result.get() + let finalResult = try queryBuilder.aggregate(searchesResponse.results) + expectedHierarchicalFacets.forEach { (attribute, facets) in + XCTAssertTrue(finalResult.hierarchicalFacets?[attribute]?.equalContents(to: facets) == true) + } + exp.fulfill() + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 15, handler: .none) + + } + + func testHierachicalEmpty() throws { + + let filterGroups: [FilterGroupType] = [] + + let hierarchicalFilters: [Filter.Facet] = [] + + let query = Query("").set(\.facets, to: Set(hierarchicalAttributes)) + + let queryBuilder = QueryBuilder(query: query, + filterGroups: filterGroups, + hierarchicalAttributes: hierarchicalAttributes, + hierachicalFilters: hierarchicalFilters) + let queries = queryBuilder.build().map { IndexedQuery(indexName: self.index.name, query: $0) } + + XCTAssertEqual(queryBuilder.hierarchicalFacetingQueriesCount, 0) + + let exp = expectation(description: "results") + + client!.multipleQueries(queries: queries) { result in + do { + let searchesResponse = try result.get() + let finalResult = try queryBuilder.aggregate(searchesResponse.results) + XCTAssertNil(finalResult.hierarchicalFacets) + exp.fulfill() + } catch let error { + XCTFail("\(error)") + } + + } + + waitForExpectations(timeout: 15, handler: .none) + + } + + func testHierarchicalLastLevel() { + + let filter = Filter.Facet(attribute: lvl2, stringValue: clothing_men_hats) + + let filterGroups = [FilterGroup.And(filters: [filter], name: "_hierarchical")] + + let hierarchicalFilters = [ + Filter.Facet(attribute: lvl0, stringValue: clothing), + Filter.Facet(attribute: lvl1, stringValue: clothing_men), + Filter.Facet(attribute: lvl2, stringValue: clothing_men_hats) + ] + + let expectedHierarchicalFacets: [(Attribute, [Facet])] = [ + (lvl0, [ + .init(value: clothing, count: 4, highlighted: nil), + .init(value: book, count: 2, highlighted: nil), + .init(value: furniture, count: 1, highlighted: nil) + ]), + (lvl1, [ + .init(value: clothing_men, count: 2, highlighted: nil), + .init(value: clothing_women, count: 2, highlighted: nil) + ]), + (lvl2, [ + .init(value: clothing_men_hats, count: 1, highlighted: nil), + .init(value: clothing_men_shirt, count: 1, highlighted: nil) + ]) + ] + + let query = Query("").set(\.facets, to: Set(hierarchicalAttributes)) + + let queryBuilder = QueryBuilder(query: query, + filterGroups: filterGroups, + hierarchicalAttributes: hierarchicalAttributes, + hierachicalFilters: hierarchicalFilters) + let queries = queryBuilder.build().map { IndexedQuery(indexName: self.index.name, query: $0) } + + XCTAssertEqual(queryBuilder.hierarchicalFacetingQueriesCount, 3) + + let exp = expectation(description: "results") + + client!.multipleQueries(queries: queries) { result in + do { + let searchesResponse = try result.get() + let finalResult = try queryBuilder.aggregate(searchesResponse.results) + expectedHierarchicalFacets.forEach { (attribute, facets) in + XCTAssertTrue(finalResult.hierarchicalFacets?[attribute]?.equalContents(to: facets) == true) + } + exp.fulfill() + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 15, handler: .none) + + } + +} + +extension Array where Element: Equatable { + func equalContents(to other: [Element]) -> Bool { + guard self.count == other.count else { return false } + for e in self { + let currentECount = filter { $0 == e }.count + let otherECount = other.filter { $0 == e }.count + guard currentECount == otherECount else { + return false + } + } + return true + } +} diff --git a/Tests/InstantSearchCoreTests/Integration/OnlineTestCase.swift b/Tests/InstantSearchCoreTests/Integration/OnlineTestCase.swift new file mode 100644 index 00000000..b22c4b8d --- /dev/null +++ b/Tests/InstantSearchCoreTests/Integration/OnlineTestCase.swift @@ -0,0 +1,80 @@ +// +// OnlineTestCase.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/07/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore +import AlgoliaSearchClient +/// Abstract base class for online test cases. +/// +class OnlineTestCase: XCTestCase { + + struct Task: Codable { + let id: Int + enum CodingKeys: String, CodingKey { + case id = "taskID" + } + } + + var expectationTimeout: TimeInterval = 10 + + var client: SearchClient! + var index: Index! + + override func setUpWithError() throws { + super.setUp() + + // Init client. + guard let credentials = TestCredentials.search else { + throw Error.missingCredentials + } + + _ = UserAgentSetter.set + + client = SearchClient(appID: credentials.applicationID, apiKey: credentials.apiKey) + + // Init index. + // NOTE: We use a different index name for each test function. + let className = String(reflecting: type(of: self)).components(separatedBy: ".").last! + let functionName = invocation!.selector.description + let indexName = "\(className).\(functionName)" + index = client.index(withName: safeIndexName(indexName)) + + // Delete the index. + // Although it's not shared with other test functions, it could remain from a previous execution. + try index.delete().wait() + } + + override func tearDown() { + super.tearDown() + + let expectation = self.expectation(description: "Delete index") + client.index(withName: index.name).delete { result in + switch result { + case .failure(let error): + XCTFail("\(error)") + case .success: + break + } + expectation.fulfill() + } + waitForExpectations(timeout: expectationTimeout, handler: nil) + } + + func fillIndex(withItems items: [O], settings: Settings) throws { + try index.saveObjects(items).wait() + try index.setSettings(settings).wait() + } + +} + +extension OnlineTestCase { + enum Error: Swift.Error { + case missingCredentials + } +} diff --git a/Tests/InstantSearchCoreTests/Integration/TestCredentials.swift b/Tests/InstantSearchCoreTests/Integration/TestCredentials.swift new file mode 100644 index 00000000..cbb0ea88 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Integration/TestCredentials.swift @@ -0,0 +1,50 @@ +// +// TestCredentials.swift +// +// +// Created by Vladislav Fitc on 05/06/2020. +// + +import Foundation +import AlgoliaSearchClient + +struct TestCredentials: Credentials { + + let applicationID: ApplicationID + let apiKey: APIKey + + static let search: TestCredentials? = { + if + let appID = String(environmentVariable: "ALGOLIA_APPLICATION_ID_1"), + let apiKey = String(environmentVariable: "ALGOLIA_ADMIN_KEY_1") { + return TestCredentials(applicationID: ApplicationID(rawValue: appID), apiKey: APIKey(rawValue: apiKey)) + } else { + return nil + } + }() + + static let places: TestCredentials? = { + if + let appID = String(environmentVariable: "ALGOLIA_PLACES_APPLICATION_ID"), + let apiKey = String(environmentVariable: "ALGOLIA_PLACES_API_KEY") { + return TestCredentials(applicationID: ApplicationID(rawValue: appID), apiKey: APIKey(rawValue: apiKey)) + } else { + return nil + } + }() + +} + +extension String { + + init?(environmentVariable: String) { + if + let rawValue = getenv(environmentVariable), + let value = String(utf8String: rawValue) { + self = value + } else { + return nil + } + } + +} diff --git a/Tests/InstantSearchCoreTests/Misc/Data+FileAccess.swift b/Tests/InstantSearchCoreTests/Misc/Data+FileAccess.swift new file mode 100644 index 00000000..b0cbb303 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Misc/Data+FileAccess.swift @@ -0,0 +1,19 @@ +// +// Data+FileAccess.swift +// +// +// Created by Vladislav Fitc on 19/03/2020. +// + +import Foundation + +extension Data { + + init(filename: String) throws { + let thisSourceFile = URL(fileURLWithPath: #file) + let thisDirectory = thisSourceFile.deletingLastPathComponent() + let url = thisDirectory.appendingPathComponent(filename) + self = try Data(contentsOf: url) + } + +} diff --git a/Tests/InstantSearchCoreTests/Misc/DisjFacetingResult1.json b/Tests/InstantSearchCoreTests/Misc/DisjFacetingResult1.json new file mode 100644 index 00000000..e21db919 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Misc/DisjFacetingResult1.json @@ -0,0 +1,34 @@ +{ + "hits": [], + "nbHits": 596, + "page": 0, + "nbPages": 60, + "hitsPerPage": 10, + "processingTimeMS": 4, + "aroundLatLng": "48.856614,2.3522219", + "facets": { + "type": { + "book": 357, + "electronics": 184, + "office": 28, + "gifts": 27 + }, + }, + "facets_stats": { + }, + "exhaustiveFacetsCount": true, + "exhaustiveNbHits": true, + "query": "Amazon", + "params": "query=Amazon&hitsPerPage=10&page=0&analytics=false&attributesToRetrieve=*&highlightPreTag=%3Cais-highlight-0000000000%3E&highlightPostTag=%3C%2Fais-highlight-0000000000%3E&getRankingInfo=true&responseFields=*&optionalFilters=%5B%22categories%3AContemporary%20AND%20type%3Abook%20AND%20binding%3APaperback%22%5D&facets=*%2Ctype%2Cbrand%2Ccolor%2Cprice%2Cauthor%2Cbinding%2CpubYear%2Cfeatured%2Cpublisher%2Ccategories%2CproductGroup&tagFilters=", + "index": "ios@algolia.com#atis-prods", + "serverUsed": "d52-usw-3.algolia.net", + "indexUsed": "ios@algolia.com#atis-prods", + "parsedQuery": "amazon", + "timeoutCounts": false, + "timeoutHits": false, + "appliedRules": [ + { + "objectID": "1530792337749" + } + ] +} diff --git a/Tests/InstantSearchCoreTests/Misc/DisjFacetingResult2.json b/Tests/InstantSearchCoreTests/Misc/DisjFacetingResult2.json new file mode 100644 index 00000000..69d5553f --- /dev/null +++ b/Tests/InstantSearchCoreTests/Misc/DisjFacetingResult2.json @@ -0,0 +1,111 @@ +{ + "hits": [], + "nbHits": 596, + "page": 0, + "nbPages": 60, + "hitsPerPage": 10, + "processingTimeMS": 4, + "aroundLatLng": "48.856614,2.3522219", + "facets": { + "price": { + "1": 11, + "2": 24, + "3": 25, + "4": 30, + "5": 20, + "6": 25, + "7": 24, + "8": 17, + "9": 29, + "10": 17, + "11": 22, + "12": 32, + "13": 14, + "14": 33, + "15": 19, + "16": 24, + "17": 17, + "18": 24, + "19": 25, + "20": 7, + "21": 3, + "22": 7, + "23": 5, + "24": 14, + "25": 6, + "26": 1, + "27": 2, + "29": 20, + "30": 1, + "32": 2, + "34": 4, + "36": 1, + "37": 1, + "39": 13, + "40": 1, + "44": 2, + "45": 1, + "48": 2, + "49": 7, + "50": 3, + "52": 1, + "56": 2, + "59": 3, + "64": 1, + "65": 2, + "69": 1, + "79": 4, + "84": 2, + "89": 3, + "99": 6, + "109": 2, + "119": 3, + "121": 1, + "124": 1, + "129": 4, + "142": 1, + "179": 2, + "184": 1, + "199": 3, + "219": 1, + "229": 1, + "289": 2, + "299": 1, + "359": 1, + "399": 1, + "449": 1, + "599": 1, + "698": 1, + "1296": 1, + "11.99": 2, + "17.99": 1, + "21.99": 1, + "40.99": 1, + "49.99": 1, + "64.99": 1 + }, + }, + "facets_stats": { + "price": { + "min": 1, + "max": 1296, + "avg": 29.3204, + "sum": 17474.9 + }, + }, + "exhaustiveFacetsCount": true, + "exhaustiveNbHits": true, + "query": "Amazon", + "params": "query=Amazon&hitsPerPage=10&page=0&analytics=false&attributesToRetrieve=*&highlightPreTag=%3Cais-highlight-0000000000%3E&highlightPostTag=%3C%2Fais-highlight-0000000000%3E&getRankingInfo=true&responseFields=*&optionalFilters=%5B%22categories%3AContemporary%20AND%20type%3Abook%20AND%20binding%3APaperback%22%5D&facets=*%2Ctype%2Cbrand%2Ccolor%2Cprice%2Cauthor%2Cbinding%2CpubYear%2Cfeatured%2Cpublisher%2Ccategories%2CproductGroup&tagFilters=", + "index": "ios@algolia.com#atis-prods", + "serverUsed": "d52-usw-3.algolia.net", + "indexUsed": "ios@algolia.com#atis-prods", + "parsedQuery": "amazon", + "timeoutCounts": false, + "timeoutHits": false, + "appliedRules": [ + { + "objectID": "1530792337749" + } + ] +} diff --git a/Tests/InstantSearchCoreTests/Misc/DisjFacetingResult3.json b/Tests/InstantSearchCoreTests/Misc/DisjFacetingResult3.json new file mode 100644 index 00000000..240d6646 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Misc/DisjFacetingResult3.json @@ -0,0 +1,82 @@ +{ + "hits": [], + "nbHits": 596, + "page": 0, + "nbPages": 60, + "hitsPerPage": 10, + "processingTimeMS": 4, + "aroundLatLng": "48.856614,2.3522219", + "facets": { + "pubYear": { + "1900": 1, + "1970": 1, + "1973": 1, + "1974": 2, + "1975": 2, + "1976": 1, + "1977": 1, + "1978": 1, + "1979": 2, + "1980": 3, + "1981": 7, + "1982": 3, + "1983": 6, + "1984": 5, + "1985": 9, + "1986": 9, + "1987": 5, + "1988": 5, + "1989": 11, + "1990": 21, + "1991": 13, + "1992": 17, + "1993": 8, + "1994": 21, + "1995": 18, + "1996": 18, + "1997": 13, + "1998": 20, + "1999": 21, + "2000": 14, + "2001": 23, + "2002": 30, + "2003": 17, + "2004": 5, + "2005": 1, + "2006": 1, + "2007": 2, + "2008": 1, + "2009": 2, + "2010": 2, + "2011": 4, + "2012": 4, + "2013": 1, + "2014": 3, + "2015": 1, + "2016": 2 + }, + }, + "facets_stats": { + "pubYear": { + "min": 1900, + "max": 2016, + "avg": 1994, + "sum": 714206 + } + }, + "exhaustiveFacetsCount": true, + "exhaustiveNbHits": true, + "query": "Amazon", + "params": "query=Amazon&hitsPerPage=10&page=0&analytics=false&attributesToRetrieve=*&highlightPreTag=%3Cais-highlight-0000000000%3E&highlightPostTag=%3C%2Fais-highlight-0000000000%3E&getRankingInfo=true&responseFields=*&optionalFilters=%5B%22categories%3AContemporary%20AND%20type%3Abook%20AND%20binding%3APaperback%22%5D&facets=*%2Ctype%2Cbrand%2Ccolor%2Cprice%2Cauthor%2Cbinding%2CpubYear%2Cfeatured%2Cpublisher%2Ccategories%2CproductGroup&tagFilters=", + "index": "ios@algolia.com#atis-prods", + "serverUsed": "d52-usw-3.algolia.net", + "indexUsed": "ios@algolia.com#atis-prods", + "parsedQuery": "amazon", + "timeoutCounts": false, + "timeoutHits": false, + "appliedRules": [ + { + "objectID": "1530792337749" + } + ] +} diff --git a/Tests/InstantSearchCoreTests/Misc/SearchResultFacets.json b/Tests/InstantSearchCoreTests/Misc/SearchResultFacets.json new file mode 100644 index 00000000..5569d74a --- /dev/null +++ b/Tests/InstantSearchCoreTests/Misc/SearchResultFacets.json @@ -0,0 +1,16 @@ +{ + "hits": [], + "nbHits": 596, + "page": 0, + "nbPages": 60, + "hitsPerPage": 10, + "processingTimeMS": 4, + "facets": { + "type": { + "book": 357, + "electronics": 184, + "office": 28, + "gifts": 27 + } + }, +} diff --git a/Tests/InstantSearchCoreTests/Misc/disjunctive.json b/Tests/InstantSearchCoreTests/Misc/disjunctive.json new file mode 100644 index 00000000..0dd4034e --- /dev/null +++ b/Tests/InstantSearchCoreTests/Misc/disjunctive.json @@ -0,0 +1,70 @@ +[ + { + "category": "shoe", + "color": "red", + "promotions": [ + "free shipping", + "free return" + ] + }, + { + "category": "shoe", + "color": "green", + "promotions": [ + "free shipping", + "on sale" + ] + }, + { + "category": "shoe", + "color": "green", + "promotions": [ + "free return", + "on sale" + ] + }, + { + "category": "shirt", + "color": "blue", + "promotions": "coupon" + }, + { + "category": "shirt", + "color": "blue", + "promotions": [ + "free return", + "on sale" + ], + }, + { + "category": "shirt", + "color": "yellow", + "promotions": "free return" + }, + { + "category": "hat", + "color": "blue", + "promotions": "free return" + }, + { + "category": "hat", + "color": "yellow", + "promotions": "free shipping" + }, + { + "category": "hat", + "promotions": "free shipping" + }, + { + "category": "hat", + "color": "orange" + }, + { + "category": "pant", + "promotions": "free shipping", + }, + { + "category": "pant", + "color": "orange" + } +] diff --git a/Tests/InstantSearchCoreTests/Misc/disjunctiveHierarchical.json b/Tests/InstantSearchCoreTests/Misc/disjunctiveHierarchical.json new file mode 100644 index 00000000..76092206 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Misc/disjunctiveHierarchical.json @@ -0,0 +1,81 @@ +[ + { + "name": "Item 1", + "hierarchicalCategories": { + "lvl0": "Category1" + } + }, + { + "name": "Item 2", + "hierarchicalCategories": { + "lvl0": "Category2", + "lvl1": "Category2 > SubCategory1" + } + }, + { + "name": "Item 3", + "hierarchicalCategories": { + "lvl0": "Category2", + "lvl1": "Category2 > SubCategory2" + } + }, + { + "name": "Item 4", + "hierarchicalCategories": { + "lvl0": "Category3", + "lvl1": "Category3 > SubCategory1", + "lvl2": "Category3 > SubCategory1 > SubSubCategory1" + } + }, + { + "name": "Item 5", + "hierarchicalCategories": { + "lvl0": "Category3", + "lvl1": "Category3 > SubCategory1", + "lvl2": "Category3 > SubCategory1 > SubSubCategory2" + } + }, + { + "name": "Item 6", + "hierarchicalCategories": { + "lvl0": "Category3", + "lvl1": "Category3 > SubCategory2", + "lvl2": "Category3 > SubCategory2 > SubSubCategory1" + } + }, + { + "name": "Item 7", + "color": "red", + "hierarchicalCategories": { + "lvl0": "Category3", + "lvl1": "Category3 > SubCategory2", + "lvl2": "Category3 > SubCategory2 > SubSubCategory2" + } + }, + { + "name": "Item 8", + "color": "red", + "hierarchicalCategories": { + "lvl0": "Category2", + "lvl1": "Category2 > SubCategory1" + } + }, + { + "name": "Item 9", + "color": "blue", + "hierarchicalCategories": { + "lvl0": "Category3", + "lvl1": "Category3 > SubCategory2", + "lvl2": "Category3 > SubCategory2 > SubSubCategory2" + } + }, + { + "name": "Item 10", + "color": "red", + "hierarchicalCategories": { + "lvl0": "Category3", + "lvl1": "Category3 > SubCategory2", + "lvl2": "Category3 > SubCategory2 > SubSubCategory1" + } + } +] diff --git a/Tests/InstantSearchCoreTests/Misc/hierarchical.json b/Tests/InstantSearchCoreTests/Misc/hierarchical.json new file mode 100644 index 00000000..8300418f --- /dev/null +++ b/Tests/InstantSearchCoreTests/Misc/hierarchical.json @@ -0,0 +1,54 @@ +[ + { + "name": "Item 7", + "hierarchicalCategories": { + "lvl0": "Clothing", + "lvl1": "Clothing > Women", + "lvl2": "Clothing > Women > Shoes" + } + }, + { + "name": "Item 6", + "hierarchicalCategories": { + "lvl0": "Clothing", + "lvl1": "Clothing > Women", + "lvl2": "Clothing > Women > Bags" + } + }, + { + "name": "Item 5", + "hierarchicalCategories": { + "lvl0": "Clothing", + "lvl1": "Clothing > Men", + "lvl2": "Clothing > Men > Hats" + } + }, + { + "name": "Item 4", + "hierarchicalCategories": { + "lvl0": "Clothing", + "lvl1": "Clothing > Men", + "lvl2": "Clothing > Men > Shirt" + } + }, + { + "name": "Item 3", + "hierarchicalCategories": { + "lvl0": "Book", + "lvl1": "Book > Romance" + } + }, + { + "name": "Item 2", + "hierarchicalCategories": { + "lvl0": "Book", + "lvl1": "Book > Science Fiction" + } + }, + { + "name": "Item 1", + "hierarchicalCategories": { + "lvl0": "Furniture" + } + } +] diff --git a/Tests/InstantSearchCoreTests/Unit/AttributedStringWithTaggedStringTests.swift b/Tests/InstantSearchCoreTests/Unit/AttributedStringWithTaggedStringTests.swift new file mode 100644 index 00000000..e920cf44 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/AttributedStringWithTaggedStringTests.swift @@ -0,0 +1,77 @@ +// +// AttributedStringWithTaggedStringTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 14/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class AttributedStringWithTaggedStringTests: XCTestCase { + + #if os(iOS) || os(watchOS) || os(tvOS) + let color = UIColor.red + #elseif os(OSX) + let color = NSColor.red + #endif + + private func checkRanges(string: NSAttributedString, ranges: [NSRange: [NSAttributedString.Key: Any]]) { + string.enumerateAttributes(in: NSRange(location: 0, length: string.length), options: []) { attributes, range, _ in + guard let expectedAttributes = ranges[range] else { + XCTFail("Range [\(range.location), \(range.location + range.length)[ not expected") + return + } + // We cannot easily compare the dictionaries because values don't necessarily conform to `Equatable`. + // So we just verify that we have the same key set. For our purposes it's enough, because non highlighted + // ranges will have empty attributes. + XCTAssertEqual(Array(expectedAttributes.keys), Array(attributes.keys)) + } + } + + func testAttributedString() { + let input = "Woodstock is Snoopy's friend" + let highlightedString = HighlightedString(string: input) + let attributes: [NSAttributedString.Key: Any] = [ + .foregroundColor: color + ] + let attributedString = NSAttributedString(taggedString: highlightedString.taggedString, attributes: attributes) + checkRanges(string: attributedString, ranges: [ + NSRange(location: 0, length: 13): [:], + NSRange(location: 13, length: 6): attributes, + NSRange(location: 19, length: 9): [:] + ]) + } + + func testInvertedAttributedString() { + let input = "Woodstock is Snoopy's friend" + let highlightedString = HighlightedString(string: input) + let attributes: [NSAttributedString.Key: Any] = [ + .foregroundColor: color + ] + let attributedString = NSAttributedString(taggedString: highlightedString.taggedString, inverted: true, attributes: attributes) + checkRanges(string: attributedString, ranges: [ + NSRange(location: 0, length: 13): attributes, + NSRange(location: 13, length: 6): [:], + NSRange(location: 19, length: 9): attributes + ]) + } + + func testAttributedStringList() { + let input = ["aaabbbccc", "dddeeefff"].map(HighlightedString.init).map { $0.taggedString } + let attributes: [NSAttributedString.Key: Any] = [ + .foregroundColor: color + ] + let attributedString = NSAttributedString(taggedStrings: input, separator: NSAttributedString(string: ", "), attributes: attributes) + checkRanges(string: attributedString, ranges: [ + NSRange(location: 0, length: 3): [:], + NSRange(location: 3, length: 3): attributes, + NSRange(location: 6, length: 8): [:], + NSRange(location: 14, length: 3): attributes, + NSRange(location: 17, length: 3): [:] + ]) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/CurrentFilters/CurrentFiltersControllerConnectionTests.swift b/Tests/InstantSearchCoreTests/Unit/CurrentFilters/CurrentFiltersControllerConnectionTests.swift new file mode 100644 index 00000000..a6262403 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/CurrentFilters/CurrentFiltersControllerConnectionTests.swift @@ -0,0 +1,129 @@ +// +// CurrentFiltersControllerConnectionTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/01/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class CurrentFiltersControllerConnectionTests: XCTestCase { + + let attribute: Attribute = "Test Attribute" + let groupName = "Test group" + + weak var disposableInteractor: CurrentFiltersInteractor? + weak var disposableController: TestCurrentFiltersController? + + func testLeak() { + let interactor = CurrentFiltersInteractor() + let controller = TestCurrentFiltersController() + + disposableInteractor = interactor + disposableController = controller + + let connection = CurrentFiltersInteractor.ControllerConnection(interactor: interactor, controller: controller) + connection.connect() + } + + override func tearDown() { + XCTAssertNil(disposableInteractor, "Leaked interactor") + XCTAssertNil(disposableController, "Leaked controller") + } + + func testConnect() { + + let interactor = CurrentFiltersInteractor() + let controller = TestCurrentFiltersController() + + let connection = CurrentFiltersInteractor.ControllerConnection(interactor: interactor, controller: controller) + connection.connect() + + checkConnection(interactor: interactor, + controller: controller, + isConnected: true) + } + + func testConnectFunction() { + + let interactor = CurrentFiltersInteractor() + let controller = TestCurrentFiltersController() + + interactor.connectController(controller) + + checkConnection(interactor: interactor, + controller: controller, + isConnected: true) + + } + + func testDisconnect() { + let interactor = CurrentFiltersInteractor() + let controller = TestCurrentFiltersController() + + let connection = CurrentFiltersInteractor.ControllerConnection(interactor: interactor, + controller: controller) + connection.connect() + connection.disconnect() + + checkConnection(interactor: interactor, + controller: controller, + isConnected: false) + } + + func checkConnection(interactor: CurrentFiltersInteractor, + controller: TestCurrentFiltersController, + isConnected: Bool) { + checkUpdateControllerWhenItemsChanged(interactor: interactor, + controller: controller, + isConnected: isConnected) + checkItemsComputedOnRemove(interactor: interactor, + controller: controller, + isConnected: isConnected) + } + + func checkItemsComputedOnRemove(interactor: CurrentFiltersInteractor, + controller: TestCurrentFiltersController, + isConnected: Bool) { + + let item = FilterAndID(filter: .tag("tag"), id: .and(name: groupName)) + + interactor.items = Set([item]) + + let selectionsComputedExpectation = expectation(description: "selections computed") + selectionsComputedExpectation.isInverted = !isConnected + + interactor.onItemsComputed.subscribe(with: self) { (_, items) in + XCTAssertTrue(items.isEmpty) + selectionsComputedExpectation.fulfill() + } + + controller.onRemoveItem?(item) + + waitForExpectations(timeout: 5, handler: nil) + } + + func checkUpdateControllerWhenItemsChanged(interactor: CurrentFiltersInteractor, + controller: TestCurrentFiltersController, + isConnected: Bool) { + + let reloadExpectation = expectation(description: "reload expectation") + reloadExpectation.isInverted = !isConnected + + let expectedItems = [FilterAndID(filter: .tag("tag"), id: .and(name: groupName), text: "tag")] + + controller.didReload = { + XCTAssertEqual(controller.items, expectedItems) + reloadExpectation.fulfill() + } + + interactor.items = Set(expectedItems) + + waitForExpectations(timeout: 5, handler: nil) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/CurrentFilters/CurrentFiltersFilterStateConnectionTests.swift b/Tests/InstantSearchCoreTests/Unit/CurrentFilters/CurrentFiltersFilterStateConnectionTests.swift new file mode 100644 index 00000000..70762bb5 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/CurrentFilters/CurrentFiltersFilterStateConnectionTests.swift @@ -0,0 +1,129 @@ +// +// CurrentFiltersFilterStateConnectionTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/01/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class CurrentFiltersFilterStateConnectionTests: XCTestCase { + + let groupName = "Test group" + let filter = Filter.Facet(attribute: "Test Attribute", stringValue: "facet") + + weak var disposableInteractor: CurrentFiltersInteractor? + weak var disposableFilterState: FilterState? + + func testLeak() { + let interactor = CurrentFiltersInteractor() + let filterState = FilterState() + + disposableInteractor = interactor + disposableFilterState = filterState + + let connection = CurrentFiltersInteractor.FilterStateConnection(interactor: interactor, + filterState: filterState) + connection.connect() + } + + override func tearDown() { + XCTAssertNil(disposableInteractor, "Leaked interactor") + XCTAssertNil(disposableFilterState, "Leaked filterState") + } + + func testConnect() { + let interactor = CurrentFiltersInteractor() + let filterState = FilterState() + + let connection = CurrentFiltersInteractor.FilterStateConnection(interactor: interactor, + filterState: filterState) + connection.connect() + + checkConnection(interactor: interactor, + filterState: filterState, + isConnected: true) + checkBackConnection(interactor: interactor, + filterState: filterState, + isConnected: true) + } + + func testConnectFunction() { + + let interactor = CurrentFiltersInteractor() + let filterState = FilterState() + + interactor.connectFilterState(filterState) + + checkConnection(interactor: interactor, + filterState: filterState, + isConnected: true) + checkBackConnection(interactor: interactor, + filterState: filterState, + isConnected: true) + } + + func testDisconnect() { + + let interactor = CurrentFiltersInteractor() + let filterState = FilterState() + + let connection = CurrentFiltersInteractor.FilterStateConnection(interactor: interactor, filterState: filterState) + + connection.connect() + connection.disconnect() + + checkConnection(interactor: interactor, + filterState: filterState, + isConnected: false) + checkBackConnection(interactor: interactor, + filterState: filterState, + isConnected: false) + } + + func checkConnection(interactor: CurrentFiltersInteractor, + filterState: FilterState, + isConnected: Bool) { + + let itemsChangedExpectation = expectation(description: "items changed expectation") + itemsChangedExpectation.isInverted = !isConnected + + interactor.onItemsChanged.subscribe(with: self) { (test, filtersAndIDs) in + XCTAssertEqual(filtersAndIDs, [FilterAndID(filter: Filter(test.filter), id: .and(name: test.groupName))]) + itemsChangedExpectation.fulfill() + } + + filterState[and: groupName].removeAll() + filterState[and: groupName].add(filter) + filterState.notifyChange() + + waitForExpectations(timeout: 5) { _ in + interactor.onItemsChanged.cancelSubscription(for: self) + } + + } + + func checkBackConnection(interactor: CurrentFiltersInteractor, + filterState: FilterState, + isConnected: Bool) { + + let filterStateChangeExpectation = expectation(description: "filter state change") + filterStateChangeExpectation.isInverted = !isConnected + + filterState.onChange.subscribe(with: self) { (test, filterContainer) in + XCTAssertTrue(filterContainer[and: test.groupName].contains(test.filter)) + filterStateChangeExpectation.fulfill() + } + + interactor.add(item: FilterAndID(filter: Filter(filter), id: .and(name: groupName))) + + waitForExpectations(timeout: 5) { _ in + filterState.onChange.cancelSubscription(for: self) + } + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/CurrentFilters/TestCurrentFiltersController.swift b/Tests/InstantSearchCoreTests/Unit/CurrentFilters/TestCurrentFiltersController.swift new file mode 100644 index 00000000..7d1c4f09 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/CurrentFilters/TestCurrentFiltersController.swift @@ -0,0 +1,28 @@ +// +// TestCurrentFiltersController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/01/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class TestCurrentFiltersController: CurrentFiltersController { + + var items: [FilterAndID] = [] + + var didReload: (() -> Void)? + var onRemoveItem: ((FilterAndID) -> Void)? + + func setItems(_ items: [FilterAndID]) { + self.items = items + } + + func reload() { + self.didReload?() + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Decodable+InitWithFile.swift b/Tests/InstantSearchCoreTests/Unit/Decodable+InitWithFile.swift new file mode 100644 index 00000000..462c65e7 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Decodable+InitWithFile.swift @@ -0,0 +1,24 @@ +// +// Decodable+InitWithFile.swift +// InstantSearchCore-iOS +// +// Created by Vladislav Fitc on 03/06/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient + +extension Decodable { + + init(jsonFilename: String) throws { + let data = try Data(filename: jsonFilename) + self = try JSONDecoder().decode(Self.self, from: data) + } + + init(json: JSON) throws { + let data = try JSONEncoder().encode(json) + self = try JSONDecoder().decode(Self.self, from: data) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/DecodingErrorPrettyPrinterTests.swift b/Tests/InstantSearchCoreTests/Unit/DecodingErrorPrettyPrinterTests.swift new file mode 100644 index 00000000..50e836cd --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/DecodingErrorPrettyPrinterTests.swift @@ -0,0 +1,116 @@ +// +// DecodingErrorPrettyPrinterTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 12/02/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class DecodingErrorPrettyPrinterTests: XCTestCase { + + struct Person: Codable { + let name: String + let age: Int + } + + func testValueNotFound() { + + let data = """ + { + "name": "Alex Smith", + "age": null + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + + do { + _ = try decoder.decode(Person.self, from: data) + } catch let error { + guard let decodingError = error as? DecodingError else { + XCTFail("Unexpected error: \(error)") + return + } + let prettyPrinter = DecodingErrorPrettyPrinter(decodingError: decodingError) + XCTAssertEqual(prettyPrinter.description, "Decoding error: 'age': Expected Int value but found null instead.") + } + + } + + func testKeyNotFound() { + + let data = """ + { + "name": "Alex Smith", + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + + do { + _ = try decoder.decode(Person.self, from: data) + } catch let error { + guard let decodingError = error as? DecodingError else { + XCTFail("Unexpected error: \(error)") + return + } + let prettyPrinter = DecodingErrorPrettyPrinter(decodingError: decodingError) + XCTAssertEqual(prettyPrinter.description, "Decoding error: : Key not found: 'age'") + } + + } + + func testTypeMismatch() { + + let data = """ + { + "name": "Alex Smith", + "age": "AGE" + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + + do { + _ = try decoder.decode(Person.self, from: data) + } catch let error { + guard let decodingError = error as? DecodingError else { + XCTFail("Unexpected error: \(error)") + return + } + let prettyPrinter = DecodingErrorPrettyPrinter(decodingError: decodingError) + + XCTAssertEqual(prettyPrinter.description, "Decoding error: 'age': Type mismatch. Expected: Int") + + } + + } + + func testDataCorrupted() { + + let data = """ + { + ___ + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + + do { + _ = try decoder.decode(Person.self, from: data) + } catch let error { + guard let decodingError = error as? DecodingError else { + XCTFail("Unexpected error: \(error)") + return + } + let prettyPrinter = DecodingErrorPrettyPrinter(decodingError: decodingError) + XCTAssertEqual(prettyPrinter.description, "Decoding error: : The given data was not valid JSON.") + } + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/DisjunctiveFacetingsTests.swift b/Tests/InstantSearchCoreTests/Unit/DisjunctiveFacetingsTests.swift new file mode 100644 index 00000000..bb5adc5e --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/DisjunctiveFacetingsTests.swift @@ -0,0 +1,137 @@ +// +// DisjunctiveFacetingsTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 26/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore +import AlgoliaSearchClient + +class DisjunctiveFacetingTests: XCTestCase { + + class TestDelegate: DisjunctiveFacetingDelegate { + + let disjunctiveFacetsAttributes: Set + let filterGroups: [FilterGroupType] + + init(disjunctiveFacetsAttributes: Set, filterGroups: [FilterGroupType]) { + self.disjunctiveFacetsAttributes = disjunctiveFacetsAttributes + self.filterGroups = filterGroups + } + + func toFilterGroups() -> [FilterGroupType] { + return filterGroups + } + + } + + func testMergeResults() { + + let query = Query() + + let queryBuilder = QueryBuilder(query: query, filterGroups: [ + FilterGroup.Or(filters: [Filter.Facet(attribute: "price", floatValue: 100)], name: "price"), + FilterGroup.Or(filters: [Filter.Facet(attribute: "pubYear", floatValue: 2000)], name: "pubYear") + ]) + + let res1 = try! SearchResponse(jsonFilename: "DisjFacetingResult1.json") + let res2 = try! SearchResponse(jsonFilename: "DisjFacetingResult2.json") + let res3 = try! SearchResponse(jsonFilename: "DisjFacetingResult3.json") + + do { + let output = try queryBuilder.aggregate([res1, res2, res3]) + XCTAssertEqual(output.facetStats?.count, 2) + XCTAssertEqual(output.disjunctiveFacets?.count, 2) + XCTAssertEqual(output.disjunctiveFacets?.map { $0.key }.contains("price"), true) + XCTAssertEqual(output.disjunctiveFacets?.map { $0.key }.contains("pubYear"), true) + + } catch let error { + + XCTFail("\(error)") + } + + } + + func testMultipleDisjunctiveGroupsOfSameType() { + + let query = Query() + + let colorGroup = FilterGroup.Or(filters: [.init(attribute: "color", stringValue: "red"), .init(attribute: "color", stringValue: "green")], name: "color") + let sizeGroup = FilterGroup.Or(filters: [.init(attribute: "size", stringValue: "m"), .init(attribute: "size", stringValue: "s")], name: "size") + + let filterGroups: [FilterGroupType] = [colorGroup, sizeGroup] + + let queryBuilder = QueryBuilder(query: query, filterGroups: filterGroups) + + let queries = queryBuilder.build() + + let andQuery = queries.first! + XCTAssertNil(andQuery.facets) + XCTAssertEqual(andQuery.filters, """ + ( "color":"red" OR "color":"green" ) AND ( "size":"m" OR "size":"s" ) + """) + + for query in queries[1...] { + switch query.facets { + case ["size"]: + XCTAssertEqual(query.filters, """ + ( "color":"red" OR "color":"green" ) + """) + + case ["color"]: + XCTAssertEqual(query.filters, """ + ( "size":"m" OR "size":"s" ) + """) + + default: + XCTFail("Unexpected case") + } + } + + } + + func testBuildHierarchicalQueries() { + + let query = Query() + + let colorGroup = FilterGroup.And(filters: [Filter.Facet(attribute: "color", stringValue: "red")], name: "color") + + let hierarchicalGroup = FilterGroup.And(filters: [Filter.Facet(attribute: "category.lvl2", stringValue: "a > b > c")], name: "h") + + let filterGroups: [FilterGroupType] = [colorGroup, hierarchicalGroup] + + let hierarchicalAttributes = (0...3) + .map { "category.lvl\($0)" } + .map(Attribute.init(rawValue:)) + + let hierarchicalFilters: [Filter.Facet] = [ + .init(attribute: "category.lvl0", stringValue: "a"), + .init(attribute: "category.lvl1", stringValue: "a > b"), + .init(attribute: "category.lvl2", stringValue: "a > b > c") + ] + + let queryBuilder = QueryBuilder(query: query, filterGroups: filterGroups, hierarchicalAttributes: hierarchicalAttributes, hierachicalFilters: hierarchicalFilters) + + let queries = queryBuilder.build() + + XCTAssertEqual(queries.count, hierarchicalAttributes.count + 1) + + XCTAssertEqual(queries[1].filters, "( \"color\":\"red\" )") + XCTAssertEqual(queries[1].facets, ["category.lvl0"]) + + XCTAssertEqual(queries[2].filters, "( \"color\":\"red\" ) AND ( \"category.lvl0\":\"a\" )") + XCTAssertEqual(queries[2].facets, ["category.lvl1"]) + + XCTAssertEqual(queries[3].filters, "( \"color\":\"red\" ) AND ( \"category.lvl1\":\"a > b\" )") + XCTAssertEqual(queries[3].facets, ["category.lvl2"]) + + XCTAssertEqual(queries[4].filters, "( \"color\":\"red\" ) AND ( \"category.lvl2\":\"a > b > c\" )") + XCTAssertEqual(queries[4].facets, ["category.lvl3"]) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/FacetList/Array+Facet.swift b/Tests/InstantSearchCoreTests/Unit/FacetList/Array+Facet.swift new file mode 100644 index 00000000..cde80575 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/FacetList/Array+Facet.swift @@ -0,0 +1,20 @@ +// +// Array+Facet.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 06/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore + +extension Array where Element == Facet { + + init(prefix: String, count: Int) { + self = (0.. Void)? + var didReload: (() -> Void)? + var selectableItems: [(item: Facet, isSelected: Bool)] = [] + + func setSelectableItems(selectableItems: [(item: Facet, isSelected: Bool)]) { + self.selectableItems = selectableItems + } + + func reload() { + didReload?() + } + + } + + func testFacetListInteractorConstructor() { + let defaultMultipleSelectionInteractor = FacetListInteractor() + XCTAssertEqual(defaultMultipleSelectionInteractor.selectionMode, .multiple) + + let singleSelectionFacetInteractor = FacetListInteractor(selectionMode: .single) + XCTAssertEqual(singleSelectionFacetInteractor.selectionMode, .single) + + let multipleSelectionFacetInteractor = FacetListInteractor(selectionMode: .multiple) + XCTAssertEqual(multipleSelectionFacetInteractor.selectionMode, .multiple) + } + + func testConnectFilterState() { + + let interactor = FacetListInteractor(selectionMode: .single) + + interactor.items = [ + Facet(value: "cat1", count: 10, highlighted: nil), + Facet(value: "cat2", count: 5, highlighted: nil), + Facet(value: "cat3", count: 5, highlighted: nil) + ] + + let filterState = FilterState() + + interactor.connectFilterState(filterState, with: "categories", operator: .and) + + let groupID: FilterGroup.ID = .and(name: "categories") + + // Interactor -> FilterState + interactor.computeSelections(selectingItemForKey: "cat1") + + XCTAssertTrue(filterState.contains(Filter.Facet(attribute: "categories", stringValue: "cat1"), inGroupWithID: groupID)) + + // FilterState -> Interactor + + filterState.notify(.add(filter: Filter.Facet(attribute: "categories", stringValue: "cat2"), toGroupWithID: groupID)) + + XCTAssertEqual(interactor.selections, ["cat1", "cat2"]) + + } + + func testConnectSearcher() { + let interactor = FacetListInteractor(selectionMode: .single) + + let query = Query() + let searcher = SingleIndexSearcher(client: .init(appID: "", apiKey: ""), indexName: "", query: query) + + interactor.connectSearcher(searcher, with: "type") + + do { + let results = try SearchResponse(jsonFilename: "SearchResultFacets.json") + + searcher.onResults.fire(results) + + let expectedFacets: Set = [ + .init(value: "book", count: 357, highlighted: nil), + .init(value: "electronics", count: 184, highlighted: nil), + .init(value: "gifts", count: 27, highlighted: nil), + .init(value: "office", count: 28, highlighted: nil) + ] + + XCTAssertEqual(Set(interactor.items), expectedFacets) + + } catch let error { + XCTFail(error.localizedDescription) + } + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/FacetList/FacetListPresenterTests.swift b/Tests/InstantSearchCoreTests/Unit/FacetList/FacetListPresenterTests.swift new file mode 100644 index 00000000..fdf59dd0 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/FacetList/FacetListPresenterTests.swift @@ -0,0 +1,304 @@ +// +// FacetListInteractorTests.swift +// InstantSearchCore +// +// Created by Guy Daher on 18/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class FacetListPresenterTests: XCTestCase { + + func testCountDescSelectedOnTop() { + + let initial: [SelectableItem] = [ + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true) + ] + + let expected: [SelectableItem] = [ + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "green", count: 0, highlighted: nil), true), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false) + ] + + let refinementFacetsPresenter = FacetListPresenter(sortBy: [.isRefined, .count(order: .descending)]) + let actual = refinementFacetsPresenter.transform(refinementFacets: initial) + + XCTAssertEqual(expected.map { $0.item }, actual.map { $0.item }) + + } + + func testCountDescNotSelectedOnTop() { + + let initial: [SelectableItem] = [ + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true) + ] + + let expected: [SelectableItem] = [ + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true) + ] + + let refinementFacetsPresenter = FacetListPresenter(sortBy: [ .count(order: .descending)]) + + let actual = refinementFacetsPresenter.transform(refinementFacets: initial) + + XCTAssertEqual(expected.map { $0.item }, actual.map { $0.item }) + + } + + func testCountAscSelectedOnTop() { + + let initial: [SelectableItem] = [ + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true) + ] + + let expected: [SelectableItem] = [ + (.init(value: "green", count: 0, highlighted: nil), true), + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false) + ] + + let refinementFacetsPresenter = FacetListPresenter(sortBy: [.isRefined, .count(order: .ascending)]) + let actual = refinementFacetsPresenter.transform(refinementFacets: initial) + + XCTAssertEqual(expected.map { $0.item }, actual.map { $0.item }) + + } + + func testCountAscNotSelectedOnTop() { + + let initial: [SelectableItem] = [ + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true) + ] + + let expected: [SelectableItem] = [ + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true) + ] + + let refinementFacetsPresenter = FacetListPresenter(sortBy: [ .count(order: .descending)]) + + let actual = refinementFacetsPresenter.transform(refinementFacets: initial) + + XCTAssertEqual(expected.map { $0.item }, actual.map { $0.item }) + } + + func testNameAscSelectedOnTop() { + + let initial: [SelectableItem] = [ + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true) + ] + + let expected: [SelectableItem] = [ + (.init(value: "green", count: 0, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "yellow", count: 30, highlighted: nil), false) + ] + + let refinementFacetsPresenter = FacetListPresenter(sortBy: [.isRefined, .alphabetical(order: .ascending)]) + let actual = refinementFacetsPresenter.transform(refinementFacets: initial) + + XCTAssertEqual(expected.map { $0.item }, actual.map { $0.item }) + } + + func testNameAscNotSelectedOnTop() { + + let initial: [SelectableItem] = [ + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true) + ] + + let expected: [SelectableItem] = [ + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "yellow", count: 30, highlighted: nil), false) + ] + + let refinementFacetsPresenter = FacetListPresenter(sortBy: [.alphabetical(order: .ascending)]) + let actual = refinementFacetsPresenter.transform(refinementFacets: initial) + + XCTAssertEqual(expected.map { $0.item }, actual.map { $0.item }) + } + + func testNameDescSelectedOnTop() { + + let initial: [SelectableItem] = [ + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true) + ] + + let expected: [SelectableItem] = [ + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "green", count: 0, highlighted: nil), true), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false) + ] + + let refinementFacetsPresenter = FacetListPresenter(sortBy: [.isRefined, .alphabetical(order: .descending)]) + let actual = refinementFacetsPresenter.transform(refinementFacets: initial) + + XCTAssertEqual(expected.map { $0.item }, actual.map { $0.item }) + } + + func testNameDescNotSelectedOnTop() { + let initial: [SelectableItem] = [ + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "green", count: 0, highlighted: nil), true) + ] + + let expected: [SelectableItem] = [ + (.init(value: "yellow", count: 30, highlighted: nil), false), + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "orange", count: 20, highlighted: nil), true), + (.init(value: "green", count: 0, highlighted: nil), true), + (.init(value: "blue", count: 40, highlighted: nil), false), + (.init(value: "black", count: 5, highlighted: nil), false) + ] + + let refinementFacetsPresenter = FacetListPresenter(sortBy: [.alphabetical(order: .descending)]) + let actual = refinementFacetsPresenter.transform(refinementFacets: initial) + + XCTAssertEqual(expected.map { $0.item }, actual.map { $0.item }) + } + +// func testRemoveSelectedValuesWithZeroCount() { +// var expectedList: [Facet] = [] +// +// expectedList.append(Facet(value: "yellow", count: 30, highlighted: nil)) +// expectedList.append(Facet(value: "red", count: 10, highlighted: nil)) +// expectedList.append(Facet(value: "orange", count: 20, highlighted: nil)) +// expectedList.append(Facet(value: "blue", count: 40, highlighted: nil)) +// expectedList.append(Facet(value: "black", count: 5, highlighted: nil)) +// +// let actualList: [Facet] = refinementListBuilder.getRefinementList(selectedValues: selectedValues, +// resultValues: facetValues, +// sortBy: [.alphabetical(order: .descending)], +// keepSelectedValuesWithZeroCount: false) +// +// XCTAssertEqual(expectedList, actualList) +// } + + func testSortWithEqualCounts() { + + let initial: [SelectableItem] = [ + (.init(value: "blue", count: 10, highlighted: nil), false), + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "green", count: 5, highlighted: nil), true), + (.init(value: "orange", count: 10, highlighted: nil), true) + ] + + let expected: [SelectableItem] = [ + (.init(value: "orange", count: 10, highlighted: nil), true), + (.init(value: "red", count: 10, highlighted: nil), true), + (.init(value: "green", count: 5, highlighted: nil), true), + (.init(value: "blue", count: 10, highlighted: nil), false) + ] + + let refinementFacetsPresenter = FacetListPresenter(sortBy: [.isRefined, .count(order: .descending), .alphabetical(order: .ascending)]) + let actual = refinementFacetsPresenter.transform(refinementFacets: initial) + + XCTAssertEqual(expected.map { $0.item }, actual.map { $0.item }) + + } + +// func testMergeWithFacetAndRefinementValues() { +// let actualList = refinementListBuilder.merge(facetValues, withRefinementValues: selectedValues) +// +// var expectedList: [Facet] = [] +// expectedList.append(contentsOf: facetValues) +// expectedList.append(Facet(value: "green", count: 0, highlighted: nil)) // The missing one, put count 0 +// +// XCTAssertEqual(expectedList, actualList) +// } +// +// func testMergeWithRefinementValues() { +// let actualList = refinementListBuilder.merge([], withRefinementValues: selectedValues) +// +// var expectedList: [Facet] = [] +// expectedList.append(Facet(value: "orange", count: 0, highlighted: nil)) +// expectedList.append(Facet(value: "red", count: 0, highlighted: nil)) +// expectedList.append(Facet(value: "green", count: 0, highlighted: nil)) // The missing one, put count 0 +// +// XCTAssertEqual(expectedList, actualList) +// } +// +// func testMergeWithFacets() { +// let actualList = refinementListBuilder.merge(facetValues, withRefinementValues: []) +// +// var expectedList: [Facet] = [] +// expectedList.append(contentsOf: facetValues) +// +// XCTAssertEqual(expectedList, actualList) +// } +// +// func testMergeWithEmptyValues() { +// let actualList = refinementListBuilder.merge([], withRefinementValues: []) +// +// let expectedList: [Facet] = [] +// +// XCTAssertEqual(expectedList, actualList) +// } +} diff --git a/Tests/InstantSearchCoreTests/Unit/FacetList/FacetListSingleIndexSearcherConnectionTests.swift b/Tests/InstantSearchCoreTests/Unit/FacetList/FacetListSingleIndexSearcherConnectionTests.swift new file mode 100644 index 00000000..72af2fb2 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/FacetList/FacetListSingleIndexSearcherConnectionTests.swift @@ -0,0 +1,116 @@ +// +// FacetListSingleIndexSearcherConnectionTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class FacetListSingleIndexSearcherConnectionTests: XCTestCase { + + let attribute: Attribute = "Test Attribute" + let facets: [Facet] = .init(prefix: "v", count: 3) + + weak var disposableSearcher: SingleIndexSearcher? + weak var disposableInteractor: FacetListInteractor? + + func testLeak() { + let searcher = SingleIndexSearcher(appID: "", apiKey: "", indexName: "") + let interactor = FacetListInteractor() + + disposableSearcher = searcher + disposableInteractor = interactor + + let connection = FacetListInteractor.SingleIndexSearcherConnection(facetListInteractor: interactor, searcher: searcher, attribute: attribute) + connection.connect() + } + + override func tearDown() { + XCTAssertNil(disposableSearcher, "Leaked searcher") + XCTAssertNil(disposableInteractor, "Leaked interactor") + } + + func testConnect() { + let searcher = SingleIndexSearcher(appID: "", apiKey: "", indexName: "") + let interactor = FacetListInteractor() + + let connection = FacetListInteractor.SingleIndexSearcherConnection(facetListInteractor: interactor, searcher: searcher, attribute: attribute) + connection.connect() + + checkConnection(interactor: interactor, + searcher: searcher, + isConnected: true) + } + + func testConnectMethod() { + let searcher = SingleIndexSearcher(appID: "", apiKey: "", indexName: "") + let interactor = FacetListInteractor() + + interactor.connectSearcher(searcher, with: attribute) + + checkConnection(interactor: interactor, + searcher: searcher, + isConnected: true) + } + + func testDisconnect() { + let searcher = SingleIndexSearcher(appID: "", apiKey: "", indexName: "") + let interactor = FacetListInteractor() + + let connection = FacetListInteractor.SingleIndexSearcherConnection(facetListInteractor: interactor, searcher: searcher, attribute: attribute) + connection.connect() + connection.disconnect() + + checkConnection(interactor: interactor, + searcher: searcher, + isConnected: false) + } + + func checkConnection(interactor: FacetListInteractor, + searcher: SingleIndexSearcher, + isConnected: Bool) { + var results = SearchResponse(hits: [TestRecord]()) + results.disjunctiveFacets = [attribute: facets] + + let onItemsChangedExpectation = expectation(description: "on items changed") + onItemsChangedExpectation.isInverted = !isConnected + + interactor.onItemsChanged.subscribe(with: self) { (test, facets) in + XCTAssertEqual(test.facets, facets) + onItemsChangedExpectation.fulfill() + } + + searcher.onResults.fire(results) + + waitForExpectations(timeout: 5, handler: .none) + } + +} + +extension SearchResponse { + + init(hits: [E]) { + let hitsJSON: JSON = try! .array(hits.map(JSON.init)) + try! self.init(json: ["hits": hitsJSON]) + } + +} + +extension Hit where T == [String: JSON] { + + init(object: [String: JSON], objectID: ObjectID? = nil) { + var mutableCopy = object + let objectID = objectID ?? ObjectID(rawValue: UUID().uuidString) + mutableCopy["objectID"] = .string(objectID.rawValue) + try! self.init(json: .dictionary(mutableCopy)) + } + + static func withJSON(_ json: [String: JSON]) -> Self { + .init(object: json) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/FacetList/TestFacetListController.swift b/Tests/InstantSearchCoreTests/Unit/FacetList/TestFacetListController.swift new file mode 100644 index 00000000..452a924d --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/FacetList/TestFacetListController.swift @@ -0,0 +1,26 @@ +// +// TestFacetListController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 06/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore + +class TestFacetListController: FacetListController { + + var onClick: ((Facet) -> Void)? + var didReload: (() -> Void)? + var selectableItems: [(item: Facet, isSelected: Bool)] = [] + + func setSelectableItems(selectableItems: [(item: Facet, isSelected: Bool)]) { + self.selectableItems = selectableItems + } + + func reload() { + didReload?() + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/FilterState/FilterGroupCollectionsTests.swift b/Tests/InstantSearchCoreTests/Unit/FilterState/FilterGroupCollectionsTests.swift new file mode 100644 index 00000000..8b5897b3 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/FilterState/FilterGroupCollectionsTests.swift @@ -0,0 +1,36 @@ +// +// FilterGroupCollectionsTests.swift +// AlgoliaSearch +// +// Created by Vladislav Fitc on 09/04/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class FilterGroupCollectionsTests: XCTestCase { + + func testConversion() { + + let andGroup = FilterGroup.And(filters: [ + Filter.Tag(value: "tag"), + Filter.Numeric(attribute: "size", operator: .equals, value: 40), + Filter.Facet(attribute: "brand", stringValue: "sony") + ] as [FilterType]) + + let orGroup = FilterGroup.Or(filters: [ + Filter.Facet(attribute: "brand", stringValue: "philips"), + Filter.Facet(attribute: "diagonal", floatValue: 42), + Filter.Facet(attribute: "featured", boolValue: true) + ]) + + let converter = FilterGroupConverter() + let groups: [FilterGroupType] = [andGroup, orGroup] + + XCTAssertEqual(converter.sql(groups), "( \"_tags\":\"tag\" AND \"size\" = 40.0 AND \"brand\":\"sony\" ) AND ( \"brand\":\"philips\" OR \"diagonal\":\"42.0\" OR \"featured\":\"true\" )") + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/FilterState/FilterGroupTests.swift b/Tests/InstantSearchCoreTests/Unit/FilterState/FilterGroupTests.swift new file mode 100644 index 00000000..7771f137 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/FilterState/FilterGroupTests.swift @@ -0,0 +1,60 @@ +// +// FilterGroupTests.swift +// AlgoliaSearch OSX +// +// Created by Vladislav Fitc on 16/01/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class FilterGroupTests: XCTestCase { + + func testAndGroupSingle() { + + let group = FilterGroup.And(filters: [ + Filter.Tag(value: "tag") + ]) + + XCTAssertEqual(group.sqlForm, "( \"_tags\":\"tag\" )") + + } + + func testAndGroupMultiple() { + + let filters: [FilterType] = [ + Filter.Tag(value: "tag"), + Filter.Numeric(attribute: "size", operator: .equals, value: 40), + Filter.Facet(attribute: "brand", stringValue: "sony") + ] + let group = FilterGroup.And(filters: filters) + + XCTAssertEqual(group.sqlForm, "( \"_tags\":\"tag\" AND \"size\" = 40.0 AND \"brand\":\"sony\" )") + + } + + func testOrGroupSingle() { + + let group = FilterGroup.Or(filters: [Filter.Facet(attribute: "brand", stringValue: "philips")]) + + XCTAssertEqual(group.sqlForm, "( \"brand\":\"philips\" )") + + } + + func testOrGroupMultiple() { + + let filters = [ + Filter.Facet(attribute: "brand", stringValue: "philips"), + Filter.Facet(attribute: "diagonal", floatValue: 42), + Filter.Facet(attribute: "featured", boolValue: true) + ] + + let group = FilterGroup.Or(filters: filters) + + XCTAssertEqual(group.sqlForm, "( \"brand\":\"philips\" OR \"diagonal\":\"42.0\" OR \"featured\":\"true\" )") + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/FilterState/FilterStateGroupTests.swift b/Tests/InstantSearchCoreTests/Unit/FilterState/FilterStateGroupTests.swift new file mode 100644 index 00000000..eb091761 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/FilterState/FilterStateGroupTests.swift @@ -0,0 +1,43 @@ +// +// FilterStateGroupTests.swift +// AlgoliaSearch OSX +// +// Created by Vladislav Fitc on 22/01/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class FilterStateGroupTests: XCTestCase { + + func testOrGroupAddAll() { + var filterState = GroupsStorage() + let group = FilterGroup.ID.or(name: "group", filterType: .facet) + let filter1 = Filter.Facet(attribute: "category", value: "table") + let filter2 = Filter.Facet(attribute: "category", value: "chair") + filterState.addAll(filters: [filter1, filter2], toGroupWithID: group) + XCTAssertTrue(filterState.contains(filter1)) + XCTAssertTrue(filterState.contains(filter2)) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "category":"chair" OR "category":"table" ) + """) + } + + func testAndGroupAddAll() { + var filterState = GroupsStorage() + let group = FilterGroup.ID.and(name: "group") + let filterPrice = Filter.Numeric(attribute: "price", operator: .greaterThan, value: 10) + let filterSize = Filter.Numeric(attribute: "size", operator: .greaterThan, value: 20) + filterState.addAll(filters: [filterPrice, filterSize], toGroupWithID: group) + XCTAssertTrue(filterState.contains(filterPrice)) + XCTAssertTrue(filterState.contains(filterSize)) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "price" > 10.0 AND "size" > 20.0 ) + """) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/FilterState/FilterStateTests.swift b/Tests/InstantSearchCoreTests/Unit/FilterState/FilterStateTests.swift new file mode 100644 index 00000000..f9bc7f99 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/FilterState/FilterStateTests.swift @@ -0,0 +1,567 @@ +// +// FilterStateTests.swift +// AlgoliaSearch +// +// Created by Guy Daher on 10/12/2018. +// Copyright © 2018 Algolia. All rights reserved. +// + +import Foundation + +@testable import InstantSearchCore +import XCTest + +extension FilterGroupsConvertible { + + func buildSQL() -> String? { + return FilterGroupConverter().sql(toFilterGroups()) + } + +} + +class FilterStateTests: XCTestCase { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testPlayground() { + + var filterState = GroupsStorage() + let filterFacet1 = Filter.Facet(attribute: "category", value: "table") + let filterFacet2 = Filter.Facet(attribute: "category", value: "chair") + let filterNumeric1 = Filter.Numeric(attribute: "price", operator: .greaterThan, value: 10) + let filterNumeric2 = Filter.Numeric(attribute: "price", operator: .lessThan, value: 20) + let filterTag1 = Filter.Tag(value: "Tom") + let filterTag2 = Filter.Tag(value: "Hank") + + let groupFacets = FilterGroup.ID.or(name: "filterFacets", filterType: .facet) + let groupFacetsOtherInstance = FilterGroup.ID.or(name: "filterFacets", filterType: .facet) + let groupNumerics = FilterGroup.ID.and(name: "filterNumerics") + let groupTagsOr = FilterGroup.ID.or(name: "filterTags", filterType: .tag) + let groupTagsAnd = FilterGroup.ID.and(name: "filterTags") + + filterState.add(filterFacet1, toGroupWithID: groupFacets) + // Make sure that if we re-create a group instance, filters will stay in same group bracket + filterState.add(filterFacet2, toGroupWithID: groupFacetsOtherInstance) + + filterState.add(filterNumeric1, toGroupWithID: groupNumerics) + filterState.add(filterNumeric2, toGroupWithID: groupNumerics) + // Repeat once to see if the Set rejects same filter + filterState.add(filterNumeric2, toGroupWithID: groupNumerics) + + filterState.addAll(filters: [filterTag1, filterTag2], toGroupWithID: groupTagsOr) + filterState.add(filterTag1, toGroupWithID: groupTagsAnd) + let expectedState = """ +( "category":"chair" OR "category":"table" ) AND ( "price" < 20.0 AND "price" > 10.0 ) AND ( "_tags":"Hank" OR "_tags":"Tom" ) AND ( "_tags":"Tom" ) +""" + XCTAssertEqual(filterState.buildSQL(), expectedState) + + XCTAssertTrue(filterState.contains(filterFacet1)) + + let missingFilter = Filter.Facet(attribute: "bla", value: false) + XCTAssertFalse(filterState.contains(missingFilter)) + + filterState.remove(filterTag1, fromGroupWithID: groupTagsAnd) // existing one + filterState.remove(filterTag1, fromGroupWithID: groupTagsAnd) // remove one more time + filterState.remove(Filter.Tag(value: "unexisting"), fromGroupWithID: groupTagsOr) // remove one that does not exist + filterState.remove(filterFacet1) // Remove in all groups + + let expectedFilterState2 = """ + ( "category":"chair" ) AND ( "price" < 20.0 AND "price" > 10.0 ) AND ( "_tags":"Hank" OR "_tags":"Tom" ) + """ + XCTAssertEqual(filterState.buildSQL(), expectedFilterState2) + + filterState.removeAll([filterNumeric1, filterNumeric2]) + + let expectedFilterState3 = """ + ( "category":"chair" ) AND ( "_tags":"Hank" OR "_tags":"Tom" ) + """ + XCTAssertEqual(filterState.buildSQL(), expectedFilterState3) + + } + + func testInversion() { + + var filterState = GroupsStorage() + + filterState.addAll(filters: [ + Filter.Tag(value: "tagA", isNegated: true), + Filter.Tag(value: "tagB", isNegated: true)], toGroupWithID: .or(name: "a", filterType: .tag)) + + filterState.addAll(filters: [ + Filter.Facet(attribute: "size", value: 40, isNegated: true), + Filter.Facet(attribute: "featured", value: true, isNegated: true) + ], toGroupWithID: .or(name: "b", filterType: .facet)) + + let expectedState = "( NOT \"_tags\":\"tagA\" OR NOT \"_tags\":\"tagB\" ) AND ( NOT \"featured\":\"true\" OR NOT \"size\":\"40.0\" )" + + XCTAssertEqual(filterState.buildSQL(), expectedState) + + } + + func testCopyConstructor() { + + let filterState = FilterState() + + filterState[and: "a"].add(Filter.Facet(attribute: "f1", boolValue: true), Filter.Numeric(attribute: "f2", range: 0...10)) + filterState[or: "b"].add(Filter.Tag(value: "t1"), Filter.Tag(value: "t2")) + filterState[hierarchical: "c"].add(.init(attribute: "f", stringValue: "test")) + + let filterStateCopy = FilterState(filterState) + + XCTAssert(filterStateCopy[and: "a"].contains(Filter.Facet(attribute: "f1", boolValue: true))) + XCTAssert(filterStateCopy[and: "a"].contains(Filter.Numeric(attribute: "f2", range: 0...10))) + XCTAssert(filterStateCopy[or: "b"].contains(Filter.Tag(value: "t1"))) + XCTAssert(filterStateCopy[or: "b"].contains(Filter.Tag(value: "t2"))) + XCTAssert(filterStateCopy[hierarchical: "c"].contains(.init(attribute: "f", stringValue: "test"))) + + } + + func testSetWithContent() { + + let filterState = FilterState() + + filterState[and: "a"].add(Filter.Facet(attribute: "f1", boolValue: true), Filter.Numeric(attribute: "f2", range: 0...10)) + filterState[or: "b"].add(Filter.Tag(value: "t1"), Filter.Tag(value: "t2")) + filterState[hierarchical: "c"].add(.init(attribute: "f", stringValue: "test")) + + let anotherFilterState = FilterState() + anotherFilterState.setWithContent(of: filterState) + + XCTAssert(anotherFilterState[and: "a"].contains(Filter.Facet(attribute: "f1", boolValue: true))) + XCTAssert(anotherFilterState[and: "a"].contains(Filter.Numeric(attribute: "f2", range: 0...10))) + XCTAssert(anotherFilterState[or: "b"].contains(Filter.Tag(value: "t1"))) + XCTAssert(anotherFilterState[or: "b"].contains(Filter.Tag(value: "t2"))) + XCTAssert(anotherFilterState[hierarchical: "c"].contains(.init(attribute: "f", stringValue: "test"))) + + } + + func testAdd() { + + var filterState = GroupsStorage() + + let filterFacet1 = Filter.Facet(attribute: Attribute("category"), value: "table") + let filterFacet2 = Filter.Facet(attribute: Attribute("category"), value: "chair") + let filterNumeric1 = Filter.Numeric(attribute: "price", operator: .greaterThan, value: 10) + let filterNumeric2 = Filter.Numeric(attribute: "price", operator: .lessThan, value: 20) + let filterTag1 = Filter.Tag(value: "Tom") + let filterTag2 = Filter.Tag(value: "Hank") + + let groupFacets: FilterGroup.ID = .or(name: "filterFacets", filterType: .facet) + let groupFacetsOtherInstance: FilterGroup.ID = .or(name: "filterFacets", filterType: .facet) + let groupNumerics: FilterGroup.ID = .and(name: "filterNumerics") + let groupTagsOr: FilterGroup.ID = .or(name: "filterTags", filterType: .tag) + let groupTagsAnd: FilterGroup.ID = .and(name: "filterTags") + + filterState.add(filterFacet1, toGroupWithID: groupFacets) + // Make sure that if we re-create a group instance, filters will stay in same group bracket + filterState.add(filterFacet2, toGroupWithID: groupFacetsOtherInstance) + + filterState.add(filterNumeric1, toGroupWithID: groupNumerics) + filterState.add(filterNumeric2, toGroupWithID: groupNumerics) + // Repeat once to see if the Set rejects same filter + filterState.add(filterNumeric2, toGroupWithID: groupNumerics) + + filterState.addAll(filters: [filterTag1, filterTag2], toGroupWithID: groupTagsOr) + filterState.add(filterTag1, toGroupWithID: groupTagsAnd) + + let expectedState = """ + ( "category":"chair" OR "category":"table" ) AND ( "price" < 20.0 AND "price" > 10.0 ) AND ( "_tags":"Hank" OR "_tags":"Tom" ) AND ( "_tags":"Tom" ) + """ + + XCTAssertEqual(filterState.buildSQL(), expectedState) + + } + + func testContains() { + + var filterState = GroupsStorage() + + let tagA = Filter.Tag(value: "A") + let tagB = Filter.Tag(value: "B") + let tagC = Filter.Tag(value: "C") + let numeric = Filter.Numeric(attribute: "price", operator: .lessThan, value: 100) + let facet = Filter.Facet(attribute: "new", value: true) + + filterState.addAll(filters: [tagA, tagB], toGroupWithID: .or(name: "tags", filterType: .tag)) + + filterState.addAll(filters: [Filter.Tag(value: "hm"), Filter.Tag(value: "other")], toGroupWithID: .or(name: "tags", filterType: .tag)) + + filterState.addAll(filters: [ + Filter.Numeric(attribute: "size", range: 15...20), + Filter.Numeric(attribute: "price", operator: .greaterThan, value: 100)], toGroupWithID: .or(name: "numeric", filterType: .numeric)) + + filterState.add(numeric, toGroupWithID: .and(name: "others")) + filterState.add(facet, toGroupWithID: .and(name: "others")) + filterState.add(Filter.Tag(value: "someTag"), toGroupWithID: .and(name: "some")) + filterState.addAll(filters: [ + Filter.Numeric(attribute: "price", operator: .greaterThan, value: 20), + Filter.Numeric(attribute: "size", range: 15...20) + ], toGroupWithID: .and(name: "some")) + filterState.addAll(filters: [ + Filter.Facet(attribute: "brand", stringValue: "apple"), + Filter.Facet(attribute: "featured", boolValue: true), + Filter.Facet(attribute: "rating", floatValue: 4) + ], toGroupWithID: .and(name: "some")) + + XCTAssertTrue(filterState.contains(tagA)) + XCTAssertTrue(filterState.contains(tagB)) + XCTAssertTrue(filterState.contains(numeric)) + XCTAssertTrue(filterState.contains(facet)) + XCTAssertTrue(filterState.contains(tagA, inGroupWithID: .or(name: "tags", filterType: .tag))) + XCTAssertTrue(filterState.contains(tagB, inGroupWithID: .or(name: "tags", filterType: .tag))) + XCTAssertTrue(filterState.contains(numeric, inGroupWithID: .and(name: "others"))) + XCTAssertTrue(filterState.contains(facet, inGroupWithID: .and(name: "others"))) + + XCTAssertFalse(filterState.contains(tagC)) + XCTAssertFalse(filterState.contains(Filter.Facet(attribute: "new", value: false))) + XCTAssertFalse(filterState.contains(tagC, inGroupWithID: .or(name: "tags", filterType: .tag))) + XCTAssertFalse(filterState.contains(tagA, inGroupWithID: .and(name: "others"))) + XCTAssertFalse(filterState.contains(tagB, inGroupWithID: .and(name: "others"))) + + let expectedResult = """ + ( "price" > 100.0 OR "size":15.0 TO 20.0 ) AND ( "new":"true" AND "price" < 100.0 ) AND ( "_tags":"someTag" AND "brand":"apple" AND "featured":"true" AND "price" > 20.0 AND "rating":"4.0" AND "size":15.0 TO 20.0 ) AND ( "_tags":"A" OR "_tags":"B" OR "_tags":"hm" OR "_tags":"other" ) + """ + + XCTAssertEqual(filterState.buildSQL(), expectedResult) + + } + + func testRemove() { + + var filterState = GroupsStorage() + + let orGroupID: FilterGroup.ID = .or(name: "orTags", filterType: .tag) + let andGroupID: FilterGroup.ID = .and(name: "any") + let tagA = Filter.Tag(value: "a") + let tagB = Filter.Tag(value: "b") + let numericFilter = Filter.Numeric(attribute: "price", range: 1...10) + filterState.addAll(filters: [tagA, tagB], toGroupWithID: orGroupID) + filterState.addAll(filters: [tagA, tagB], toGroupWithID: andGroupID) + filterState.add(numericFilter, toGroupWithID: andGroupID) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "_tags":"a" AND "_tags":"b" AND "price":1.0 TO 10.0 ) AND ( "_tags":"a" OR "_tags":"b" ) + """) + +// XCTAssertTrue(filterState.remove(tagA, fromGroupWithID: andGroupID)) + XCTAssertTrue(filterState.remove(tagA)) + XCTAssertFalse(filterState.contains(tagA, inGroupWithID: andGroupID)) + +// XCTAssertTrue(filterState.remove(tagA, fromGroupWithID: orGroupID)) + XCTAssertFalse(filterState.contains(tagA, inGroupWithID: orGroupID)) + +// XCTAssertFalse(filterState.contains(Filter.Tag(value: "a"), inGroupWithID: orGroupID)) + XCTAssertFalse(filterState.contains(Filter.Tag(value: "a"))) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "_tags":"b" AND "price":1.0 TO 10.0 ) AND ( "_tags":"b" ) + """) + + // Try to delete one more time + XCTAssertFalse(filterState.remove(Filter.Tag(value: "a"))) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "_tags":"b" AND "price":1.0 TO 10.0 ) AND ( "_tags":"b" ) + """) + + // Remove filter occuring in multiple groups from one group + + XCTAssertTrue(filterState.remove(Filter.Tag(value: "b"), fromGroupWithID: .and(name: "any"))) + + XCTAssertTrue(filterState.contains(Filter.Tag(value: "b"))) + XCTAssertFalse(filterState.contains(Filter.Tag(value: "b"), inGroupWithID: .and(name: "any"))) + XCTAssertTrue(filterState.contains(Filter.Tag(value: "b"), inGroupWithID: .or(name: "orTags", filterType: .tag))) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "price":1.0 TO 10.0 ) AND ( "_tags":"b" ) + """) + + // Remove all from group + filterState.removeAll(fromGroupWithID: .and(name: "any")) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .and(name: "any")).isEmpty) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "_tags":"b" ) + """) + + // Remove all anywhere + filterState.removeAll() + XCTAssertTrue(filterState.isEmpty) + + XCTAssertEqual(filterState.buildSQL(), nil) + + } + + func testSubscriptAndOperatorPlayground() { + + var filterState = GroupsStorage() + + let filterFacet1 = Filter.Facet(attribute: "category", value: "table") + let filterFacet2 = Filter.Facet(attribute: "category", value: "chair") + let filterNumeric1 = Filter.Numeric(attribute: "price", operator: .greaterThan, value: 10) + let filterNumeric2 = Filter.Numeric(attribute: "price", operator: .lessThan, value: 20) + let filterTag1 = Filter.Tag(value: "Tom") + + filterState.add(filterFacet1, toGroupWithID: .or(name: "a", filterType: .facet)) + filterState.remove(filterFacet2, fromGroupWithID: .or(name: "a", filterType: .facet)) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "category":"table" ) + """) + + filterState.add(filterNumeric1, toGroupWithID: .and(name: "b")) + filterState.add(filterTag1, toGroupWithID: .and(name: "b")) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "category":"table" ) AND ( "_tags":"Tom" AND "price" > 10.0 ) + """) + + filterState.addAll(filters: [filterFacet1, filterFacet2], toGroupWithID: .or(name:"a", filterType: .facet)) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "category":"chair" OR "category":"table" ) AND ( "_tags":"Tom" AND "price" > 10.0 ) + """) + + filterState.addAll(filters: [filterNumeric1, filterNumeric2], toGroupWithID: .and(name: "b")) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "category":"chair" OR "category":"table" ) AND ( "_tags":"Tom" AND "price" < 20.0 AND "price" > 10.0 ) + """) + + } + + func testClearAttribute() { + + let filterNumeric1 = Filter.Numeric(attribute: "price", operator: .greaterThan, value: 10) + let filterNumeric2 = Filter.Numeric(attribute: "price", operator: .lessThan, value: 20) + let filterTag1 = Filter.Tag(value: "Tom") + let filterTag2 = Filter.Tag(value: "Hank") + + let groupNumericsOr = FilterGroup.ID.or(name: "filterNumeric", filterType: .numeric) + let groupTagsOr = FilterGroup.ID.or(name: "filterTags", filterType: .tag) + + var filterState = GroupsStorage() + + filterState.addAll(filters: [filterNumeric1, filterNumeric2], toGroupWithID: groupNumericsOr) + XCTAssertEqual(filterState.buildSQL(), """ + ( "price" < 20.0 OR "price" > 10.0 ) + """) + + filterState.addAll(filters: [filterTag1, filterTag2], toGroupWithID: groupTagsOr) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "price" < 20.0 OR "price" > 10.0 ) AND ( "_tags":"Hank" OR "_tags":"Tom" ) + """) + + filterState.removeAll(for: "price") + + XCTAssertFalse(filterState.contains(filterNumeric1)) + XCTAssertFalse(filterState.contains(filterNumeric2)) + XCTAssertTrue(filterState.contains(filterTag1)) + XCTAssertTrue(filterState.contains(filterTag2)) + + XCTAssertEqual(filterState.buildSQL(), """ + ( "_tags":"Hank" OR "_tags":"Tom" ) + """) + + } + + func testIsEmpty() { + var filterState = GroupsStorage() + let filter = Filter.Numeric(attribute: "price", operator: .greaterThan, value: 10) + let group = FilterGroup.ID.or(name: "group", filterType: .numeric) + XCTAssertTrue(filterState.isEmpty) + filterState.add(filter, toGroupWithID: group) + XCTAssertEqual(filterState.buildSQL(), """ + ( "price" > 10.0 ) + """) + XCTAssertFalse(filterState.isEmpty) + filterState.remove(filter) + XCTAssertTrue(filterState.isEmpty, filterState.buildSQL()!) + XCTAssertEqual(filterState.buildSQL(), nil) + } + + func testClear() { + var filterState = GroupsStorage() + let filterNumeric = Filter.Numeric(attribute: "price", operator: .greaterThan, value: 10) + let filterTag = Filter.Tag(value: "Tom") + let group = FilterGroup.ID.and(name: "group") + filterState.add(filterNumeric, toGroupWithID: group) + XCTAssertEqual(filterState.buildSQL(), """ + ( "price" > 10.0 ) + """) + filterState.add(filterTag, toGroupWithID: group) + XCTAssertEqual(filterState.buildSQL(), """ + ( "_tags":"Tom" AND "price" > 10.0 ) + """) + filterState.removeAll() + XCTAssertTrue(filterState.isEmpty) + XCTAssertEqual(filterState.buildSQL(), nil) + } + + func testToggle() { + + var filterState = GroupsStorage() + + let filter = Filter.Facet(attribute: "brand", stringValue: "sony") + + // Conjunctive Group + + XCTAssertFalse(filterState.getFilters(forGroupWithID: .or(name: "a", filterType: .facet)).contains(.facet(filter))) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .or(name: "a", filterType: .facet)).isEmpty) + + filterState.toggle(filter, inGroupWithID: .or(name: "a", filterType: .facet)) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .or(name: "a", filterType: .facet)).contains(.facet(filter))) + XCTAssertFalse(filterState.getFilters(forGroupWithID: .or(name: "a", filterType: .facet)).isEmpty) + + filterState.toggle(filter, inGroupWithID: .or(name: "a", filterType: .facet)) + XCTAssertFalse(filterState.contains(filter, inGroupWithID: .or(name: "a", filterType: .facet))) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .or(name: "a", filterType: .facet)).isEmpty) + + // Disjunctive Group + + XCTAssertFalse(filterState.contains(filter, inGroupWithID: .and(name: "a"))) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .and(name: "a")).isEmpty) + + filterState.toggle(filter, inGroupWithID: .and(name: "a")) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .and(name: "a")).contains(.facet(filter))) + XCTAssertFalse(filterState.getFilters(forGroupWithID: .and(name: "a")).isEmpty) + + filterState.toggle(filter, inGroupWithID: .and(name: "a")) + XCTAssertFalse(filterState.contains(filter, inGroupWithID: .and(name: "a"))) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .and(name: "a")).isEmpty) + + filterState.toggle(Filter.Numeric(attribute: "size", operator: .equals, value: 40), inGroupWithID: .and(name: "a")) + filterState.toggle(Filter.Facet(attribute: "country", stringValue: "france"), inGroupWithID: .and(name: "a")) + + XCTAssertFalse(filterState.getFilters(forGroupWithID: .and(name: "a")).isEmpty) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .and(name: "a")).contains(.numeric(Filter.Numeric(attribute: "size", operator: .equals, value: 40)))) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .and(name: "a")).contains(.facet(Filter.Facet(attribute: "country", stringValue: "france")))) + + filterState.toggle(Filter.Numeric(attribute: "size", operator: .equals, value: 40), inGroupWithID: .and(name: "a")) + filterState.toggle(Filter.Facet(attribute: "country", stringValue: "france"), inGroupWithID: .and(name: "a")) + + XCTAssertTrue(filterState.getFilters(forGroupWithID: .and(name: "a")).isEmpty) + XCTAssertFalse(filterState.getFilters(forGroupWithID: .and(name: "a")).contains(.numeric(Filter.Numeric(attribute: "size", operator: .equals, value: 40)))) + XCTAssertFalse(filterState.getFilters(forGroupWithID: .and(name: "a")).contains(.facet(Filter.Facet(attribute: "country", stringValue: "france")))) + + filterState.toggle(Filter.Facet(attribute: "size", floatValue: 40), inGroupWithID: .or(name: "a", filterType: .facet)) + filterState.toggle(Filter.Facet(attribute: "count", floatValue: 25), inGroupWithID: .or(name: "a", filterType: .facet)) + + XCTAssertFalse(filterState.getFilters(forGroupWithID: .or(name: "a", filterType: .facet)).isEmpty) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .or(name: "a", filterType: .facet)).contains(.facet(Filter.Facet(attribute: "size", floatValue: 40)))) + XCTAssertTrue(filterState.getFilters(forGroupWithID: .or(name: "a", filterType: .facet)).contains(.facet(Filter.Facet(attribute: "count", floatValue: 25)))) + + } + + func testDisjunctiveFacetAttributes() { + + var filterState = GroupsStorage() + + filterState.addAll(filters: [ + Filter.Facet(attribute: "color", stringValue: "red"), + Filter.Facet(attribute: "color", stringValue: "green"), + Filter.Facet(attribute: "color", stringValue: "blue") + ], toGroupWithID: .or(name: "g1", filterType: .facet)) + + XCTAssertEqual(filterState.getDisjunctiveFacetsAttributes(), ["color"]) + + filterState.add(Filter.Facet(attribute: "country", stringValue: "france"), toGroupWithID: .or(name: "g2", filterType: .facet)) + + XCTAssertEqual(filterState.getDisjunctiveFacetsAttributes(), ["color", "country"]) + + filterState.add(Filter.Facet(attribute: "country", stringValue: "uk"), toGroupWithID: .or(name: "g2", filterType: .facet)) + + filterState.add(Filter.Facet(attribute: "size", floatValue: 40), toGroupWithID: .or(name: "g2", filterType: .facet)) + + XCTAssertEqual(filterState.getDisjunctiveFacetsAttributes(), ["color", "country", "size"]) + + filterState.add(Filter.Numeric(attribute: "price", operator: .greaterThan, value: 50), toGroupWithID: .and(name: "g3")) + filterState.add(Filter.Facet(attribute: "featured", boolValue: true), toGroupWithID: .and(name: "g3")) + + XCTAssertEqual(filterState.getDisjunctiveFacetsAttributes(), ["color", "country", "size"]) + + filterState.add(Filter.Numeric(attribute: "price", operator: .lessThan, value: 100), toGroupWithID: .and(name: "g3")) + + XCTAssertEqual(filterState.getDisjunctiveFacetsAttributes(), ["color", "country", "size"]) + + filterState.add(Filter.Facet(attribute: "size", floatValue: 42), toGroupWithID: .or(name: "g2", filterType: .facet)) + + XCTAssertEqual(filterState.getDisjunctiveFacetsAttributes(), ["color", "country", "size"]) + + filterState.removeAll([ + Filter.Facet(attribute: "color", stringValue: "red"), + Filter.Facet(attribute: "color", stringValue: "green"), + Filter.Facet(attribute: "color", stringValue: "blue") + ], fromGroupWithID: .or(name: "g1", filterType: .facet)) + + XCTAssertEqual(filterState.getDisjunctiveFacetsAttributes(), ["country", "size"]) + + } + + func testRefinements() { + + var filterState = GroupsStorage() + + filterState.addAll(filters: [ + Filter.Facet(attribute: "color", stringValue: "red"), + Filter.Facet(attribute: "color", stringValue: "green"), + Filter.Facet(attribute: "color", stringValue: "blue") + ], toGroupWithID: .or(name: "g1", filterType: .facet)) + + XCTAssertEqual(filterState.getRawFacetFilters()["color"].flatMap(Set.init), Set(["red", "green", "blue"])) + + filterState.add(Filter.Facet(attribute: "country", stringValue: "france"), toGroupWithID: .or(name: "g2", filterType: .facet)) + + XCTAssertEqual(filterState.getRawFacetFilters()["color"].flatMap(Set.init), Set(["red", "green", "blue"])) + XCTAssertEqual(filterState.getRawFacetFilters()["country"], ["france"]) + + filterState.add(Filter.Facet(attribute: "country", stringValue: "uk"), toGroupWithID: .and(name: "g3")) + + XCTAssertEqual(filterState.getRawFacetFilters()["color"].flatMap(Set.init), Set(["red", "green", "blue"])) + XCTAssertEqual(filterState.getRawFacetFilters()["country"].flatMap(Set.init), Set(["france", "uk"])) + + filterState.remove(Filter.Facet(attribute: "color", stringValue: "green"), fromGroupWithID: .or(name: "g1", filterType: .facet)) + + XCTAssertEqual(filterState.getRawFacetFilters()["color"].flatMap(Set.init), Set(["red", "blue"])) + XCTAssertEqual(filterState.getRawFacetFilters()["country"].flatMap(Set.init), Set(["france", "uk"])) + + } + + func testFilterScoring() { + + var filterState = GroupsStorage() + + let filterFacet1 = Filter.Facet(attribute: Attribute("category"), value: "table", score: 5) + let filterFacet2 = Filter.Facet(attribute: Attribute("category"), value: "chair", score: 10) + let filterFacet3 = Filter.Facet(attribute: Attribute("type"), value: "equipment", score: 3) + + + let groupFacetsOr = FilterGroup.ID.or(name: "filterFacetsOr", filterType: .facet) + let groupFacetsAnd = FilterGroup.ID.and(name: "filterFacetsAnd") + + filterState.add(filterFacet1, toGroupWithID: groupFacetsOr) + filterState.add(filterFacet2, toGroupWithID: groupFacetsOr) + filterState.add(filterFacet3, toGroupWithID: groupFacetsAnd) + + let expectedResult = """ + ( "type":"equipment" ) AND ( "category":"chair" OR "category":"table" ) + """ + + XCTAssertEqual(filterState.buildSQL(), expectedResult) + + let expectedResultLegacy: FiltersStorage? = [ + .and("type:equipment"), + .or("category:chair", "category:table") + ] + XCTAssertEqual(FilterGroupConverter().legacy(filterState.toFilterGroups())?.units, expectedResultLegacy?.units) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/FilterState/FilterTests.swift b/Tests/InstantSearchCoreTests/Unit/FilterState/FilterTests.swift new file mode 100644 index 00000000..30eaebdd --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/FilterState/FilterTests.swift @@ -0,0 +1,125 @@ +// +// FilterTests.swift +// AlgoliaSearch OSX +// +// Created by Vladislav Fitc on 27/12/2018. +// Copyright © 2018 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class FilterTests: XCTestCase { + + func testFilterFacetVariants() { + testFilterFacet(with: "value") + testFilterFacet(with: 10) + testFilterFacet(with: true) + } + + func testFilterFacet(with value: Filter.Facet.ValueType) { + let attribute: Attribute = "a" + var facetFilter = Filter.Facet(attribute: attribute, value: value) + let expectedExpression = "\"\(attribute)\":\"\(value)\"" + XCTAssertEqual(facetFilter.attribute, attribute) + XCTAssertEqual(facetFilter.sqlForm, expectedExpression) + XCTAssertFalse(facetFilter.isNegated) + XCTAssertEqual(facetFilter.value, value) + // Test inversion + facetFilter.not() + XCTAssertTrue(facetFilter.isNegated) + XCTAssertEqual(facetFilter.sqlForm, "NOT \(expectedExpression)") + } + + func testFilterNumericComparisonConstruction() { + let attribute: Attribute = "a" + let value: Double = 10 + let op: Filter.Numeric.Operator = .equals + let expectedExpression = """ + "\(attribute)" \(op.rawValue) \(value) + """ + var numericFilter = Filter.Numeric(attribute: attribute, operator: op, value: value) + XCTAssertEqual(numericFilter.attribute, attribute) + XCTAssertEqual(numericFilter.sqlForm, expectedExpression) + XCTAssertFalse(numericFilter.isNegated) + // Test inversion + numericFilter.not() + XCTAssertTrue(numericFilter.isNegated) + XCTAssertEqual(numericFilter.sqlForm, "NOT \(expectedExpression)") + } + + func testFilterNumericRangeConstruction() { + let attribute: Attribute = "a" + let value: ClosedRange = 0...10 + let expectedExpression = """ + "\(attribute)":\(value.lowerBound) TO \(value.upperBound) + """ + var numericFilter = Filter.Numeric(attribute: attribute, range: value) + XCTAssertEqual(numericFilter.attribute, attribute) + XCTAssertEqual(numericFilter.sqlForm, expectedExpression) + XCTAssertFalse(numericFilter.isNegated) + // Test inversion + numericFilter.not() + XCTAssertTrue(numericFilter.isNegated) + XCTAssertEqual(numericFilter.sqlForm, "NOT \(expectedExpression)") + } + + func testTimeStamp() { + let attribute: Attribute = "beginDate" + let timeStamp = Date().timeIntervalSince1970 + let numericFilter = Filter.Numeric(attribute: attribute, operator: .greaterThan, value: timeStamp) + XCTAssertEqual(numericFilter.sqlForm, "\"beginDate\" > \(timeStamp)") + } + + func testFilterTagConstruction() { + let value = "a" + let attribute: Attribute = .tags + let expectedExpression = """ + "\(attribute)":"\(value)" + """ + var tagFilter = Filter.Tag(value: value) + XCTAssertEqual(tagFilter.attribute, attribute) + XCTAssertEqual(tagFilter.sqlForm, expectedExpression) + XCTAssertFalse(tagFilter.isNegated) + // Test inversion + tagFilter.not() + XCTAssertTrue(tagFilter.isNegated) + XCTAssertEqual(tagFilter.sqlForm, "NOT \(expectedExpression)") + } + + func testInversion() { + + let boolFacetFilter = Filter.Facet(attribute: "a", value: true) + XCTAssertFalse(boolFacetFilter.isNegated) + XCTAssertTrue((!boolFacetFilter).isNegated) + XCTAssertEqual(!boolFacetFilter, Filter.Facet(attribute: "a", value: true, isNegated: true)) + + let numericFacetFilter = Filter.Facet(attribute: "a", value: 1) + XCTAssertFalse(numericFacetFilter.isNegated) + XCTAssertTrue((!numericFacetFilter).isNegated) + XCTAssertEqual(!numericFacetFilter, Filter.Facet(attribute: "a", value: 1, isNegated: true)) + + let stringFacetFilter = Filter.Facet(attribute: "a", value: "b") + XCTAssertFalse(stringFacetFilter.isNegated) + XCTAssertTrue((!stringFacetFilter).isNegated) + XCTAssertEqual(!stringFacetFilter, Filter.Facet(attribute: "a", value: "b", isNegated: true)) + + let filterTag = Filter.Tag(value: "a") + XCTAssertFalse(filterTag.isNegated) + XCTAssertTrue((!filterTag).isNegated) + XCTAssertEqual(!filterTag, Filter.Tag(value: "a", isNegated: true)) + + let comparisonNumericFilter = Filter.Numeric(attribute: "a", operator: .equals, value: 10) + XCTAssertFalse(comparisonNumericFilter.isNegated) + XCTAssertTrue((!comparisonNumericFilter).isNegated) + XCTAssertEqual(!comparisonNumericFilter, Filter.Numeric(attribute: "a", operator: .equals, value: 10, isNegated: true)) + + let rangeNumericFilter = Filter.Numeric(attribute: "a", range: 0...10) + XCTAssertFalse(rangeNumericFilter.isNegated) + XCTAssertTrue((!rangeNumericFilter).isNegated) + XCTAssertEqual(!rangeNumericFilter, Filter.Numeric(attribute: "a", range: 0...10, isNegated: true)) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Helpers.swift b/Tests/InstantSearchCoreTests/Unit/Helpers.swift new file mode 100644 index 00000000..758b03ac --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Helpers.swift @@ -0,0 +1,29 @@ +import Foundation +import AlgoliaSearchClient + +func safeIndexName(_ name: String) -> IndexName { + var targetName = Bundle.main.object(forInfoDictionaryKey: "BUILD_TARGET_NAME") as? String ?? "" + targetName = targetName.replacingOccurrences(of: " ", with: "-") + + let rawName: String + if let travisBuild = ProcessInfo.processInfo.environment["TRAVIS_JOB_NUMBER"] { + rawName = "\(name)_travis_\(travisBuild)" + } else if let bitriseBuild = Bundle.main.object(forInfoDictionaryKey: "BITRISE_BUILD_NUMBER") as? String { + rawName = "\(name)_bitrise_\(bitriseBuild)_\(targetName)" + } else { + rawName = name + } + return IndexName(rawValue: rawName) +} + +func average(values: [Double]) -> Double { + return values.reduce(0, +) / Double(values.count) +} + +/// Generate a new host name in the `algolia.biz` domain. +/// The DNS lookup for any host in the `algolia.biz` domain will time-out. +/// Generating a new host name every time avoids any system-level or network-level caching side effect. +/// +func uniqueAlgoliaBizHost() -> String { + return "swift-\(UInt32(NSDate().timeIntervalSince1970)).algolia.biz" +} diff --git a/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorControllerConnectionTests.swift b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorControllerConnectionTests.swift new file mode 100644 index 00000000..928d5799 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorControllerConnectionTests.swift @@ -0,0 +1,114 @@ +// +// HitsInteractorControllerConnectionTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class HitsInteractorControllerConnectionTests: XCTestCase { + + var interactor: HitsInteractor { + return HitsInteractor(settings: .init(infiniteScrolling: .on(withOffset: 10), showItemsOnEmptyQuery: true), + paginationController: .init(), + infiniteScrollingController: TestInfiniteScrollingController()) + } + + weak var disposableController: TestHitsController? + weak var disposableInteractor: HitsInteractor? + + func testLeak() { + let interactor = self.interactor + let controller = TestHitsController() + + disposableController = controller + disposableInteractor = interactor + + let connection = HitsInteractor.ControllerConnection(interactor: interactor, controller: controller) + connection.connect() + } + + override func tearDown() { + XCTAssertNil(disposableInteractor, "Leaked interactor") + XCTAssertNil(disposableController, "Leaked controller") + } + + func testConnect() { + + let controller = TestHitsController() + let interactor = self.interactor + + let connection = HitsInteractor.ControllerConnection(interactor: interactor, controller: controller) + + connection.connect() + + checkConnection(interactor: interactor, + controller: controller, + isConnected: true) + + } + + func testConnectMethod() { + + let controller = TestHitsController() + let interactor = self.interactor + + interactor.connectController(controller) + + checkConnection(interactor: interactor, + controller: controller, + isConnected: true) + + } + + func testDisconnect() { + + let controller = TestHitsController() + let interactor = self.interactor + + let connection = HitsInteractor.ControllerConnection(interactor: interactor, controller: controller) + + connection.disconnect() + + checkConnection(interactor: interactor, + controller: controller, + isConnected: false) + + } + + func checkConnection(interactor: HitsInteractor, + controller: TestHitsController, + isConnected: Bool) { + + if isConnected { + XCTAssertTrue(controller.hitsSource === interactor) + } else { + XCTAssertNil(controller.hitsSource) + } + + let requestChangedExpectation = expectation(description: "request changed") + requestChangedExpectation.isInverted = !isConnected + + controller.didScrollToTop = { + requestChangedExpectation.fulfill() + } + + interactor.onRequestChanged.fire(()) + + let resultsUpdatedExpectation = expectation(description: "results updated") + resultsUpdatedExpectation.isInverted = !isConnected + + controller.didReload = { + resultsUpdatedExpectation.fulfill() + } + + interactor.onResultsUpdated.fire(SearchResponse(hits: [TestRecord]())) + + waitForExpectations(timeout: 5, handler: .none) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorFilterStateConnectionTests.swift b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorFilterStateConnectionTests.swift new file mode 100644 index 00000000..a37edd95 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorFilterStateConnectionTests.swift @@ -0,0 +1,96 @@ +// +// HitsInteractorFilterStateConnectionTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class HitsInteractorFilterStateConnectionTests: XCTestCase { + + var interactor: HitsInteractor { + return HitsInteractor(settings: .init(infiniteScrolling: .on(withOffset: 10), showItemsOnEmptyQuery: true), + paginationController: .init(), + infiniteScrollingController: TestInfiniteScrollingController()) + } + + var filterState: FilterState { + return .init() + } + + weak var disposableInteractor: HitsInteractor? + weak var disposableFilterState: FilterState? + + func testLeak() { + let interactor = self.interactor + let filterState = self.filterState + + disposableInteractor = interactor + disposableFilterState = filterState + + let connection = HitsInteractorFilterStateConnection(interactor: interactor, + filterState: filterState) + + connection.connect() + } + + override func tearDown() { + XCTAssertNil(disposableInteractor, "Leaked interactor") + XCTAssertNil(disposableFilterState, "Leaked filterState") + } + + func testConnection() { + let interactor = self.interactor + let filterState = self.filterState + + let connection = HitsInteractorFilterStateConnection(interactor: interactor, + filterState: filterState) + + connection.connect() + checkConnection(interactor: interactor, filterState: filterState, isConnected: true) + } + + func testConnectionMethod() { + let interactor = self.interactor + let filterState = self.filterState + + interactor.connectFilterState(filterState) + + checkConnection(interactor: interactor, filterState: filterState, isConnected: true) + } + + func testDisconnection() { + let interactor = self.interactor + let filterState = self.filterState + + let connection = HitsInteractorFilterStateConnection(interactor: interactor, + filterState: filterState) + + connection.connect() + connection.disconnect() + checkConnection(interactor: interactor, filterState: filterState, isConnected: false) + } + + func checkConnection(interactor: HitsInteractor, + filterState: FilterState, + isConnected: Bool) { + + let exp = expectation(description: "change query when filter state changed") + exp.isInverted = !isConnected + + interactor.onRequestChanged.subscribe(with: self) { _, _ in + exp.fulfill() + } + + filterState.add(Filter.Tag("t"), toGroupWithID: .and(name: "")) + filterState.notifyChange() + + waitForExpectations(timeout: 2, handler: nil) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorRelatedItemsTests.swift b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorRelatedItemsTests.swift new file mode 100644 index 00000000..0e851095 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorRelatedItemsTests.swift @@ -0,0 +1,51 @@ +// +// HitsInteractorRelatedItemsTests.swift +// InstantSearchCore +// +// Created by test test on 23/04/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation + +import Foundation +import XCTest +@testable import InstantSearchCore + +class HitsInteractorRelatedItemsTests: XCTestCase { + + struct Product: Codable { + let name: String + let brand: String + let type: String + let categories: [String] + let image: URL + } + + func testConnect() { + let matchingPatterns: [MatchingPattern] = + [ + MatchingPattern(attribute: "brand", score: 3, filterPath: \.brand), + MatchingPattern(attribute: "type", score: 10, filterPath: \.type), + MatchingPattern(attribute: "categories", score: 2, filterPath: \.categories), + ] + + let searcher = SingleIndexSearcher(appID: "", apiKey: "", indexName: "") + let product = Product(name: "productName", brand: "Amazon", type: "Streaming media plyr", categories: ["Streaming Media Players", "TV & Home Theater"], image: URL.init(string: "http://url.com")!) + + let hitsInteractor = HitsInteractor.init() + + let hit: ObjectWrapper = .init(objectID: "objectID123", object: product) + hitsInteractor.connectSearcher(searcher, withRelatedItemsTo: hit, with: matchingPatterns) + + let expectedOptionalFilter: FiltersStorage = [ + .and("brand:Amazon"), + .or("categories:Streaming Media Players", "categories:TV & Home Theater"), + .and("type:Streaming media plyr")] + + XCTAssertEqual(searcher.indexQueryState.query.sumOrFiltersScores, true) + XCTAssertEqual(searcher.indexQueryState.query.optionalFilters?.units, expectedOptionalFilter.units) + XCTAssertEqual(searcher.indexQueryState.query.facetFilters?.units, (["objectID:-objectID123"] as FiltersStorage).units) + + } +} diff --git a/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorSearcherConnectionTests.swift b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorSearcherConnectionTests.swift new file mode 100644 index 00000000..a5558cf5 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorSearcherConnectionTests.swift @@ -0,0 +1,153 @@ +// +// HitsInteractorSearcherConnectionTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import AlgoliaSearchClient +@testable import InstantSearchCore +import XCTest + +class HitsInteractorSearcherConnectionTests: XCTestCase { + + weak var disposableInteractor: HitsInteractor? + weak var disposableSearcher: SingleIndexSearcher? + + func getInteractor(with infiniteScrollingController: InfiniteScrollable) -> HitsInteractor { + + let paginator = Paginator() + + let page1 = ["i1", "i2", "i3"].map { JSON.string($0) } + paginator.pageMap = PageMap([1: page1]) + + let interactor = HitsInteractor(settings: .init(infiniteScrolling: .on(withOffset: 10), showItemsOnEmptyQuery: true), + paginationController: paginator, + infiniteScrollingController: infiniteScrollingController) + + return interactor + } + + func testLeak() { + let infiniteScrollingController = TestInfiniteScrollingController() + infiniteScrollingController.pendingPages = [0, 2] + + let searcher = SingleIndexSearcher(client: SearchClient(appID: "", apiKey: ""), indexName: "") + let interactor = getInteractor(with: infiniteScrollingController) + + disposableInteractor = interactor + disposableSearcher = searcher + + let connection: Connection = HitsInteractor.SingleIndexSearcherConnection(interactor: interactor, + searcher: searcher) + connection.connect() + } + + override func tearDown() { + + XCTAssertNil(disposableInteractor, "Leaked interactor") + XCTAssertNil(disposableSearcher, "Leaked searcher") + } + + func testConnect() { + + let infiniteScrollingController = TestInfiniteScrollingController() + infiniteScrollingController.pendingPages = [0, 2] + + let searcher = SingleIndexSearcher(client: SearchClient(appID: "", apiKey: ""), indexName: "") + let interactor = getInteractor(with: infiniteScrollingController) + + let connection: Connection = HitsInteractor.SingleIndexSearcherConnection(interactor: interactor, + searcher: searcher) + connection.connect() + + checkConnection(searcher: searcher, + interactor: interactor, + infiniteScrollingController: infiniteScrollingController, + isConnected: true) + } + + func testDisconnect() { + + let infiniteScrollingController = TestInfiniteScrollingController() + infiniteScrollingController.pendingPages = [0, 2] + + let searcher = SingleIndexSearcher(client: SearchClient(appID: "", apiKey: ""), indexName: "") + let interactor = getInteractor(with: infiniteScrollingController) + + let connection: Connection = HitsInteractor.SingleIndexSearcherConnection(interactor: interactor, + searcher: searcher) + connection.connect() + connection.disconnect() + + checkConnection(searcher: searcher, + interactor: interactor, + infiniteScrollingController: infiniteScrollingController, + isConnected: false) + + } + + func testConnectMethod() { + + let infiniteScrollingController = TestInfiniteScrollingController() + infiniteScrollingController.pendingPages = [0, 2] + + let searcher = SingleIndexSearcher(client: SearchClient(appID: "", apiKey: ""), indexName: "") + let interactor = getInteractor(with: infiniteScrollingController) + + interactor.connectSearcher(searcher) + + checkConnection(searcher: searcher, + interactor: interactor, + infiniteScrollingController: infiniteScrollingController, + isConnected: true) + + } + + func checkConnection(searcher: SingleIndexSearcher, + interactor: HitsInteractor, + infiniteScrollingController: TestInfiniteScrollingController, + isConnected: Bool) { + if isConnected { + XCTAssertTrue(searcher === infiniteScrollingController.pageLoader) + } else { + XCTAssertNil(infiniteScrollingController.pageLoader) + } + + let queryChangedExpectation = expectation(description: "query changed") + queryChangedExpectation.isInverted = !isConnected + + interactor.onRequestChanged.subscribe(with: self) { _, _ in + queryChangedExpectation.fulfill() + } + + searcher.query = "query" + searcher.indexQueryState.query.page = 0 + infiniteScrollingController.pendingPages = [0] + + let resultsUpdatedExpectation = expectation(description: "results updated") + resultsUpdatedExpectation.isInverted = !isConnected + + interactor.onResultsUpdated.subscribe(with: self) { _, _ in + resultsUpdatedExpectation.fulfill() + XCTAssertTrue(infiniteScrollingController.pendingPages.isEmpty) + } + + let searchResponse = SearchResponse(hits: [Hit(object: ["field": "value"])]) + searcher.onResults.fire(searchResponse) + + infiniteScrollingController.pendingPages = [0] + searcher.onError.fire((searcher.indexQueryState.query, NSError())) + + if isConnected { + XCTAssertTrue(infiniteScrollingController.pendingPages.isEmpty) + } else { + XCTAssertFalse(infiniteScrollingController.pendingPages.isEmpty) + } + + waitForExpectations(timeout: 2, handler: nil) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift new file mode 100644 index 00000000..5a252b9d --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Hits/HitsInteractorTests.swift @@ -0,0 +1,202 @@ +// +// HitsInteractorTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 14/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest +import AlgoliaSearchClient +extension Index { + + static var test: Index = SearchClient(appID: "", apiKey: "").index(withName: "") + +} + +class HitsInteractorTests: XCTestCase { + + func testConstructionWithExplicitSettings() { + + let vm = HitsInteractor(infiniteScrolling: .off, showItemsOnEmptyQuery: false) + + if case .off = vm.settings.infiniteScrolling { + } else { XCTFail() } + XCTAssertFalse(vm.settings.showItemsOnEmptyQuery) + + let vm1 = HitsInteractor(infiniteScrolling: .on(withOffset: 1000), showItemsOnEmptyQuery: true) + + if case .on(let offset) = vm1.settings.infiniteScrolling { + XCTAssertEqual(offset, 1000) + } else { XCTFail() } + XCTAssertTrue(vm1.settings.showItemsOnEmptyQuery) + + } + + func testUpdateAndContent() throws { + + let vm = HitsInteractor>(infiniteScrolling: .off, showItemsOnEmptyQuery: true) + + let hits = ["h1", "h2", "h3"].map(TestRecord.withValue) + let results = SearchResponse(hits: hits) + + XCTAssertEqual(vm.numberOfHits(), 0) + XCTAssertNil(vm.hit(atIndex: 0)) + + let exp = expectation(description: "on results updated") + + vm.onResultsUpdated.subscribe(with: self) { (_, _) in + XCTAssertEqual(vm.numberOfHits(), 3) + XCTAssertEqual(vm.hit(atIndex: 0)?.value, "h1") + XCTAssertEqual(vm.hit(atIndex: 1)?.value, "h2") + XCTAssertEqual(vm.hit(atIndex: 2)?.value, "h3") + exp.fulfill() + } + + vm.update(results) + + waitForExpectations(timeout: 3, handler: .none) + + } + + func testHitsAppearanceOnEmptyQueryIfDesactivated() { + + let paginator = Paginator>() + + let hits = (0..<20).map(TestRecord.withValue) + let results = SearchResponse(hits: hits) + + let vm = HitsInteractor( + settings: .init(showItemsOnEmptyQuery: false), + paginationController: paginator, + infiniteScrollingController: TestInfiniteScrollingController()) + + let exp = expectation(description: "on results updated") + + vm.onResultsUpdated.subscribe(with: self) { (_, _) in + XCTAssertEqual(vm.numberOfHits(), 0) + exp.fulfill() + } + + vm.update(results) + + waitForExpectations(timeout: 3, handler: .none) + } + + func testHitsAppearanceOnEmptyQueryIfActivated() { + + let paginationController = Paginator>() + let infiniteScrollingController = TestInfiniteScrollingController() + + let hits = (0..<20).map(TestRecord.withValue) + let results = SearchResponse(hits: hits) + + let vm = HitsInteractor( + settings: .init(showItemsOnEmptyQuery: true), + paginationController: paginationController, + infiniteScrollingController: infiniteScrollingController + ) + + let exp = expectation(description: "on results updated") + + vm.onResultsUpdated.subscribe(with: self) { (_, _) in + XCTAssertEqual(vm.numberOfHits(), hits.count) + exp.fulfill() + } + + vm.update(results) + + waitForExpectations(timeout: 3, handler: .none) + } + + func testRawHitAtIndex() throws { + + let paginationController = Paginator>() + let infiniteScrollingController = TestInfiniteScrollingController() + + let hits = (0..<20).map(TestRecord.withValue) + let results = SearchResponse(hits: hits) + + let vm = HitsInteractor( + settings: .init(showItemsOnEmptyQuery: true), + paginationController: paginationController, + infiniteScrollingController: infiniteScrollingController + ) + + let exp = expectation(description: "on results updated") + + vm.onResultsUpdated.subscribe(with: self) { (_, _) in + do { + let rawHit = try XCTUnwrap(vm.rawHitAtIndex(5)) + XCTAssertEqual(rawHit["value"] as? NSNumber, 5) + } catch let error { + XCTFail("\(error)") + } + exp.fulfill() + } + + vm.update(results) + + waitForExpectations(timeout: 3, handler: .none) + } + + func testInfiniteScrollingTriggering() { + + let pc = Paginator() + + let page1 = ["i1", "i2", "i3"].map { JSON.string($0) } + pc.pageMap = PageMap([1: page1]) + + let isc = TestInfiniteScrollingController() + + let loadPagesTriggered = expectation(description: "load pages triggered") + + let vm = HitsInteractor( + settings: .init(infiniteScrolling: .on(withOffset: 10), showItemsOnEmptyQuery: true), + paginationController: pc, + infiniteScrollingController: isc) + + isc.didCalculatePages = { _, _ in + loadPagesTriggered.fulfill() + } + + _ = vm.hit(atIndex: 4) + + waitForExpectations(timeout: 2, handler: nil) + + } + + func testChangeQuery() { + + let pc = Paginator() + + let page1 = ["i1", "i2", "i3"].map { JSON.string($0) } + pc.pageMap = PageMap([1: page1]) + + let isc = TestInfiniteScrollingController() + isc.pendingPages = [0, 2] + + let vm = HitsInteractor( + settings: .init(infiniteScrolling: .on(withOffset: 10), showItemsOnEmptyQuery: true), + paginationController: pc, + infiniteScrollingController: isc) + + let onRequestChangedExpectation = expectation(description: "on request changed") + + vm.onRequestChanged.subscribe(with: self) { _, _ in + + XCTAssertTrue(pc.isInvalidated) + XCTAssertTrue(isc.pendingPages.isEmpty) + + onRequestChangedExpectation.fulfill() + } + + vm.notifyQueryChanged() + + waitForExpectations(timeout: 3, handler: nil) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Hits/TestHitsController.swift b/Tests/InstantSearchCoreTests/Unit/Hits/TestHitsController.swift new file mode 100644 index 00000000..3c3148ef --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Hits/TestHitsController.swift @@ -0,0 +1,27 @@ +// +// TestHitsController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore + +class TestHitsController: HitsController { + + var hitsSource: HitsInteractor? + + var didReload: (() -> Void)? + var didScrollToTop: (() -> Void)? + + func reload() { + didReload?() + } + + func scrollToTop() { + didScrollToTop?() + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Hits/TestInfiniteScrollingController.swift b/Tests/InstantSearchCoreTests/Unit/Hits/TestInfiniteScrollingController.swift new file mode 100644 index 00000000..d50bd918 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Hits/TestInfiniteScrollingController.swift @@ -0,0 +1,34 @@ +// +// TestInfiniteScrollingController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 02/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore + +class TestInfiniteScrollingController: InfiniteScrollable { + + var lastPageIndex: Int? + + var pageLoader: PageLoadable? + + var pendingPages = Set() + + var didCalculatePages: ((Int, Int) -> Void)? + + func calculatePagesAndLoad(currentRow: Int, offset: Int, pageMap: PageMap) { + didCalculatePages?(currentRow, offset) + } + + func notifyPending(pageIndex: Int) { + pendingPages.remove(pageIndex) + } + + func notifyPendingAll() { + pendingPages.removeAll() + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/InfiniteScrollingControllerTests.swift b/Tests/InstantSearchCoreTests/Unit/InfiniteScrollingControllerTests.swift new file mode 100644 index 00000000..dd2abbfa --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/InfiniteScrollingControllerTests.swift @@ -0,0 +1,247 @@ +// +// InfiniteScrollingControllerTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 05/06/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class InfiniteScrollingControllerTests: XCTestCase { + + func testPreviousPagesComputation() { + + let isc = InfiniteScrollingController() + + let pageMap = PageMap([1: ["i1", "i2", "i3"]])! + + let previousPagesToLoad = isc.computePreviousPagesToLoad(currentRow: 5, offset: 3, pageMap: pageMap) + + XCTAssertEqual(previousPagesToLoad, [0]) + + } + + func testPreviousPagesComputation_NoPage() { + + let isc = InfiniteScrollingController() + + let pageMap = PageMap([1: ["i1", "i2", "i3"]])! + + let previousPagesToLoad = isc.computePreviousPagesToLoad(currentRow: 5, offset: 2, pageMap: pageMap) + + XCTAssertTrue(previousPagesToLoad.isEmpty) + + } + + func testPreviousPagesComputation_Negative() { + + let isc = InfiniteScrollingController() + + let pageMap = PageMap([0: ["i1", "i2", "i3"]])! + + let previousPagesToLoad = isc.computePreviousPagesToLoad(currentRow: 1, offset: 2, pageMap: pageMap) + + XCTAssertTrue(previousPagesToLoad.isEmpty) + + } + + func testPreviousPagesComputation_PageLoaded() { + + let isc = InfiniteScrollingController() + + let pageMap = PageMap([0: ["a1", "a2", "a3"], 1: ["i1", "i2", "i3"]])! + + let previousPagesToLoad = isc.computePreviousPagesToLoad(currentRow: 3, offset: 2, pageMap: pageMap) + + XCTAssertTrue(previousPagesToLoad.isEmpty) + + } + + func testPreviousPagesComputation_MultiplePagesMissing() { + + let isc = InfiniteScrollingController() + + let pageMap = PageMap([ + 2: ["d1", "d2", "d3"] + ])! + + let previousPagesToLoad = isc.computePreviousPagesToLoad(currentRow: 6, offset: 5, pageMap: pageMap) + + XCTAssertEqual(previousPagesToLoad, [0, 1]) + + } + + func testPreviousPagesComputation_PreviousPagesPartiallyLoaded() { + + let isc = InfiniteScrollingController() + + let pageMap = PageMap([ + 1: ["i1", "i2", "i3"], + 2: ["d1", "d2", "d3"] + ])! + + let previousPagesToLoad = isc.computePreviousPagesToLoad(currentRow: 6, offset: 5, pageMap: pageMap) + + XCTAssertEqual(previousPagesToLoad, [0]) + + } + + func testPreviousPagesComputation_PreviousPagesPartiallyLoadedWithHole() { + + let isc = InfiniteScrollingController() + + let pageMap = PageMap([ + 0: ["a1", "a2", "a3"], + 2: ["d1", "d2", "d3"] + ])! + + let previousPagesToLoad = isc.computePreviousPagesToLoad(currentRow: 6, offset: 5, pageMap: pageMap) + + XCTAssertEqual(previousPagesToLoad, [1]) + + } + + func testNextPageComputation() { + + let isc = InfiniteScrollingController() + + let pageMap = PageMap([1: ["i1", "i2", "i3"]])! + + let nextPageIndex = isc.computeNextPagesToLoad(currentRow: 5, offset: 3, pageMap: pageMap) + + XCTAssertEqual(nextPageIndex, [2]) + + } + + func testNextPagesComputation_NoPage() { + + let isc = InfiniteScrollingController() + + let pageMap = PageMap([1: ["i1", "i2", "i3"]])! + + let nextPagesToLoad = isc.computeNextPagesToLoad(currentRow: 3, offset: 2, pageMap: pageMap) + + XCTAssertTrue(nextPagesToLoad.isEmpty) + + } + + func testNextPagesComputation_OnLastPage() { + + let isc = InfiniteScrollingController() + isc.lastPageIndex = 1 + + let pageMap = PageMap([1: ["i1", "i2", "i3"]])! + + let nextPagesToLoad = isc.computeNextPagesToLoad(currentRow: 5, offset: 2, pageMap: pageMap) + + XCTAssertTrue(nextPagesToLoad.isEmpty) + + } + + func testNextPagesComputation_NextPageLoaded() { + + let isc = InfiniteScrollingController() + + let pageMap = PageMap([0: ["a1", "a2", "a3"], 1: ["i1", "i2", "i3"]])! + + let nextPagesToLoad = isc.computeNextPagesToLoad(currentRow: 1, offset: 2, pageMap: pageMap) + + XCTAssertTrue(nextPagesToLoad.isEmpty) + + } + + func testNextPagesComputation_MultiplePagesMissing() { + + let isc = InfiniteScrollingController() + isc.lastPageIndex = 3 + + let pageMap = PageMap([ + 0: ["a1", "a2", "a3"] , + 1: ["i1", "i2", "i3"] + ])! + + let nextPagesToLoad = isc.computeNextPagesToLoad(currentRow: 5, offset: 5, pageMap: pageMap) + + XCTAssertEqual(nextPagesToLoad, [2, 3]) + + } + + func testNextPagesComputation_NextPagesPartiallyLoaded() { + + let isc = InfiniteScrollingController() + isc.lastPageIndex = 2 + + let pageMap = PageMap([ + 0: ["a1", "a2", "a3"] , + 1: ["i1", "i2", "i3"] + ])! + + let nextPagesToLoad = isc.computeNextPagesToLoad(currentRow: 5, offset: 2, pageMap: pageMap) + + XCTAssertEqual(nextPagesToLoad, [2]) + + } + + func testNextPagesComputation_NextPagesPartiallyLoadedWithHole() { + + let isc = InfiniteScrollingController() + isc.lastPageIndex = 3 + + let pageMap = PageMap([ + 0: ["a1", "a2", "a3"] , + 1: ["i1", "i2", "i3"], + 3: ["d1", "d2"] + ])! + + let nextPagesToLoad = isc.computeNextPagesToLoad(currentRow: 5, offset: 10, pageMap: pageMap) + + XCTAssertEqual(nextPagesToLoad, [2]) + + } + + func testNotifyLoading() { + + let isc = InfiniteScrollingController() + let pageLoader = TestPageLoader() + isc.pageLoader = pageLoader + + let pageMap = PageMap([ + 1: ["i1", "i2", "i3"] + ])! + + XCTAssertFalse(isc.isLoadedOrPending(pageIndex: 0, pageMap: pageMap)) + XCTAssertTrue(isc.isLoadedOrPending(pageIndex: 1, pageMap: pageMap)) + XCTAssertFalse(isc.isLoadedOrPending(pageIndex: 2, pageMap: pageMap)) + + isc.calculatePagesAndLoad(currentRow: 4, offset: 2, pageMap: pageMap) + + XCTAssertTrue(isc.isLoadedOrPending(pageIndex: 0, pageMap: pageMap)) + XCTAssertTrue(isc.isLoadedOrPending(pageIndex: 1, pageMap: pageMap)) + XCTAssertTrue(isc.isLoadedOrPending(pageIndex: 2, pageMap: pageMap)) + + isc.notifyPending(pageIndex: 0) + + XCTAssertFalse(isc.isLoadedOrPending(pageIndex: 0, pageMap: pageMap)) + XCTAssertTrue(isc.isLoadedOrPending(pageIndex: 1, pageMap: pageMap)) + XCTAssertTrue(isc.isLoadedOrPending(pageIndex: 2, pageMap: pageMap)) + + isc.notifyPending(pageIndex: 2) + + XCTAssertFalse(isc.isLoadedOrPending(pageIndex: 0, pageMap: pageMap)) + XCTAssertTrue(isc.isLoadedOrPending(pageIndex: 1, pageMap: pageMap)) + XCTAssertFalse(isc.isLoadedOrPending(pageIndex: 2, pageMap: pageMap)) + + isc.calculatePagesAndLoad(currentRow: 4, offset: 2, pageMap: pageMap) + + isc.notifyPendingAll() + + XCTAssertFalse(isc.isLoadedOrPending(pageIndex: 0, pageMap: pageMap)) + XCTAssertTrue(isc.isLoadedOrPending(pageIndex: 1, pageMap: pageMap)) + XCTAssertFalse(isc.isLoadedOrPending(pageIndex: 2, pageMap: pageMap)) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/ItemInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/ItemInteractorTests.swift new file mode 100644 index 00000000..73504233 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/ItemInteractorTests.swift @@ -0,0 +1,41 @@ +// +// ItemInteractorTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 31/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class ItemInteractorTests: XCTestCase { + + typealias VM = ItemInteractor + + func testConstruction() { + + let interactor = VM(item: "i") + + XCTAssertEqual(interactor.item, "i") + + } + + func testSwitchItem() { + + let interactor = VM(item: "i") + + let switchItemExpectation = expectation(description: "item changed") + + interactor.onItemChanged.subscribe(with: self) { _, newItem in + XCTAssertEqual(newItem, "o") + switchItemExpectation.fulfill() + } + + interactor.item = "o" + + waitForExpectations(timeout: 2, handler: nil) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/MultiIndexHitsInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/MultiIndexHitsInteractorTests.swift new file mode 100644 index 00000000..e93dac9a --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/MultiIndexHitsInteractorTests.swift @@ -0,0 +1,387 @@ +// +// MultiHitsInteractorTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 18/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import AlgoliaSearchClient +import XCTest + +class TestPageLoader: PageLoadable { + + var didLoadPage: ((Int) -> Void)? + + func loadPage(atIndex pageIndex: Int) { + didLoadPage?(pageIndex) + } + +} + +struct TestRecord: Codable { + + let objectID: ObjectID + let value: Value + + init(_ value: Value, objectID: ObjectID = ObjectID(rawValue: UUID().uuidString)) { + self.value = value + self.objectID = objectID + } + + static func withValue(_ value: Value) -> Self { + .init(value) + } + +} + +class MultiIndexHitsInteractorTests: XCTestCase { + + func testConstruction() { + let interactor = MultiIndexHitsInteractor(hitsInteractors: []) + XCTAssertEqual(interactor.numberOfSections(), 0) + } + + func testAppend() { + let interactor1 = HitsInteractor<[String: Int]>() + let interactor2 = HitsInteractor<[String: [String: Int]]>() + let multiInteractor = MultiIndexHitsInteractor(hitsInteractors: [interactor1, interactor2]) + + XCTAssertEqual(multiInteractor.numberOfSections(), 2) + XCTAssertTrue(multiInteractor.contains(interactor1)) + XCTAssertTrue(multiInteractor.contains(interactor2)) + + } + + func testAccessByIndex() { + let interactor1 = HitsInteractor<[String: Int]>() + let interactor2 = HitsInteractor<[String: [String: Int]]>() + let multiInteractor = MultiIndexHitsInteractor(hitsInteractors: [interactor1, interactor2]) + + XCTAssertEqual(multiInteractor.numberOfSections(), 2) + XCTAssertTrue(multiInteractor.contains(interactor1)) + XCTAssertTrue(multiInteractor.contains(interactor2)) + XCTAssertEqual(multiInteractor.section(of: interactor1), 0) + XCTAssertEqual(multiInteractor.section(of: interactor2), 1) + + } + + func testSearchByIndexThrows() { + + let interactor1 = HitsInteractor<[String: Int]>() + let interactor2 = HitsInteractor<[String: [String: Int]]>() + + let multiInteractor = MultiIndexHitsInteractor(hitsInteractors: [interactor1, interactor2]) + + XCTAssertNoThrow(try multiInteractor.hitsInteractor(forSection: 0) as HitsInteractor<[String: Int]>) + XCTAssertNoThrow(try multiInteractor.hitsInteractor(forSection: 1) as HitsInteractor<[String: [String: Int]]>) + XCTAssertThrowsError(try multiInteractor.hitsInteractor(forSection: 0) as HitsInteractor<[String: [String: String]]>) + XCTAssertThrowsError(try multiInteractor.hitsInteractor(forSection: 1) as HitsInteractor) + + } + + func testUpdatePerInteractor() throws { + + let interactor1 = HitsInteractor>() + let interactor2 = HitsInteractor>() + let multiInteractor = MultiIndexHitsInteractor(hitsInteractors: [interactor1, interactor2]) + + let hits1 = (1...3).map(TestRecord.withValue) + let results1 = SearchResponse(hits: hits1) + + let hits2 = [true, false, true].map(TestRecord.withValue) + let results2 = SearchResponse(hits: hits2) + + let noErrorExpectation = expectation(description: "correct update") + noErrorExpectation.isInverted = true + + let resultsExpectation = expectation(description: "results") + resultsExpectation.expectedFulfillmentCount = 2 + + multiInteractor.onError.subscribe(with: self) { (_, _) in + noErrorExpectation.fulfill() + } + + multiInteractor.onResultsUpdated.subscribe(with: self) { (_, _) in + resultsExpectation.fulfill() + } + + multiInteractor.update(results1, forInteractorInSection: 0) + multiInteractor.update(results2, forInteractorInSection: 1) + + waitForExpectations(timeout: 3, handler: .none) + + } + + func testIncorrectUpdatePerInteractor() throws { + + let interactor1 = HitsInteractor<[String: Int]>() + let interactor2 = HitsInteractor<[String: Bool]>() + let multiInteractor = MultiIndexHitsInteractor(hitsInteractors: [interactor1, interactor2]) + + let hits1 = [["a": 1], ["b": 2], ["c": 3]].map(Hit.withJSON) + let results1 = SearchResponse(hits: hits1) + + let hits2 = [["a": true], ["b": false], ["c": true]].map(Hit.withJSON) + let results2 = SearchResponse(hits: hits2) + + let errorExpectation = expectation(description: "incorrect update") + errorExpectation.expectedFulfillmentCount = 2 + + let resultsExpectation = expectation(description: "results") + resultsExpectation.expectedFulfillmentCount = 2 + + multiInteractor.onError.subscribe(with: self) { (_, _) in + errorExpectation.fulfill() + } + + multiInteractor.onResultsUpdated.subscribe(with: self) { (_, _) in + resultsExpectation.fulfill() + } + + multiInteractor.update(results1, forInteractorInSection: 1) + multiInteractor.update(results2, forInteractorInSection: 0) + + waitForExpectations(timeout: 3, handler: .none) + + } + + func testUpdateSimultaneously() throws { + + let pageLoader = TestPageLoader() + + let interactor1 = HitsInteractor>() + interactor1.pageLoader = pageLoader + let interactor2 = HitsInteractor>() + interactor2.pageLoader = pageLoader + + let multiInteractor = MultiIndexHitsInteractor(hitsInteractors: [interactor1, interactor2]) + + let hits1 = [1, 2, 3].map(TestRecord.withValue) + let results1 = SearchResponse(hits: hits1) + + let hits2 = [true, false].map(TestRecord.withValue) + let results2 = SearchResponse(hits: hits2) + + let interactor1Exp = expectation(description: "Interactor 1") + let interactor2Exp = expectation(description: "Interactor 2") + let multiInteractorExp = expectation(description: "MultiInteractor expectation") + + interactor1.onResultsUpdated.subscribe(with: self) { (_, _) in + XCTAssertEqual(multiInteractor.numberOfSections(), 2) + XCTAssertEqual(multiInteractor.numberOfHits(inSection: 0), hits1.count) + interactor1Exp.fulfill() + } + + interactor2.onResultsUpdated.subscribe(with: self) { (_, _) in + XCTAssertEqual(multiInteractor.numberOfSections(), 2) + XCTAssertEqual(multiInteractor.numberOfHits(inSection: 1), hits2.count) + interactor2Exp.fulfill() + } + + multiInteractor.onResultsUpdated.subscribe(with: self) { (_, _) in + multiInteractorExp.fulfill() + } + + // Update multihits Interactor with a correct list of results + multiInteractor.update([results1, results2]) + + waitForExpectations(timeout: 3, handler: .none) + + } + + func testIncorrectUpdateSimultaneously() throws { + + let pageLoader = TestPageLoader() + + let interactor1 = HitsInteractor<[String: Int]>() + interactor1.pageLoader = pageLoader + let interactor2 = HitsInteractor<[String: Bool]>() + interactor2.pageLoader = pageLoader + + let multiInteractor = MultiIndexHitsInteractor(hitsInteractors: [interactor1, interactor2]) + + let hits1 = [1, 2, 3].map(TestRecord.withValue) + let results1 = SearchResponse(hits: hits1) + + let hits2 = [true, false].map(TestRecord.withValue) + let results2 = SearchResponse(hits: hits2) + + let interactor1Exp = expectation(description: "Interactor 1") + let interactor2Exp = expectation(description: "Interactor 2") + let multiInteractorExp = expectation(description: "MultiInteractor expectation") + multiInteractorExp.expectedFulfillmentCount = 2 + + interactor1.onError.subscribe(with: self) { (_, _) in + XCTAssertEqual(multiInteractor.numberOfSections(), 2) + XCTAssertEqual(multiInteractor.numberOfHits(inSection: 0), 0) + interactor1Exp.fulfill() + } + + interactor2.onError.subscribe(with: self) { (_, _) in + XCTAssertEqual(multiInteractor.numberOfSections(), 2) + XCTAssertEqual(multiInteractor.numberOfHits(inSection: 1), 0) + interactor2Exp.fulfill() + } + + multiInteractor.onError.subscribe(with: self) { (_, _) in + multiInteractorExp.fulfill() + } + + // Update multihits Interactor with a correct list of results + multiInteractor.update([results2, results1]) + + waitForExpectations(timeout: 3, handler: .none) + + } + + func testPartiallyCorrectUpdateSimultaneously() throws { + + let pageLoader = TestPageLoader() + + let interactor1 = HitsInteractor>() + interactor1.pageLoader = pageLoader + let interactor2 = HitsInteractor>() + interactor2.pageLoader = pageLoader + + let multiInteractor = MultiIndexHitsInteractor(hitsInteractors: [interactor1, interactor2]) + + let hits1 = [1, 2, 3].map(TestRecord.withValue) + let results1 = SearchResponse(hits: hits1) + + let hits2 = ["a", "b"].map(TestRecord.withValue) + let results2 = SearchResponse(hits: hits2) + + let interactor1Exp = expectation(description: "Interactor 1") + let interactor2Exp = expectation(description: "Interactor 2") + let multiInteractorResultsExp = expectation(description: "MultiInteractor results expectation") + let multiInteractorErrorExp = expectation(description: "MultiInteractor error expectation") + + interactor1.onResultsUpdated.subscribe(with: self) { (_, _) in + XCTAssertEqual(multiInteractor.numberOfSections(), 2) + XCTAssertEqual(multiInteractor.numberOfHits(inSection: 0), 3) + interactor1Exp.fulfill() + } + + interactor2.onError.subscribe(with: self) { (_, _) in + XCTAssertEqual(multiInteractor.numberOfSections(), 2) + XCTAssertEqual(multiInteractor.numberOfHits(inSection: 1), 0) + interactor2Exp.fulfill() + } + + multiInteractor.onResultsUpdated.subscribe(with: self) { (_, _) in +// print("ok") + multiInteractorResultsExp.fulfill() + } + + multiInteractor.onError.subscribe(with: self) { (_, _) in +// print("\(error)") + multiInteractorErrorExp.fulfill() + } + + // Update multihits Interactor with a correct list of results + multiInteractor.update([results1, results2]) + + waitForExpectations(timeout: 100, handler: .none) + + } + + func testHitForRow() throws { + + let pageLoader = TestPageLoader() + + let interactor1 = HitsInteractor>() + interactor1.pageLoader = pageLoader + let interactor2 = HitsInteractor>() + interactor2.pageLoader = pageLoader + + let multiInteractor = MultiIndexHitsInteractor(hitsInteractors: [interactor1, interactor2]) + + let hits1 = [1, 2, 3].map(TestRecord.withValue) + let results1 = SearchResponse(hits: hits1) + + let hits2 = [true, false].map(TestRecord.withValue) + let results2 = SearchResponse(hits: hits2) + + let resultsUpdatedExp = expectation(description: "Results updated") + + multiInteractor.onResultsUpdated.subscribe(with: self) { (_, _) in + + do { + + XCTAssertNoThrow(try multiInteractor.hit(atIndex: 0, inSection: 0) as TestRecord?) + XCTAssertNoThrow(try multiInteractor.hit(atIndex: 1, inSection: 1) as TestRecord?) + XCTAssertThrowsError(try multiInteractor.hit(atIndex: 0, inSection: 0) as TestRecord?) + XCTAssertThrowsError(try multiInteractor.hit(atIndex: 1, inSection: 1) as TestRecord?) + + let hit1 = try multiInteractor.hit(atIndex: 0, inSection: 0) as TestRecord? + XCTAssertEqual(hit1?.value, 1) + + let hit2 = try multiInteractor.hit(atIndex: 1, inSection: 1) as TestRecord? + XCTAssertEqual(hit2?.value, false) + + } catch let error { + XCTFail("Unexpected error \(error)") + } + + resultsUpdatedExp.fulfill() + } + + multiInteractor.update([results1, results2]) + + waitForExpectations(timeout: 3, handler: .none) + + } + + class TestHitsInteractor: AnyHitsInteractor { + + func getCurrentGenericHits() throws -> [R] where R: Decodable { + return [] + } + + func getCurrentRawHits() -> [[String: Any]] { + return [] + } + + var onError: Observer = .init() + + var pageLoader: PageLoadable? + + var didCallLoadMoreResults: () -> Void + + init(didCallLoadMoreResults: @escaping () -> Void) { + self.didCallLoadMoreResults = didCallLoadMoreResults + } + + func update(_ searchResults: HitsExtractable & SearchStatsConvertible) -> Operation { + return Operation() + } + + func process(_ error: Error, for query: Query) { + + } + + func notifyQueryChanged() { + + } + + func rawHitAtIndex(_ index: Int) -> [String: Any]? { + return .none + } + + func numberOfHits() -> Int { + return 0 + } + + func genericHitAtIndex(_ index: Int) throws -> R? where R: Decodable { + return (0 as! R) + } + + func loadMoreResults() { + didCallLoadMoreResults() + } + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/MultiSourceHitsReloaderTests.swift b/Tests/InstantSearchCoreTests/Unit/MultiSourceHitsReloaderTests.swift new file mode 100644 index 00000000..ebd0c59d --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/MultiSourceHitsReloaderTests.swift @@ -0,0 +1,74 @@ +// +// MultiSourceHitsReloaderTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 11/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class TestResultUpdatable: ResultUpdatable { + + var results: [Int] + + var onResultsUpdated: Observer<[Int]> + + init() { + self.results = [] + self.onResultsUpdated = .init() + } + + @discardableResult func update(_ results: [Int]) -> Operation { + self.results = results + onResultsUpdated.fire(results) + return .init() + } + +} + +class MultiSourceHitsReloaderTests: XCTestCase { + + func testUpdate() { + + let interactor1 = TestResultUpdatable() + let results1 = [1, 2, 3] + + let interactor2 = TestResultUpdatable() + let results2 = [4, 5, 6] + + let interactor3 = TestResultUpdatable() + let results3 = [7, 8, 9] + + let controller = TestHitsController() + + let connection = MultiSourceReloadNotifier(target: controller) + + connection.register(interactor1) + connection.register(interactor2) + connection.register(interactor3) + + let reloadExpectation = expectation(description: "Reload") + + controller.didReload = { + interactor1.results = results1 + interactor2.results = results2 + interactor3.results = results3 + reloadExpectation.fulfill() + } + + connection.notifyReload() + + let operationQueue = OperationQueue() + + let operations = zip([interactor1, interactor2, interactor3], [results1, results2, results3]).map { (i, r) in BlockOperation { i.update(r) } } + + operationQueue.addOperations(operations, waitUntilFinished: false) + + waitForExpectations(timeout: 10, handler: .none) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Number/NumberInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/Number/NumberInteractorTests.swift new file mode 100644 index 00000000..e755b2cc --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Number/NumberInteractorTests.swift @@ -0,0 +1,22 @@ +// +// NumberInteractorTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/02/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class NumberInteractorTestsTests: XCTestCase { + + func testInit() { + _ = NumberInteractor(item: Int(1)) + _ = NumberInteractor(item: UInt(1)) + _ = NumberInteractor(item: Float(1)) + _ = NumberInteractor(item: Double(1)) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Number/NumberRangeInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/Number/NumberRangeInteractorTests.swift new file mode 100644 index 00000000..694ecfea --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Number/NumberRangeInteractorTests.swift @@ -0,0 +1,22 @@ +// +// NumberRangeInteractorTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 10/02/2020. +// Copyright © 2020 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class NumberRangeInteractorTestsTests: XCTestCase { + + func testInit() { + _ = NumberRangeInteractor(item: Int(1)...Int(10)) + _ = NumberRangeInteractor(item: UInt(1)...UInt(10)) + _ = NumberRangeInteractor(item: Float(1)...Float(10)) + _ = NumberRangeInteractor(item: Double(1)...Double(10)) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/PageMapTests.swift b/Tests/InstantSearchCoreTests/Unit/PageMapTests.swift new file mode 100644 index 00000000..bbc2f7ee --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/PageMapTests.swift @@ -0,0 +1,245 @@ +// +// PageMapTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 14/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class PageMapTests: XCTestCase { + + func toPages(_ tuples: [(index: Int, items: [T])]) -> [PageMap.Page] { + return tuples.map { PageMap.Page(index: $0.index, items: $0.items) } + } + + func testConstructionWithSequence() { + + let pageMap = PageMap(["i1", "i2", "i3"])! + + XCTAssertEqual(pageMap.loadedPages, [ + .init(index: 0, items: ["i1", "i2", "i3"]) + ]) + XCTAssertEqual(pageMap.latestPageIndex, 0) + XCTAssertEqual(pageMap.loadedPagesCount, 1) + XCTAssertEqual(pageMap.count, 3) + + } + + func testConstructionWithDictionary() { + + XCTAssertNil(PageMap([:])) + + let p0 = ["i1", "i2", "i3"] + let p1 = ["i4", "i5"] + + let dictionary = [ + 0: p0, + 1: p1 + ] + + let expectedPages: [PageMap.Page] = [ + .init(index: 0, items: p0), + .init(index: 1, items: p1) + ] + guard let pageMap = PageMap(dictionary) else { + XCTFail("PageMap must be correctly constructed") + return + } + + XCTAssertEqual(pageMap.loadedPages, expectedPages) + XCTAssertEqual(pageMap.latestPageIndex, 1) + XCTAssertEqual(pageMap.loadedPagesCount, 2) + XCTAssertEqual(pageMap.count, 5) + + } + + func testTotalPagesCount() { + + let pageMap = PageMap([0: ["i1"], 1: ["i2"]]) + + XCTAssertEqual(pageMap?.totalPagesCount, 2) + + let pageMap2 = PageMap([0: ["i1"], 10: ["i2"]]) + + XCTAssertEqual(pageMap2?.totalPagesCount, 11) + + } + + func testIteration() { + + let dictionary = [0: ["i1", "i2", "i3"], 1: ["i4", "i5"]] + + let expectedSequence = dictionary.sorted { $0.key < $1.key }.map { $0.value }.flatMap { $0 } + + guard let pageMap = PageMap(dictionary) else { + XCTFail("PageMap must be correctly constructed") + return + } + + XCTAssertEqual(Array(pageMap), expectedSequence) + + } + + func testInsertion() { + + let p0 = ["i1", "i2", "i3"] + let p1 = ["i4", "i5", "i6"] + let pageMap = PageMap(p0)! + + let updatedPageMap = pageMap.inserting(p1, withIndex: 1) + + XCTAssertEqual(updatedPageMap.loadedPages, [ + .init(index: 0, items: p0), + .init(index: 1, items: p1) + ]) + XCTAssertEqual(updatedPageMap.latestPageIndex, 1) + XCTAssertEqual(updatedPageMap.loadedPagesCount, 2) + XCTAssertEqual(updatedPageMap.count, 6) + + } + + func testInsertionKeepingMissingPage() { + + let p0 = ["i4", "i5", "i6"] + let p2 = ["i10", "i11", "i12"] + + var pageMap = PageMap(p0)! + + XCTAssertEqual(pageMap.loadedPages, [.init(index: 0, items: p0)]) + XCTAssertEqual(pageMap.latestPageIndex, 0) + XCTAssertEqual(pageMap.loadedPagesCount, 1) + XCTAssertEqual(pageMap.totalPagesCount, 1) + XCTAssertEqual(pageMap.count, 3) + XCTAssertTrue(pageMap.containsPage(atIndex: 0)) + XCTAssertFalse(pageMap.containsPage(atIndex: 1)) + + pageMap.insert(p2, withIndex: 2) + + XCTAssertEqual(pageMap.loadedPages, toPages([(0, p0), (2, p2)])) + XCTAssertEqual(pageMap.latestPageIndex, 2) + XCTAssertEqual(pageMap.loadedPagesCount, 2) + XCTAssertEqual(pageMap.totalPagesCount, 3) + XCTAssertEqual(pageMap.count, 9) + XCTAssertTrue(pageMap.containsPage(atIndex: 0)) + XCTAssertFalse(pageMap.containsPage(atIndex: 1)) + XCTAssertTrue(pageMap.containsPage(atIndex: 2)) + + let expectedSequence: [String?] = p0 + Array(repeating: nil, count: 3) + p2 + + XCTAssertEqual(Array(pageMap), expectedSequence) + + } + + func testContainsItem() { + + let p0 = ["i4", "i5", "i6"] + let pageMap = PageMap(p0)! + + XCTAssertTrue(pageMap.containsItem(atIndex: 1)) + XCTAssertFalse(pageMap.containsItem(atIndex: 4)) + XCTAssertTrue(pageMap.containsPage(atIndex: 0)) + XCTAssertFalse(pageMap.containsPage(atIndex: 1)) + XCTAssertEqual(pageMap[0], "i4") + XCTAssertEqual(pageMap[1], "i5") + XCTAssertEqual(pageMap[2], "i6") + + } + + func testPageMapConvertibleInit() { + let items = ["i1", "i11", "i21"] + let testPageMapConvertible = TestPageable(index: 5, items: items) + let pageMap = PageMap(testPageMapConvertible)! + XCTAssertEqual(pageMap.loadedPages, toPages([(5, items)])) + } + + func testCleanUp() { + + let page0 = (0...10).map { "a\($0)" } + let page1 = (0...10).map { "b\($0)" } + let page2 = (0...10).map { "c\($0)" } + let page3 = (0...10).map { "d\($0)" } + + var pageMap = PageMap([0: page0, 1: page1, 2: page2, 3: page3]) + + pageMap?.cleanUp(basePageIndex: 1, keepingPagesOffset: 1) + + XCTAssertEqual(pageMap?.loadedPages, [(0, page0), (1, page1), (2, page2)].map { PageMap.Page(index: $0.0, items: $0.1) }) + + } + + func testCleanUp2() { + + let page0 = (0...10).map { "a\($0)" } + let page1 = (0...10).map { "b\($0)" } + let page2 = (0...10).map { "c\($0)" } + let page3 = (0...10).map { "d\($0)" } + + var pageMap = PageMap([0: page0, 1: page1, 2: page2, 3: page3]) + + pageMap?.cleanUp(basePageIndex: 2, keepingPagesOffset: 1) + + XCTAssertEqual(pageMap?.loadedPages, toPages([(1, page1), (2, page2), (3, page3)])) + + } + + func testCleanUp3() { + + let page0 = (0...10).map { "a\($0)" } + let page1 = (0...10).map { "b\($0)" } + let page2 = (0...10).map { "c\($0)" } + let page3 = (0...10).map { "d\($0)" } + + var pageMap = PageMap([0: page0, 1: page1, 2: page2, 3: page3]) + pageMap?.cleanUp(basePageIndex: 2, keepingPagesOffset: 0) + + XCTAssertEqual(pageMap?.loadedPages, toPages([(2, page2)])) + + } + + func testCleanUp4() { + + let page0 = (0...10).map { "a\($0)" } + let page1 = (0...10).map { "b\($0)" } + let page2 = (0...10).map { "c\($0)" } + let page3 = (0...10).map { "d\($0)" } + + var pageMap = PageMap([0: page0, 1: page1, 2: page2, 3: page3]) + pageMap?.cleanUp(basePageIndex: 2, keepingPagesOffset: 3) + + XCTAssertEqual(pageMap?.loadedPages, toPages([(0, page0), (1, page1), (2, page2), (3, page3)])) + + } + + func testCleanUpFirstElement() { + + let page0 = (0...10).map { "a\($0)" } + let page1 = (0...10).map { "b\($0)" } + let page2 = (0...10).map { "c\($0)" } + let page3 = (0...10).map { "d\($0)" } + + var pageMap = PageMap([0: page0, 1: page1, 2: page2, 3: page3]) + pageMap?.cleanUp(basePageIndex: 0, keepingPagesOffset: 1) + + XCTAssertEqual(pageMap?.loadedPages, toPages([(0, page0), (1, page1)])) + + } + + func testCleanUpLastElement() { + + let page0 = (0...10).map { "a\($0)" } + let page1 = (0...10).map { "b\($0)" } + let page2 = (0...10).map { "c\($0)" } + let page3 = (0...10).map { "d\($0)" } + + var pageMap = PageMap([0: page0, 1: page1, 2: page2, 3: page3]) + pageMap?.cleanUp(basePageIndex: 3, keepingPagesOffset: 1) + + XCTAssertEqual(pageMap?.loadedPages, toPages([(2, page2), (3, page3)])) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift b/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift new file mode 100644 index 00000000..0ada0bd3 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/PaginatorTests.swift @@ -0,0 +1,97 @@ +// +// PaginatorTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 14/03/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +struct TestPageable: Pageable { + typealias PageItem = Item + + var index: Int + + var items: [Item] +} + +class TestPageLoaderDelegate: PageLoadable { + + var requestedLoadPage: ((Int) -> Void)? + + func loadPage(atIndex pageIndex: Int) { + requestedLoadPage?(pageIndex) + } + +} + +class PaginatorTests: XCTestCase { + + func toPages(_ tuples: [(index: Int, items: [T])]) -> [PageMap.Page] { + return tuples.map { PageMap.Page(index: $0.index, items: $0.items) } + } + + func testProcessing() { + + let paginator = Paginator() + + let p0 = ["i1", "i2", "i3"] + let p1 = ["i4", "i5", "i6"] + + XCTAssertNil(paginator.pageMap) + + // Adding a first page of dataset + + let page0 = TestPageable(index: 0, items: p0) + + paginator.process(page0) + + guard let pageMap0 = paginator.pageMap else { + XCTFail("PageMap cannot be nil after page processing") + return + } + + XCTAssertEqual(pageMap0.loadedPages, [.init(index: 0, items: p0)]) + XCTAssertEqual(pageMap0.latestPageIndex, page0.index) + XCTAssertEqual(pageMap0.loadedPagesCount, 1) + XCTAssertEqual(pageMap0.count, 3) + + // Adding another page for same dataset + + let page1 = TestPageable(index: 1, items: p1) + + paginator.process(page1) + + guard let pageMap1 = paginator.pageMap else { + XCTFail("PageMap cannot be nil after page processing") + return + } + + XCTAssertEqual(pageMap1.loadedPages, toPages([(0, p0), (1, p1)])) + XCTAssertEqual(pageMap1.latestPageIndex, page1.index) + XCTAssertEqual(pageMap1.loadedPagesCount, 2) + XCTAssertEqual(pageMap1.count, 6) + + } + + func testInvalidation() { + + let paginator = Paginator() + let page0 = TestPageable(index: 0, items: ["i1", "i2", "i3"]) + let page1 = TestPageable(index: 0, items: ["i4", "i5", "i6"]) + paginator.process(page0) + XCTAssertNotNil(paginator.pageMap) + XCTAssertEqual(paginator.pageMap?.count, 3) + XCTAssertFalse(paginator.isInvalidated) + paginator.invalidate() + XCTAssertTrue(paginator.isInvalidated) + paginator.process(page1) + XCTAssertNotNil(paginator.pageMap) + XCTAssertEqual(paginator.pageMap?.count, 3) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/QueryInput/QueryInputControllerConnectionTests.swift b/Tests/InstantSearchCoreTests/Unit/QueryInput/QueryInputControllerConnectionTests.swift new file mode 100644 index 00000000..7b8376f3 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/QueryInput/QueryInputControllerConnectionTests.swift @@ -0,0 +1,116 @@ +// +// QueryInputControllerConnectionTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class QueryInputControllerConnectionTests: XCTestCase { + + weak var disposableInteractor: QueryInputInteractor? + weak var disposableController: TestQueryInputController? + + func testLeak() { + let controller = TestQueryInputController() + let interactor = QueryInputInteractor() + + disposableController = controller + disposableInteractor = interactor + + let connection = QueryInputInteractor.ControllerConnection(interactor: interactor, controller: controller) + connection.connect() + } + + override func tearDown() { + XCTAssertNil(disposableInteractor, "Leaked interactor") + XCTAssertNil(disposableInteractor, "Leaked controller") + } + + func testConnect() { + + let controller = TestQueryInputController() + let interactor = QueryInputInteractor() + let presetQuery = "q1" + interactor.query = presetQuery + + let connection = QueryInputInteractor.ControllerConnection(interactor: interactor, controller: controller) + + connection.connect() + + check(interactor: interactor, + controller: controller, + presetQuery: presetQuery, + isConnected: true) + + } + + func testConnectMethod() { + + let controller = TestQueryInputController() + let interactor = QueryInputInteractor() + let presetQuery = "q1" + interactor.query = presetQuery + + interactor.connectController(controller) + + check(interactor: interactor, + controller: controller, + presetQuery: presetQuery, + isConnected: true) + + } + + func testDisconnect() { + + let controller = TestQueryInputController() + let interactor = QueryInputInteractor() + + let connection = QueryInputInteractor.ControllerConnection(interactor: interactor, controller: controller) + + connection.connect() + connection.disconnect() + + check(interactor: interactor, + controller: controller, + presetQuery: nil, + isConnected: false) + + } + + func check(interactor: QueryInputInteractor, + controller: TestQueryInputController, + presetQuery: String?, + isConnected: Bool) { + + XCTAssertEqual(controller.query, presetQuery) + + controller.query = "q2" + + if isConnected { + XCTAssertEqual(interactor.query, "q2") + } else { + XCTAssertNil(interactor.query) + } + + controller.query = "q3" + + let querySubmittedExpectation = expectation(description: "query submitted") + querySubmittedExpectation.isInverted = !isConnected + + interactor.onQuerySubmitted.subscribe(with: self) { _, query in + XCTAssertEqual(query, "q3") + querySubmittedExpectation.fulfill() + } + + controller.submitQuery() + + waitForExpectations(timeout: 2, handler: nil) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/QueryInput/QueryInputInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/QueryInput/QueryInputInteractorTests.swift new file mode 100644 index 00000000..163b4b57 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/QueryInput/QueryInputInteractorTests.swift @@ -0,0 +1,61 @@ +// +// QueryInputInteractorTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 28/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class QueryInputInteractorTests: XCTestCase { + + func testOnQueryChangedEvent() { + + let interactor = QueryInputInteractor() + + let onQueryChangedExpectation = expectation(description: "on query changed") + + let changedQuery = "q1" + + interactor.onQueryChanged.subscribe(with: self) { _, query in + XCTAssertEqual(query, changedQuery) + onQueryChangedExpectation.fulfill() + } + + interactor.query = changedQuery + + waitForExpectations(timeout: 2, handler: nil) + + } + + func testOnQuerySubmittedEvent() { + + let interactor = QueryInputInteractor() + let onQuerySubmittedExpectation = expectation(description: "on query submitted") + let submittedQuery = "q2" + + interactor.onQuerySubmitted.subscribe(with: self) { _, query in + XCTAssertEqual(submittedQuery, query) + onQuerySubmittedExpectation.fulfill() + } + + interactor.query = submittedQuery + interactor.submitQuery() + + waitForExpectations(timeout: 2, handler: nil) + + } + + func testSearcherQuerySet() { + let searcher = TestSearcher() + let interactor = QueryInputInteractor() + let query = "q1" + searcher.query = query + interactor.connectSearcher(searcher, searchTriggeringMode: .searchOnSubmit) + XCTAssertEqual(interactor.query, query) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/QueryInput/QueryInputSearcherConnectionTests.swift b/Tests/InstantSearchCoreTests/Unit/QueryInput/QueryInputSearcherConnectionTests.swift new file mode 100644 index 00000000..73b2f437 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/QueryInput/QueryInputSearcherConnectionTests.swift @@ -0,0 +1,135 @@ +// +// QueryInputSearcherConnectionTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class QueryInputSearcherConnectionTests: XCTestCase { + + weak var disposableSearcher: TestSearcher? + weak var disposableInteractor: QueryInputInteractor? + + func testLeak() { + let searcher = TestSearcher() + let interactor = QueryInputInteractor() + let connection = QueryInputInteractor.SearcherConnection(interactor: interactor, searcher: searcher, searchTriggeringMode: .searchAsYouType) + connection.connect() + } + + override func tearDown() { + XCTAssertNil(disposableSearcher, "Leaked searcher") + XCTAssertNil(disposableInteractor, "Leaked interactor") + } + + func testSearchAsYouTypeConnect() { + let searcher = TestSearcher() + let interactor = QueryInputInteractor() + check(connect: true, interactor: interactor, searcher: searcher, mode: .searchAsYouType) + } + + func testSearchAsYouTypeConnectMethod() { + let searcher = TestSearcher() + let interactor = QueryInputInteractor() + interactor.connectSearcher(searcher, searchTriggeringMode: .searchAsYouType) + checkConnection(interactor: interactor, + searcher: searcher, + triggeringMode: .searchAsYouType, + isConnected: true) + } + + func testSearchAsYouTypeDisconnect() { + let searcher = TestSearcher() + let interactor = QueryInputInteractor() + check(connect: false, interactor: interactor, searcher: searcher, mode: .searchAsYouType) + } + + func testSearchOnSubmitConnect() { + let searcher = TestSearcher() + let interactor = QueryInputInteractor() + check(connect: true, interactor: interactor, searcher: searcher, mode: .searchOnSubmit) + } + + func testSearchOnSubmitConnectMethod() { + let searcher = TestSearcher() + let interactor = QueryInputInteractor() + interactor.connectSearcher(searcher, searchTriggeringMode: .searchOnSubmit) + checkConnection(interactor: interactor, + searcher: searcher, + triggeringMode: .searchOnSubmit, + isConnected: true) + } + + func testSearchOnSubmitDisconnect() { + let searcher = TestSearcher() + let interactor = QueryInputInteractor() + check(connect: false, interactor: interactor, searcher: searcher, mode: .searchOnSubmit) + } + + func check(connect: Bool, + interactor: QueryInputInteractor, + searcher: TestSearcher, + mode: SearchTriggeringMode) { + let connection = QueryInputInteractor.SearcherConnection(interactor: interactor, searcher: searcher, searchTriggeringMode: mode) + connection.connect() + + if connect { + checkConnection(interactor: interactor, + searcher: searcher, + triggeringMode: mode, + isConnected: true) + } else { + connection.disconnect() + + checkConnection(interactor: interactor, + searcher: searcher, + triggeringMode: mode, + isConnected: false) + } + + } + + func checkConnection(interactor: QueryInputInteractor, + searcher: TestSearcher, + triggeringMode: SearchTriggeringMode, + isConnected: Bool) { + + let query = "q1" + + let launchSearchExpectation = expectation(description: "search launched search") + launchSearchExpectation.isInverted = !isConnected + + let queryChangedExpectation = expectation(description: "query changed expectation") + + let querySubmittedExpectation = expectation(description: "query submitted expectation") + querySubmittedExpectation.isInverted = triggeringMode != .searchOnSubmit + + interactor.onQuerySubmitted.subscribe(with: self) { _, _ in + querySubmittedExpectation.fulfill() + } + + interactor.onQueryChanged.subscribe(with: self) { (_, _) in + queryChangedExpectation.fulfill() + } + + searcher.didLaunchSearch = { + XCTAssertEqual(searcher.query, query) + launchSearchExpectation.fulfill() + } + + interactor.query = query + + if triggeringMode == .searchOnSubmit { + interactor.submitQuery() + } + + waitForExpectations(timeout: 5, handler: nil) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/QueryInput/TestQueryInputController.swift b/Tests/InstantSearchCoreTests/Unit/QueryInput/TestQueryInputController.swift new file mode 100644 index 00000000..06f787fe --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/QueryInput/TestQueryInputController.swift @@ -0,0 +1,32 @@ +// +// TestQueryInputController.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import InstantSearchCore + +class TestQueryInputController: QueryInputController { + + var query: String? { + didSet { + guard oldValue != query else { return } + onQueryChanged?(query) + } + } + + var onQueryChanged: ((String?) -> Void)? + var onQuerySubmitted: ((String?) -> Void)? + + func setQuery(_ query: String?) { + self.query = query + } + + func submitQuery() { + onQuerySubmitted?(query) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/QueryInput/TestSearcher.swift b/Tests/InstantSearchCoreTests/Unit/QueryInput/TestSearcher.swift new file mode 100644 index 00000000..e826d1b0 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/QueryInput/TestSearcher.swift @@ -0,0 +1,34 @@ +// +// TestSearcher.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 04/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import InstantSearchCore + +class TestSearcher: Searcher { + + var query: String? { + didSet { + guard oldValue != query else { return } + onQueryChanged.fire(query) + } + } + + var didLaunchSearch: (() -> Void)? + + var isLoading: Observer = .init() + + var onQueryChanged: Observer = .init() + + func search() { + didLaunchSearch?() + } + + func cancel() { + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/SelectableInteractorConnectorsTests.swift b/Tests/InstantSearchCoreTests/Unit/SelectableInteractorConnectorsTests.swift new file mode 100644 index 00000000..d1eb5263 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/SelectableInteractorConnectorsTests.swift @@ -0,0 +1,126 @@ +// +// SelectableInteractorConnectorsTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 20/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class TestSelectableController: SelectableController { + + var item: Item? + var onClick: ((Bool) -> Void)? + + var isSelected: Bool = false + + var onSelectedChanged: (() -> Void)? + var onItemChanged: (() -> Void)? + + func setSelected(_ isSelected: Bool) { + self.isSelected = isSelected + onSelectedChanged?() + } + + func setItem(_ item: Item) { + self.item = item + onItemChanged?() + } + + func toggle() { + isSelected = !isSelected + onClick?(isSelected) + } + +} + +class SelectableInteractorConnectorsTests: XCTestCase { + + func testConnectFilterState() { + + let filterState = FilterState() + + let interactor = SelectableInteractor(item: "tag") + + interactor.connectFilterState(filterState) + + // Interactor to FilterState + + XCTAssertTrue(filterState.filters.isEmpty) + + interactor.computeIsSelected(selecting: true) + + let groupID: FilterGroup.ID = .or(name: "_tags", filterType: .tag) + + XCTAssertTrue(filterState.filters.contains(Filter.Tag("tag"), inGroupWithID: groupID)) + + interactor.computeIsSelected(selecting: false) + + XCTAssertTrue(filterState.filters.isEmpty) + + // FilterState to Interactor + + filterState.notify(.add(filter: Filter.Tag("tag"), toGroupWithID: groupID)) + + XCTAssertTrue(interactor.isSelected) + + } + + func testConnectController() { + + let interactor = SelectableInteractor(item: "tag") + + interactor.isSelected = true + + let controller = TestSelectableController() + + let onChangeExp = expectation(description: "on change") + onChangeExp.expectedFulfillmentCount = 3 + + // Pre-selection transmission + + controller.onSelectedChanged = { + XCTAssertTrue(controller.isSelected) + onChangeExp.fulfill() + } + + interactor.connectController(controller) + + // Interactor -> Controller + + controller.onSelectedChanged = { + XCTAssertFalse(controller.isSelected) + onChangeExp.fulfill() + } + + interactor.isSelected = false + + waitForExpectations(timeout: 5, handler: nil) + } + + func testConnectControllerToggle() { + let interactor = SelectableInteractor(item: "tag") + + interactor.isSelected = false + + let controller = TestSelectableController() + + interactor.connectController(controller) + + let selectedComputedExpectation = expectation(description: "selected computed") + + interactor.onSelectedComputed.subscribe(with: self) { _, isSelected in + XCTAssertTrue(isSelected) + selectedComputedExpectation.fulfill() + } + + controller.toggle() + + waitForExpectations(timeout: 5, handler: nil) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/SelectableInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/SelectableInteractorTests.swift new file mode 100644 index 00000000..a42972ac --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/SelectableInteractorTests.swift @@ -0,0 +1,81 @@ +// +// SelectableInteractorTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 20/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class SelectableInteractorTests: XCTestCase { + + typealias VM = SelectableInteractor + + func testConstruction() { + + let interactor = SelectableInteractor(item: "i") + + XCTAssertFalse(interactor.isSelected) + XCTAssertEqual(interactor.item, "i") + + } + + func testSwitchItem() { + + let interactor = SelectableInteractor(item: "i") + + let switchItemExpectation = expectation(description: "item changed") + + interactor.onItemChanged.subscribe(with: self) { _, newItem in + XCTAssertEqual(newItem, "o") + switchItemExpectation.fulfill() + } + + interactor.item = "o" + + waitForExpectations(timeout: 2, handler: nil) + } + + func testSelection() { + + let interactor = SelectableInteractor(item: "i") + + let selectionExpectation = expectation(description: "item selected") + let deselectionExpectation = expectation(description: "item deselected") + + interactor.onSelectedChanged.subscribe(with: self) { _, isSelected in + if isSelected { + selectionExpectation.fulfill() + } else { + deselectionExpectation.fulfill() + } + } + + interactor.isSelected = true + interactor.isSelected = false + + waitForExpectations(timeout: 2, handler: nil) + + } + + func testSelectedComputed() { + + let interactor = SelectableInteractor(item: "i") + + let selectedComputedExpectation = expectation(description: "computed selected") + + interactor.onSelectedComputed.subscribe(with: self) { _, isSelected in + XCTAssertEqual(isSelected, false) + selectedComputedExpectation.fulfill() + } + + interactor.computeIsSelected(selecting: false) + + waitForExpectations(timeout: 2, handler: nil) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/SelectableListInteractorFilterConnectorsTests.swift b/Tests/InstantSearchCoreTests/Unit/SelectableListInteractorFilterConnectorsTests.swift new file mode 100644 index 00000000..02ad1d52 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/SelectableListInteractorFilterConnectorsTests.swift @@ -0,0 +1,176 @@ +// +// SelectableListInteractorFilterConnectorsTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 21/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class SelectableListInteractorFilterConnectorsTests: XCTestCase { + + func testConstructors() { + + let facetFilterListInteractor = FilterListInteractor() + XCTAssertEqual(facetFilterListInteractor.selectionMode, .multiple) + + let numericFilterListInteractor = FilterListInteractor() + XCTAssertEqual(numericFilterListInteractor.selectionMode, .single) + + let tagFilterListInteractor = FilterListInteractor() + XCTAssertEqual(tagFilterListInteractor.selectionMode, .multiple) + + } + + func testFilterStateConnector() { + + let interactor = FilterListInteractor() + + interactor.items = [ + "tag1", "tag2", "tag3" + ] + + let filterState = FilterState() + + filterState.notify(.add(filter: Filter.Tag(value: "tag3"), toGroupWithID: .or(name: "", filterType: .tag))) + + interactor.connectFilterState(filterState, operator: .or) + + // FilterState -> Interactor preselection + + XCTAssertEqual(interactor.selections, ["tag3"]) + + // FilterState -> Interactor + + filterState.notify(.add(filter: Filter.Tag(value: "tag1"), toGroupWithID: .or(name: "", filterType: .tag))) + + XCTAssertEqual(interactor.selections, ["tag1", "tag3"]) + + // Interactor -> FilterState + + interactor.computeSelections(selectingItemForKey: "tag2") + + XCTAssertTrue(filterState.contains(Filter.Tag(value: "tag2"), inGroupWithID: .or(name: "", filterType: .tag))) + + } + + class TestController: SelectableListController { + + typealias Item = F + + var onClick: ((F) -> Void)? + var didReload: (() -> Void)? + + var selectableItems: [(item: F, isSelected: Bool)] = [] + + func setSelectableItems(selectableItems: [(item: F, isSelected: Bool)]) { + self.selectableItems = selectableItems + } + + func reload() { + didReload?() + } + + func clickOn(_ item: F) { + onClick?(item) + } + + } + + func testControllerConnector() { + + let interactor = FilterListInteractor() + let controller = TestController() + + interactor.items = ["tag1", "tag2", "tag3"] + interactor.selections = ["tag2"] + + interactor.connectController(controller) + + // Test preselection + + XCTAssertEqual(controller.selectableItems.map { $0.0 }, [ + Filter.Tag(value: "tag1"), + Filter.Tag(value: "tag2"), + Filter.Tag(value: "tag3") + ]) + + XCTAssertEqual(controller.selectableItems.map { $0.1 }, [ + false, + true, + false + ]) + + // Items change + + let itemsChangedReloadExpectation = expectation(description: "items changed reload expectation") + + controller.didReload = { + + XCTAssertEqual(controller.selectableItems.map { $0.0 }, [ + Filter.Tag(value: "tag1"), + Filter.Tag(value: "tag2"), + Filter.Tag(value: "tag3"), + Filter.Tag(value: "tag4") + ]) + + XCTAssertEqual(controller.selectableItems.map { $0.1 }, [ + false, + true, + false, + false + ]) + + itemsChangedReloadExpectation.fulfill() + } + + interactor.items = ["tag1", "tag2", "tag3", "tag4"] + + waitForExpectations(timeout: 2, handler: nil) + + // Selection change + + let selectionsChangedReloadExpectation = expectation(description: "selections changed reload expectation") + + controller.didReload = { + + XCTAssertEqual(controller.selectableItems.map { $0.0 }, [ + Filter.Tag(value: "tag1"), + Filter.Tag(value: "tag2"), + Filter.Tag(value: "tag3"), + Filter.Tag(value: "tag4") + ]) + + XCTAssertEqual(controller.selectableItems.map { $0.1 }, [ + false, + false, + true, + true + ]) + + selectionsChangedReloadExpectation.fulfill() + } + + interactor.selections = ["tag3", "tag4"] + + waitForExpectations(timeout: 2, handler: nil) + + // Selection computation on click + + let selectionsComputedExpectation = expectation(description: "selections computed") + + interactor.onSelectionsComputed.subscribe(with: self) { _, selectedTags in + XCTAssertEqual(selectedTags, ["tag1", "tag3", "tag4"]) + selectionsComputedExpectation.fulfill() + } + + controller.clickOn("tag1") + + waitForExpectations(timeout: 2, handler: nil) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/SelectableListInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/SelectableListInteractorTests.swift new file mode 100644 index 00000000..1bf0aff8 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/SelectableListInteractorTests.swift @@ -0,0 +1,150 @@ +// +// SelectableListInteractorTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 20/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class SelectableListInteractorTests: XCTestCase { + + typealias VM = SelectableListInteractor + + func testConstruction() { + + let interactor = VM(items: [], selectionMode: .single) + + XCTAssert(interactor.items.isEmpty) + XCTAssertEqual(interactor.selectionMode, .single) + + let anotherInteractor = VM(items: ["s1", "s2"], selectionMode: .multiple) + + XCTAssertEqual(anotherInteractor.items, ["s1", "s2"]) + XCTAssertEqual(anotherInteractor.selectionMode, .multiple) + + } + + func testSwitchItems() { + + let interactor = VM(items: [], selectionMode: .single) + + let items = ["s1", "s2", "s3"] + let switchItemsExpectation = expectation(description: "switch items") + + interactor.onItemsChanged.subscribe(with: self) { _, newItems in + XCTAssertEqual(newItems, items) + switchItemsExpectation.fulfill() + } + + interactor.items = items + + waitForExpectations(timeout: 2, handler: .none) + } + + func testOnSelectionsChanged() { + let interactor = VM(items: ["s1", "s2", "s3"], selectionMode: .single) + + let selections = Set(["s2"]) + let selectionChangedExpectatiom = expectation(description: "selection") + + interactor.onSelectionsChanged.subscribe(with: self) { _, dispatchedSelections in + XCTAssertEqual(dispatchedSelections, selections) + selectionChangedExpectatiom.fulfill() + } + + interactor.selections = selections + + waitForExpectations(timeout: 2, handler: .none) + } + + func testOnSelectionsComputedSingle() { + + let interactor = VM(items: ["s1", "s2", "s3"], selectionMode: .single) + interactor.selections = ["s2"] + let deselectionExpectation = expectation(description: "deselection") + + interactor.onSelectionsComputed.subscribe(with: self) { _, dispatchedSelections in + XCTAssert(dispatchedSelections.isEmpty) + deselectionExpectation.fulfill() + } + + interactor.computeSelections(selectingItemForKey: "s2") + + waitForExpectations(timeout: 2, handler: .none) + + interactor.onSelectionsComputed.cancelAllSubscriptions() + + let selectionExpectation = expectation(description: "selection expectation") + + interactor.onSelectionsComputed.subscribe(with: self) { _, dispatchedSelections in + XCTAssertEqual(dispatchedSelections, ["s1"]) + selectionExpectation.fulfill() + } + + interactor.computeSelections(selectingItemForKey: "s1") + + waitForExpectations(timeout: 2, handler: .none) + + interactor.onSelectionsComputed.cancelAllSubscriptions() + + let replacementExpectation = expectation(description: "replacement expectation") + + interactor.onSelectionsComputed.subscribe(with: self) { _, dispatchedSelections in + XCTAssertEqual(dispatchedSelections, ["s3"]) + replacementExpectation.fulfill() + } + + interactor.computeSelections(selectingItemForKey: "s3") + + waitForExpectations(timeout: 2, handler: .none) + + } + + func testOnSelectionsComputedMultiple() { + + let interactor = VM(items: ["s1", "s2", "s3"], selectionMode: .multiple) + interactor.selections = ["s2"] + let deselectionExpectation = expectation(description: "deselection expectation") + + interactor.onSelectionsComputed.subscribe(with: self) { _, dispatchedSelections in + XCTAssert(dispatchedSelections.isEmpty) + deselectionExpectation.fulfill() + } + + interactor.computeSelections(selectingItemForKey: "s2") + + waitForExpectations(timeout: 2, handler: .none) + + interactor.onSelectionsComputed.cancelAllSubscriptions() + + let selectionExpectation = expectation(description: "selection expectation") + + interactor.onSelectionsComputed.subscribe(with: self) { _, dispatchedSelections in + XCTAssertEqual(dispatchedSelections, ["s1", "s2"]) + selectionExpectation.fulfill() + } + + interactor.computeSelections(selectingItemForKey: "s1") + + waitForExpectations(timeout: 2, handler: .none) + + interactor.onSelectionsComputed.cancelAllSubscriptions() + + let additionExpectation = expectation(description: "addition expectation") + + interactor.onSelectionsComputed.subscribe(with: self) { _, dispatchedSelections in + XCTAssertEqual(dispatchedSelections, ["s2", "s3"]) + additionExpectation.fulfill() + } + + interactor.computeSelections(selectingItemForKey: "s3") + + waitForExpectations(timeout: 2, handler: .none) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/SelectableSegmentInteractorConnectorsTests.swift b/Tests/InstantSearchCoreTests/Unit/SelectableSegmentInteractorConnectorsTests.swift new file mode 100644 index 00000000..f71af1df --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/SelectableSegmentInteractorConnectorsTests.swift @@ -0,0 +1,135 @@ +// +// SelectableSegmentInteractorConnectorsTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 21/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import AlgoliaSearchClient +import XCTest + +class SelectableSegmentInteractorConnectorsTests: XCTestCase { + + class TestController: SelectableSegmentController { + + typealias SegmentKey = Int + + var selected: Int? + var onClick: ((Int) -> Void)? + var items: [Int: String] = [:] + + var onSelectedChanged: (() -> Void)? + var onItemsChanged: (() -> Void)? + + func setSelected(_ selected: Int?) { + self.selected = selected + onSelectedChanged?() + } + + func setItems(items: [Int: String]) { + self.items = items + onItemsChanged?() + } + + func clickItem(withKey key: Int) { + onClick?(key) + } + + } + + func testConnectSearcher() { + + let filterState = FilterState() + let searcher = SingleIndexSearcher(appID: "", apiKey: "", indexName: "") + + let interactor = SelectableSegmentInteractor(items: [0: "t1", 1: "t2", 2: "t3"]) + interactor.connectSearcher(searcher, attribute: "tags") + interactor.connectFilterState(filterState, attribute: "tags", operator: .or) + + XCTAssertTrue((searcher.indexQueryState.query.facets ?? []).contains("tags")) + + } + + func testConnectFilterState() { + + let filterState = FilterState() + let interactor = SelectableSegmentInteractor(items: [0: "t1", 1: "t2", 2: "t3"]) + + interactor.connectFilterState(filterState, attribute: "tags", operator: .or) + // Interactor -> FilterState + + interactor.computeSelected(selecting: 1) + + XCTAssertTrue(filterState.contains(Filter.Tag(value: "t2"), inGroupWithID: .or(name: "tags", filterType: .tag))) + + // FilterState -> Interactor + + filterState.notify(.remove(filter: Filter.Tag(value: "t2"), fromGroupWithID: .or(name: "tags", filterType: .tag))) + + XCTAssertNil(interactor.selected) + + filterState.notify(.add(filter: Filter.Tag(value: "t3"), toGroupWithID: .or(name: "tags", filterType: .tag))) + + XCTAssertEqual(interactor.selected, 2) + + } + + func testConnectController() { + + let interactor = SelectableSegmentInteractor(items: [0: "t1", 1: "t2", 2: "t3"]) + let controller = TestController() + + interactor.selected = 1 + + let onChangeExp = expectation(description: "on change") + onChangeExp.expectedFulfillmentCount = 5 + + // Preselection + + controller.onItemsChanged = { + XCTAssertEqual(controller.items, [0: "t1", 1: "t2", 2: "t3"]) + onChangeExp.fulfill() + } + + controller.onSelectedChanged = { + XCTAssertEqual(controller.selected, 1) + onChangeExp.fulfill() + } + + interactor.connectController(controller) + + // Interactor -> Controller + + controller.onSelectedChanged = { + XCTAssertEqual(controller.selected, 2) + onChangeExp.fulfill() + } + + interactor.selected = 2 + + controller.onItemsChanged = { + XCTAssertEqual(controller.items, [0: "t4", 1: "t5", 2: "t6"]) + onChangeExp.fulfill() + } + + interactor.items = [0: "t4", 1: "t5", 2: "t6"] + + // Controller -> Interactor + + let selectedComputedExpectation = expectation(description: "selected computed") + + interactor.onSelectedComputed.subscribe(with: self) { _, selected in + XCTAssertEqual(selected, 0) + selectedComputedExpectation.fulfill() + } + + controller.clickItem(withKey: 0) + + waitForExpectations(timeout: 5, handler: nil) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/SelectableSegmentInteractorTests.swift b/Tests/InstantSearchCoreTests/Unit/SelectableSegmentInteractorTests.swift new file mode 100644 index 00000000..94355410 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/SelectableSegmentInteractorTests.swift @@ -0,0 +1,94 @@ +// +// SelectableSegmentInteractorTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 20/05/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore +import XCTest + +class SelectableSegmentInteractorTests: XCTestCase { + + typealias VM = SelectableSegmentInteractor + + func testConstruction() { + + let interactor = VM(items: ["k1": "i1", "k2": "i2", "k3": "i3"]) + + XCTAssertEqual(interactor.items, ["k1": "i1", "k2": "i2", "k3": "i3"]) + XCTAssertNil(interactor.selected) + + } + + func testSwitchItems() { + + let interactor = VM(items: ["k1": "i1", "k2": "i2", "k3": "i3"]) + + let switchItemsExpectation = expectation(description: "switch items") + + interactor.onItemsChanged.subscribe(with: self) { _, newItems in + XCTAssertEqual(newItems, ["k4": "i4"]) + switchItemsExpectation.fulfill() + } + + interactor.items = ["k4": "i4"] + + waitForExpectations(timeout: 2, handler: nil) + + } + + func testSelection() { + + let interactor = VM(items: ["k1": "i1", "k2": "i2", "k3": "i3"]) + + let selectionExpectation = expectation(description: "selection") + + interactor.onSelectedChanged.subscribe(with: self) { _, selectedKey in + XCTAssertEqual(selectedKey, "k3") + selectionExpectation.fulfill() + } + + interactor.selected = "k3" + + XCTAssertEqual(interactor.selected, "k3") + + waitForExpectations(timeout: 2, handler: nil) + + } + + func testSelectionComputed() { + + let interactor = VM(items: ["k1": "i1", "k2": "i2", "k3": "i3"]) + + let selectionComputedExpectation = expectation(description: "selection computed") + + interactor.onSelectedComputed.subscribe(with: self) { _, computedSelection in + XCTAssertEqual(computedSelection, "k3") + selectionComputedExpectation.fulfill() + } + + interactor.computeSelected(selecting: "k3") + + waitForExpectations(timeout: 2, handler: .none) + + } + + func nilSelectedComputedTest() { + let interactor = VM(items: ["k1": "i1", "k2": "i2", "k3": "i3"]) + + let selectionComputedExp = expectation(description: "selection computed") + + interactor.onSelectedComputed.subscribe(with: self) { _, computedSelection in + XCTAssertNil(computedSelection) + selectionComputedExp.fulfill() + } + + interactor.computeSelected(selecting: nil) + + waitForExpectations(timeout: 2, handler: .none) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/SequencerTest.swift b/Tests/InstantSearchCoreTests/Unit/SequencerTest.swift new file mode 100644 index 00000000..b0d63409 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/SequencerTest.swift @@ -0,0 +1,217 @@ +// +// Copyright (c) 2017 Algolia +// http://www.algolia.com/ +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +@testable import InstantSearchCore +import XCTest + +class AsyncOperation: Operation { + + public enum State: String { + case ready, executing, finished + + fileprivate var keyPath: String { + return "is" + rawValue.capitalized + } + } + + public var state: State = .ready { + willSet { + willChangeValue(forKey: newValue.keyPath) + willChangeValue(forKey: state.keyPath) + } + didSet { + didChangeValue(forKey: oldValue.keyPath) + didChangeValue(forKey: state.keyPath) + } + } + + override var isAsynchronous: Bool { + return true + } + + override var isReady: Bool { + return super.isReady && state == .ready + } + + override var isExecuting: Bool { + return state == .executing + } + + override var isFinished: Bool { + return state == .finished + } + + override func start() { + if isCancelled { + state = .finished + return + } + + main() + state = .executing + } + + override func cancel() { + super.cancel() + state = .finished + } + +} + +class DelayedOperation: AsyncOperation { + + let delay: Int + let completionHandler: (() -> Void)? + + init(delay: Int = 20, completionHandler: (() -> Void)? = .none) { + self.delay = delay + self.completionHandler = completionHandler + super.init() + } + + override var debugDescription: String { + return "\(name ?? "")" + } + + override func main() { + let deadline = DispatchTime.now() + .milliseconds(delay) + DispatchQueue.main.asyncAfter(deadline: deadline) { [weak self] in + guard let operation = self else { return } + defer { + operation.state = .finished + } + if operation.isCancelled { + return + } + operation.completionHandler?() + } + + } + + override func cancel() { + super.cancel() + } + +} + +class SequencerTest: XCTestCase { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testObsoleteOperationsCancellation() { + + let sequencer = Sequencer() + sequencer.maxPendingOperationsCount = 10 + + let operationsCount = 100 + let exp = expectation(description: "delayed operation") + exp.expectedFulfillmentCount = sequencer.maxPendingOperationsCount + + let operations: [Operation] = (0.. Void)? + + typealias Item = String + + func setItem(_ item: Item) { + didSetItem?(item) + } + } + + func testConnectSearcher() { + + let vm = StatsInteractor() + let results = SearchResponse(hits: [TestRecord]()) + let query = Query() + + let searcher = SingleIndexSearcher(appID: "", apiKey: "", indexName: "", query: query) + vm.connectSearcher(searcher) + + let exp = expectation(description: "on item changed") + + vm.onItemChanged.subscribe(with: self) { _, _ in + exp.fulfill() + } + + searcher.onResults.fire(results) + + waitForExpectations(timeout: 2, handler: .none) + + } + + func testConnectController() { + + let vm = StatsInteractor() + + let controller = TestStatsController() + + vm.connectController(controller, presenter: { _ in return "test string" }) + + let exp = expectation(description: "did set item") + + controller.didSetItem = { string in + XCTAssertEqual(string, "test string") + exp.fulfill() + } + + vm.item = SearchStats(totalHitsCount: 100, hitsPerPage: 10, pagesCount: 10, page: 0, processingTimeMS: 1, query: "q1") + + waitForExpectations(timeout: 2, handler: nil) + + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Tracker/FilterTrackerTests.swift b/Tests/InstantSearchCoreTests/Unit/Tracker/FilterTrackerTests.swift new file mode 100644 index 00000000..eec262bd --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Tracker/FilterTrackerTests.swift @@ -0,0 +1,148 @@ +// +// FilterTrackerTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 20/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class FilterTrackerTests: XCTestCase { + + struct Constants { + static let appID: ApplicationID = "test_app_id" + static let apiKey: APIKey = "test_api_key" + static let indexName: IndexName = "test index name" + static let eventName: EventName = "event name" + static let customEventName: EventName = "custom event name" + static let queryID: QueryID = "test query id" + + struct Filter { + static let facet = Facet(value: "test filter value", count: 10) + static let attribute: Attribute = "test attribute" + static let value = "test filter value" + static let serialized = "\"test attribute\":\"test filter value\"" + } + } + + let searcher = SingleIndexSearcher(appID: Constants.appID, apiKey: Constants.apiKey, indexName: Constants.indexName) + + let testTracker = TestFiltersTracker() + + let testFilter = Filter.Facet(attribute: Constants.Filter.attribute, stringValue: Constants.Filter.value) + + lazy var tracker: FilterTracker = { + return FilterTracker(eventName: Constants.eventName, searcher: .singleIndex(searcher), tracker: testTracker) + }() + + func testClick() { + let clickExpectation = expectation(description: #function) + clickExpectation.expectedFulfillmentCount = 2 + testTracker.did = { arg in + XCTAssertEqual(arg.0, .click) + XCTAssertEqual(arg.filters.first, Constants.Filter.serialized) + XCTAssertEqual(arg.eventName, Constants.eventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.userToken, nil) + clickExpectation.fulfill() + } + + tracker.trackClick(for: testFilter) + tracker.trackClick(for: Constants.Filter.facet, attribute: Constants.Filter.attribute) + waitForExpectations(timeout: 5, handler: .none) + } + + func testClickCustomEventName() { + let clickCustomEventExpectation = expectation(description: #function) + clickCustomEventExpectation.expectedFulfillmentCount = 2 + + testTracker.did = { arg in + XCTAssertEqual(arg.0, .click) + XCTAssertEqual(arg.filters.first, Constants.Filter.serialized) + XCTAssertEqual(arg.eventName, Constants.customEventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.userToken, nil) + clickCustomEventExpectation.fulfill() + } + + tracker.trackClick(for: testFilter, eventName: Constants.customEventName) + tracker.trackClick(for: Constants.Filter.facet, attribute: Constants.Filter.attribute, eventName: Constants.customEventName) + waitForExpectations(timeout: 5, handler: .none) + } + + func testView() { + let viewExpectation = expectation(description: #function) + viewExpectation.expectedFulfillmentCount = 2 + + testTracker.did = { arg in + XCTAssertEqual(arg.0, .view) + XCTAssertEqual(arg.filters.first, Constants.Filter.serialized) + XCTAssertEqual(arg.eventName, Constants.eventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.userToken, nil) + viewExpectation.fulfill() + } + + tracker.trackView(for: testFilter) + tracker.trackView(for: Constants.Filter.facet, attribute: Constants.Filter.attribute) + waitForExpectations(timeout: 5, handler: .none) + } + + func testViewCustomEventName() { + let viewCustomEventExpectation = expectation(description: #function) + viewCustomEventExpectation.expectedFulfillmentCount = 2 + + testTracker.did = { arg in + XCTAssertEqual(arg.0, .view) + XCTAssertEqual(arg.filters.first, Constants.Filter.serialized) + XCTAssertEqual(arg.eventName, Constants.customEventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.userToken, nil) + viewCustomEventExpectation.fulfill() + } + + tracker.trackView(for: testFilter, eventName: Constants.customEventName) + tracker.trackView(for: Constants.Filter.facet, attribute: Constants.Filter.attribute, eventName: Constants.customEventName) + waitForExpectations(timeout: 5, handler: .none) + } + + func testConvert() { + let convertExpectation = expectation(description: #function) + convertExpectation.expectedFulfillmentCount = 2 + + testTracker.did = { arg in + XCTAssertEqual(arg.0, .convert) + XCTAssertEqual(arg.filters.first, Constants.Filter.serialized) + XCTAssertEqual(arg.eventName, Constants.eventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.userToken, nil) + convertExpectation.fulfill() + } + + tracker.trackConversion(for: testFilter) + tracker.trackConversion(for: Constants.Filter.facet, attribute: Constants.Filter.attribute) + waitForExpectations(timeout: 5, handler: .none) + } + + func textConvertCustomEventName() { + let convertCustomEventExpectation = expectation(description: #function) + convertCustomEventExpectation.expectedFulfillmentCount = 2 + + testTracker.did = { arg in + XCTAssertEqual(arg.0, .convert) + XCTAssertEqual(arg.filters.first, Constants.Filter.serialized) + XCTAssertEqual(arg.eventName, Constants.customEventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.userToken, nil) + convertCustomEventExpectation.fulfill() + } + + tracker.trackConversion(for: testFilter, eventName: Constants.customEventName) + tracker.trackConversion(for: Constants.Filter.facet, attribute: Constants.Filter.attribute, eventName: Constants.customEventName) + waitForExpectations(timeout: 5, handler: .none) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Tracker/HitsTrackerTests.swift b/Tests/InstantSearchCoreTests/Unit/Tracker/HitsTrackerTests.swift new file mode 100644 index 00000000..08f22590 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Tracker/HitsTrackerTests.swift @@ -0,0 +1,135 @@ +// +// HitsTrackerTests.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 20/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +import XCTest +@testable import InstantSearchCore + +class HitsTrackerTests: XCTestCase { + + struct Constants { + static let appID: ApplicationID = "test_app_id" + static let apiKey: APIKey = "test_api_key" + static let indexName: IndexName = "test index name" + static let objectID: ObjectID = "test object id" + static let eventName: EventName = "event name" + static let customEventName: EventName = "custom event name" + static let position = 10 + static let queryID: QueryID = "test query id" + static let object: [String: JSON] = ["field": "value"] + } + + let searcher = SingleIndexSearcher(appID: Constants.appID, apiKey: Constants.apiKey, indexName: Constants.indexName) + + let testTracker = TestHitsTracker() + + lazy var tracker: HitsTracker = { + let tracker = HitsTracker(eventName: Constants.eventName, searcher: .singleIndex(searcher), tracker: testTracker) + tracker.queryID = Constants.queryID + return tracker + }() + + let hit = Hit<[String: JSON]>(object: Constants.object, objectID: Constants.objectID) + + func testClick() { + let clickExpectation = expectation(description: #function) + + testTracker.didClick = { arg in + XCTAssertEqual(arg.eventName, Constants.eventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.objectIDsWithPositions.first!.0, Constants.objectID) + XCTAssertEqual(arg.objectIDsWithPositions.first!.1, Constants.position) + XCTAssertEqual(arg.queryID, Constants.queryID) + XCTAssertEqual(arg.userToken, nil) + clickExpectation.fulfill() + } + + tracker.trackClick(for: hit, position: Constants.position) + waitForExpectations(timeout: 5, handler: .none) + } + + func testClickCustomEventName() { + let clickCustomEventExpectation = expectation(description: #function) + + testTracker.didClick = { arg in + XCTAssertEqual(arg.eventName, Constants.customEventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.objectIDsWithPositions.first!.0, Constants.objectID) + XCTAssertEqual(arg.objectIDsWithPositions.first!.1, Constants.position) + XCTAssertEqual(arg.queryID, Constants.queryID) + XCTAssertEqual(arg.userToken, nil) + clickCustomEventExpectation.fulfill() + } + + tracker.trackClick(for: hit, position: Constants.position, eventName: Constants.customEventName) + waitForExpectations(timeout: 5, handler: .none) + } + + func testView() { + let viewExpectation = expectation(description: #function) + + testTracker.didView = { arg in + XCTAssertEqual(arg.eventName, Constants.eventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.objectIDs.first, Constants.objectID) + XCTAssertEqual(arg.userToken, nil) + viewExpectation.fulfill() + } + + tracker.trackView(for: hit) + waitForExpectations(timeout: 5, handler: .none) + } + + func testViewCustomEventName() { + let viewCustomEventExpectation = expectation(description: #function) + + testTracker.didView = { arg in + XCTAssertEqual(arg.eventName, Constants.customEventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.objectIDs.first, Constants.objectID) + XCTAssertEqual(arg.userToken, nil) + viewCustomEventExpectation.fulfill() + } + + tracker.trackView(for: hit, eventName: Constants.customEventName) + waitForExpectations(timeout: 5, handler: .none) + } + + func testConvert() { + let convertExpectation = expectation(description: #function) + + testTracker.didConvert = { arg in + XCTAssertEqual(arg.eventName, Constants.eventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.objectIDs.first, Constants.objectID) + XCTAssertEqual(arg.queryID, Constants.queryID) + XCTAssertEqual(arg.userToken, nil) + convertExpectation.fulfill() + } + + tracker.trackConvert(for: hit) + waitForExpectations(timeout: 5, handler: .none) + } + + func textConvertCustomEventName() { + let convertCustomEventExpectation = expectation(description: #function) + + testTracker.didConvert = { arg in + XCTAssertEqual(arg.eventName, Constants.customEventName) + XCTAssertEqual(arg.indexName, Constants.indexName) + XCTAssertEqual(arg.objectIDs.first!, Constants.objectID) + XCTAssertEqual(arg.queryID, Constants.queryID) + XCTAssertEqual(arg.userToken, nil) + convertCustomEventExpectation.fulfill() + } + + tracker.trackConvert(for: hit, eventName: Constants.customEventName) + waitForExpectations(timeout: 5, handler: .none) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Tracker/TestFiltersTracker.swift b/Tests/InstantSearchCoreTests/Unit/Tracker/TestFiltersTracker.swift new file mode 100644 index 00000000..0c267d76 --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Tracker/TestFiltersTracker.swift @@ -0,0 +1,30 @@ +// +// TestFiltersTracker.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 20/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore + +class TestFiltersTracker: FilterTrackable { + + enum EventType { case view, click, convert } + + var did: (((EventType, eventName: EventName, indexName: IndexName, filters: [String], userToken: UserToken?)) -> Void)? + + func viewed(eventName: EventName, indexName: IndexName, filters: [String], userToken: UserToken?) { + did?((.view, eventName, indexName, filters, userToken)) + } + + func clicked(eventName: EventName, indexName: IndexName, filters: [String], userToken: UserToken?) { + did?((.click, eventName, indexName, filters, userToken)) + } + + func converted(eventName: EventName, indexName: IndexName, filters: [String], userToken: UserToken?) { + did?((.convert, eventName, indexName, filters, userToken)) + } + +} diff --git a/Tests/InstantSearchCoreTests/Unit/Tracker/TestHitsTracker.swift b/Tests/InstantSearchCoreTests/Unit/Tracker/TestHitsTracker.swift new file mode 100644 index 00000000..8ae13adb --- /dev/null +++ b/Tests/InstantSearchCoreTests/Unit/Tracker/TestHitsTracker.swift @@ -0,0 +1,30 @@ +// +// TestHitsTracker.swift +// InstantSearchCore +// +// Created by Vladislav Fitc on 20/12/2019. +// Copyright © 2019 Algolia. All rights reserved. +// + +import Foundation +@testable import InstantSearchCore + +class TestHitsTracker: HitsAfterSearchTrackable { + + var didClick: (((eventName: EventName, indexName: IndexName, objectIDsWithPositions: [(ObjectID, Int)], queryID: QueryID, userToken: UserToken?)) -> Void)? + var didConvert: (((eventName: EventName, indexName: IndexName, objectIDs: [ObjectID], queryID: QueryID, userToken: UserToken?)) -> Void)? + var didView: (((eventName: EventName, indexName: IndexName, objectIDs: [ObjectID], userToken: UserToken?)) -> Void)? + + func clickedAfterSearch(eventName: EventName, indexName: IndexName, objectIDsWithPositions: [(ObjectID, Int)], queryID: QueryID, userToken: UserToken?) { + didClick?((eventName, indexName, objectIDsWithPositions, queryID, userToken)) + } + + func convertedAfterSearch(eventName: EventName, indexName: IndexName, objectIDs: [ObjectID], queryID: QueryID, userToken: UserToken?) { + didConvert?((eventName, indexName, objectIDs, queryID, userToken)) + } + + func viewed(eventName: EventName, indexName: IndexName, objectIDs: [ObjectID], userToken: UserToken?) { + didView?((eventName, indexName, objectIDs, userToken)) + } + +} diff --git a/Tests/InstantSearchCoreTests/XCTestManifests.swift b/Tests/InstantSearchCoreTests/XCTestManifests.swift new file mode 100644 index 00000000..0b42292a --- /dev/null +++ b/Tests/InstantSearchCoreTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(InstantSearchCoreTests.allTests) + ] +} +#endif diff --git a/Tests/InstantSearchTests/CollectionViewHitsControllerTests.swift b/Tests/InstantSearchTests/CollectionViewHitsControllerTests.swift new file mode 100644 index 00000000..b1c2dc01 --- /dev/null +++ b/Tests/InstantSearchTests/CollectionViewHitsControllerTests.swift @@ -0,0 +1,137 @@ +// +// CollectionViewHitsControllerTests.swift +// InstantSearchTests +// +// Created by Vladislav Fitc on 04/09/2019. +// + + +@testable import InstantSearch +import Foundation +import XCTest +import InstantSearchCore +#if canImport(UIKit) +import UIKit + +class TestTemplateCollectionViewCell: UICollectionViewCell {} + +class CollectionViewHitsControllerTests: XCTestCase { + +// func testMissingDataSource() { +// +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) +// +// let dataSource = HitsCollectionViewDataSource { (_, hit, _) in +// let cell = TestCollectionViewCell() +// cell.content = hit +// return cell +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = dataSource.collectionView(collectionView, cellForItemAt: .init()) +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = dataSource.collectionView(collectionView, numberOfItemsInSection: .init()) +// } +// +// let delegate = HitsCollectionViewDelegate { _,_,_ in } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = delegate.collectionView(collectionView, didSelectItemAt: .init()) +// } +// +// } +// +// func testTemplate() { +// +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) +// +// let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) +// +// let dataSource = HitsCollectionViewDataSource { (_, hit, _) -> UICollectionViewCell in +// let cell = TestCollectionViewCell() +// cell.content = hit +// return cell +// } +// +// dataSource.hitsSource = hitsDataSource +// +// dataSource.templateCellProvider = { return TestTemplateCollectionViewCell() } +// +// XCTAssert(dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 4, section: 0)) is TestTemplateCollectionViewCell, "") +// +// } +// +// func testDataSource() { +// +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) +// +// let dataSource = HitsCollectionViewDataSource { (_, hit, _) -> UICollectionViewCell in +// let cell = TestCollectionViewCell() +// cell.content = hit +// return cell +// } +// +// let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) +// +// dataSource.hitsSource = hitsDataSource +// +// XCTAssertEqual(dataSource.collectionView(collectionView, numberOfItemsInSection: 0), 3) +// XCTAssertEqual((dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 1, section: 0)) as? TestCollectionViewCell)?.content, "t2") +// +// } +// +// func testDelegate() { +// +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) +// +// let itemToSelect = 2 +// +// let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) +// +// let exp = expectation(description: "Hit selection") +// +// let delegate = HitsCollectionViewDelegate { (_, hit, _) in +// XCTAssertEqual(hit, hitsDataSource.hits[itemToSelect]) +// exp.fulfill() +// } +// +// delegate.hitsSource = hitsDataSource +// +// delegate.collectionView(collectionView, didSelectItemAt: IndexPath(item: itemToSelect, section: 0)) +// +// waitForExpectations(timeout: 1, handler: .none) +// +// } + +// func testWidget() { +// +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) +// +// let vm = HitsInteractor() +// +// let dataSource = HitsCollectionViewDataSource> { (_, hit, _) -> UICollectionViewCell in +// let cell = TestCollectionViewCell() +// cell.content = hit +// return cell +// } +// +// dataSource.hitsSource = vm +// +// let delegate = HitsCollectionViewDelegate> { (_, _, _) in } +// +// delegate.hitsSource = vm +// +// let widget = HitsCollectionController>(collectionView: collectionView) +// +// widget.dataSource = dataSource +// widget.delegate = delegate + +// XCTAssertTrue(collectionView.delegate === delegate) +// XCTAssertTrue(collectionView.dataSource === dataSource) + +// } + +} +#endif diff --git a/Tests/InstantSearchTests/CollectionViewMultiIndexHitsControllerTests.swift b/Tests/InstantSearchTests/CollectionViewMultiIndexHitsControllerTests.swift new file mode 100644 index 00000000..3154f6ec --- /dev/null +++ b/Tests/InstantSearchTests/CollectionViewMultiIndexHitsControllerTests.swift @@ -0,0 +1,122 @@ +// +// CollectionViewMultiIndexHitsControllerTests.swift +// InstantSearch +// +// Created by Vladislav Fitc on 04/09/2019. +// + +import Foundation + +@testable import InstantSearch +import InstantSearchCore +import Foundation +import XCTest +#if canImport(UIKit) +import UIKit + +class TestCollectionViewCell: UICollectionViewCell { + var content: String? +} + +class CollectionViewMultiIndexHitsControllerTests: XCTestCase { + +// func testDataSource() { +// +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) +// +// let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) +// +// let dataSource = MultiIndexHitsCollectionViewDataSource() +// +// dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) in +// let cell = TestCollectionViewCell() +// cell.content = h +// return cell +// } +// +// dataSource.hitsSource = hitsSource +// +// XCTAssertEqual(dataSource.numberOfSections(in: collectionView), 2) +// XCTAssertEqual(dataSource.collectionView(collectionView, numberOfItemsInSection: 0), 2) +// +// } +// +// func testDelegate() { +// +// let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) +// +// let delegate = MultiIndexHitsTableViewDelegate() +// delegate.hitsSource = hitsSource +// +// } +// +// func testMissingHitsSource() { +// +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) +// +// let dataSource = MultiIndexHitsCollectionViewDataSource() +// +// dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) in +// let cell = TestCollectionViewCell() +// cell.content = h +// return cell +// } +// +// let delegate = MultiIndexHitsCollectionViewDelegate() +// +// delegate.setClickHandler(forSection: 0) { (_, h: String, _) in +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = dataSource.numberOfSections(in: collectionView) +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = dataSource.collectionView(collectionView, numberOfItemsInSection: 0) +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 0, section: 0)) +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// delegate.collectionView(collectionView, didSelectItemAt: IndexPath(item: 0, section: 0)) +// } +// +// } +// +// +// func testMissingCellHandler() { +// +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) +// +// let dataSource = MultiIndexHitsCollectionViewDataSource() +// +// let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) +// +// dataSource.hitsSource = hitsSource +// +// expectLog(expectedMessage: "No cell configurator found for section 0", expectedLevel: .warning) { +// _ = dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 0, section: 0)) +// } +// +// } +// +// func testMissingClickHandler() { +// +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) +// +// let delegate = MultiIndexHitsCollectionViewDelegate() +// +// let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) +// +// delegate.hitsSource = hitsSource +// +// expectLog(expectedMessage: "No click handler found for section 0", expectedLevel: .warning) { +// _ = delegate.collectionView(collectionView, didSelectItemAt: IndexPath(row: 0, section: 0)) +// } +// +// } + +} +#endif diff --git a/Tests/InstantSearchTests/InstantSearchTests.swift b/Tests/InstantSearchTests/InstantSearchTests.swift new file mode 100644 index 00000000..06c0c8d7 --- /dev/null +++ b/Tests/InstantSearchTests/InstantSearchTests.swift @@ -0,0 +1,11 @@ +import XCTest +@testable import InstantSearch + +final class InstantSearchTests: XCTestCase { + func testExample() { + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/Tests/LogTest.swift b/Tests/InstantSearchTests/LogTest.swift similarity index 84% rename from Tests/LogTest.swift rename to Tests/InstantSearchTests/LogTest.swift index ead791ea..2b413ae3 100644 --- a/Tests/LogTest.swift +++ b/Tests/InstantSearchTests/LogTest.swift @@ -6,13 +6,17 @@ // @testable import InstantSearch +import InstantSearchCore import Foundation import XCTest +import Logging extension XCTestCase { class TestLoggingService: Loggable { + var minSeverityLevel: LogLevel = .trace + var closure: (LogLevel, String) -> Void init(_ closure: @escaping (LogLevel, String) -> Void) { @@ -38,7 +42,7 @@ extension XCTestCase { testcase() waitForExpectations(timeout: 10) { _ in - Logger.loggingService = SwiftLog(label: "com.algolia.InstantSearch") + Logger.loggingService = Logging.Logger(label: "com.algolia.InstantSearch") } diff --git a/Tests/InstantSearchTests/TableViewHitsControllerTests.swift b/Tests/InstantSearchTests/TableViewHitsControllerTests.swift new file mode 100644 index 00000000..cfa950ec --- /dev/null +++ b/Tests/InstantSearchTests/TableViewHitsControllerTests.swift @@ -0,0 +1,137 @@ +// +// TableViewHitsControllerTests.swift +// InstantSearch +// +// Created by Vladislav Fitc on 27/03/2019. +// + +@testable import InstantSearch +import InstantSearchCore +import Foundation +import XCTest +#if canImport(UIKit) +import UIKit + +class TestTemplateCell: UITableViewCell {} + +class TableViewHitsControllerTests: XCTestCase { + +// func testMissingDataSource() { +// +// let tableView = UITableView() +// +// let dataSource = HitsTableViewDataSource { (_, hit, _) -> UITableViewCell in +// let cell = UITableViewCell() +// cell.textLabel?.text = hit +// return cell +// } +// +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = dataSource.tableView(tableView, cellForRowAt: .init()) +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = dataSource.tableView(tableView, numberOfRowsInSection: .init()) +// } +// +// let delegate = HitsTableViewDelegate { _,_,_ in } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = delegate.tableView(tableView, didSelectRowAt: .init()) +// } +// +// } +// +// func testTemplate() { +// +// let tableView = UITableView() +// +// let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) +// +// let dataSource = HitsTableViewDataSource { (_, hit, _) -> UITableViewCell in +// let cell = UITableViewCell() +// cell.textLabel?.text = hit +// return cell +// } +// +// dataSource.hitsSource = hitsDataSource +// +// dataSource.templateCellProvider = { return TestTemplateCell() } +// +// XCTAssert(dataSource.tableView(tableView, cellForRowAt: IndexPath(row: 4, section: 0)) is TestTemplateCell, "") +// +// } +// +// func testDataSource() { +// +// let tableView = UITableView() +// +// let dataSource = HitsTableViewDataSource { (_, hit, _) -> UITableViewCell in +// let cell = UITableViewCell() +// cell.textLabel?.text = hit +// return cell +// } +// +// let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) +// +// dataSource.hitsSource = hitsDataSource +// +// XCTAssertEqual(dataSource.tableView(tableView, numberOfRowsInSection: 0), 3) +// XCTAssertEqual(dataSource.tableView(tableView, cellForRowAt: IndexPath(row: 1, section: 0)).textLabel?.text, "t2") +// +// } +// +// func testDelegate() { +// +// let tableView = UITableView() +// +// let rowToSelect = 2 +// +// let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) +// +// let exp = expectation(description: "Hit selection") +// +// let delegate = HitsTableViewDelegate { (_, hit, _) in +// XCTAssertEqual(hit, hitsDataSource.hits[rowToSelect]) +// exp.fulfill() +// } +// +// delegate.hitsSource = hitsDataSource +// +// delegate.tableView(tableView, didSelectRowAt: IndexPath(row: rowToSelect, section: 0)) +// +// waitForExpectations(timeout: 1, handler: .none) +// +// } + +// func testWidget() { +// +// let tableView = UITableView() +// +// let vm = HitsInteractor() +// +// let dataSource = HitsTableViewDataSource> { (_, hit, _) -> UITableViewCell in +// let cell = UITableViewCell() +// cell.textLabel?.text = hit +// return cell +// } +// +// dataSource.hitsSource = vm +// +// let delegate = HitsTableViewDelegate> { (_, _, _) in } +// +// delegate.hitsSource = vm +// +// let widget = HitsTableController>(tableView: tableView) +// +// widget.dataSource = dataSource +// widget.delegate = delegate + +// XCTAssertTrue(tableView.delegate === delegate) +// XCTAssertTrue(tableView.dataSource === dataSource) + +// } + +} +#endif diff --git a/Tests/InstantSearchTests/TableViewMultiIndexHitsControllerTests.swift b/Tests/InstantSearchTests/TableViewMultiIndexHitsControllerTests.swift new file mode 100644 index 00000000..25980c0e --- /dev/null +++ b/Tests/InstantSearchTests/TableViewMultiIndexHitsControllerTests.swift @@ -0,0 +1,116 @@ +// +// TableViewMultiHitsWidgetTests.swift +// InstantSearch +// +// Created by Vladislav Fitc on 27/03/2019. +// + +@testable import InstantSearch +import InstantSearchCore +import Foundation +import XCTest +#if canImport(UIKit) +import UIKit + +class TableViewMultiIndexHitsControllerTests: XCTestCase { + +// func testDataSource() { +// +// let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) +// +// let dataSource = MultiIndexHitsTableViewDataSource() +// +// dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) -> UITableViewCell in +// let cell = UITableViewCell() +// cell.textLabel?.text = h +// return cell +// } +// +// dataSource.hitsSource = hitsSource +// +// let tableView = UITableView() +// +// XCTAssertEqual(dataSource.numberOfSections(in: tableView), 2) +// XCTAssertEqual(dataSource.tableView(tableView, numberOfRowsInSection: 0), 2) +// +// } +// +// func testDelegate() { +// +// let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) +// +// let delegate = MultiIndexHitsTableViewDelegate() +// delegate.hitsSource = hitsSource +// +// } +// +// func testMissingHitsSource() { +// +// let tableView = UITableView() +// +// let dataSource = MultiIndexHitsTableViewDataSource() +// +// dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) -> UITableViewCell in +// let cell = UITableViewCell() +// cell.textLabel?.text = h +// return cell +// } +// +// let delegate = MultiIndexHitsTableViewDelegate() +// +// delegate.setClickHandler(forSection: 0) { (_, h: String, _) in +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = dataSource.numberOfSections(in: tableView) +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = dataSource.tableView(tableView, numberOfRowsInSection: 0) +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// _ = dataSource.tableView(tableView, cellForRowAt: IndexPath(item: 0, section: 0)) +// } +// +// expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { +// delegate.tableView(tableView, didSelectRowAt: IndexPath(item: 0, section: 0)) +// } +// +// } +// +// +// func testMissingCellHandler() { +// +// let tableView = UITableView() +// +// let dataSource = MultiIndexHitsTableViewDataSource() +// +// let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) +// +// dataSource.hitsSource = hitsSource +// +// expectLog(expectedMessage: "No cell configurator found for section 0", expectedLevel: .warning) { +// _ = dataSource.tableView(tableView, cellForRowAt: IndexPath(item: 0, section: 0)) +// } +// +// } +// +// func testMissingClickHandler() { +// +// let tableView = UITableView() +// +// let delegate = MultiIndexHitsTableViewDelegate() +// +// let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) +// +// delegate.hitsSource = hitsSource +// +// expectLog(expectedMessage: "No click handler found for section 0", expectedLevel: .warning) { +// _ = delegate.tableView(tableView, didSelectRowAt: IndexPath(row: 0, section: 0)) +// } +// +// } + +} +#endif diff --git a/Tests/TestHitsSource.swift b/Tests/InstantSearchTests/TestHitsSource.swift similarity index 95% rename from Tests/TestHitsSource.swift rename to Tests/InstantSearchTests/TestHitsSource.swift index 08ca33b7..a99a1e0e 100644 --- a/Tests/TestHitsSource.swift +++ b/Tests/InstantSearchTests/TestHitsSource.swift @@ -7,6 +7,7 @@ @testable import InstantSearch import Foundation +import InstantSearchCore class TestHitsSource: HitsSource { diff --git a/Tests/TestMultiHitsDataSource.swift b/Tests/InstantSearchTests/TestMultiHitsDataSource.swift similarity index 96% rename from Tests/TestMultiHitsDataSource.swift rename to Tests/InstantSearchTests/TestMultiHitsDataSource.swift index e7815396..aff08e68 100644 --- a/Tests/TestMultiHitsDataSource.swift +++ b/Tests/InstantSearchTests/TestMultiHitsDataSource.swift @@ -6,7 +6,7 @@ // @testable import InstantSearch -import Foundation +import InstantSearchCore class TestMultiHitsDataSource: MultiIndexHitsSource { diff --git a/Tests/InstantSearchTests/XCTestManifests.swift b/Tests/InstantSearchTests/XCTestManifests.swift new file mode 100644 index 00000000..d1480a5a --- /dev/null +++ b/Tests/InstantSearchTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(InstantSearchTests.allTests), + ] +} +#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 00000000..00c7cdc7 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import InstantSearchTests + +var tests = [XCTestCaseEntry]() +tests += InstantSearchTests.allTests() +XCTMain(tests) diff --git a/Tests/TableViewHitsControllerTests.swift b/Tests/TableViewHitsControllerTests.swift deleted file mode 100644 index 44562ae4..00000000 --- a/Tests/TableViewHitsControllerTests.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// TableViewHitsControllerTests.swift -// InstantSearch -// -// Created by Vladislav Fitc on 27/03/2019. -// - -@testable import InstantSearch -import InstantSearchCore -import Foundation -import XCTest - -class TestTemplateCell: UITableViewCell {} - -class TableViewHitsControllerTests: XCTestCase { - - func testMissingDataSource() { - - let tableView = UITableView() - - let dataSource = HitsTableViewDataSource { (_, hit, _) -> UITableViewCell in - let cell = UITableViewCell() - cell.textLabel?.text = hit - return cell - } - - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = dataSource.tableView(tableView, cellForRowAt: .init()) - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = dataSource.tableView(tableView, numberOfRowsInSection: .init()) - } - - let delegate = HitsTableViewDelegate { _,_,_ in } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = delegate.tableView(tableView, didSelectRowAt: .init()) - } - - } - - func testTemplate() { - - let tableView = UITableView() - - let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) - - let dataSource = HitsTableViewDataSource { (_, hit, _) -> UITableViewCell in - let cell = UITableViewCell() - cell.textLabel?.text = hit - return cell - } - - dataSource.hitsSource = hitsDataSource - - dataSource.templateCellProvider = { return TestTemplateCell() } - - XCTAssert(dataSource.tableView(tableView, cellForRowAt: IndexPath(row: 4, section: 0)) is TestTemplateCell, "") - - } - - func testDataSource() { - - let tableView = UITableView() - - let dataSource = HitsTableViewDataSource { (_, hit, _) -> UITableViewCell in - let cell = UITableViewCell() - cell.textLabel?.text = hit - return cell - } - - let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) - - dataSource.hitsSource = hitsDataSource - - XCTAssertEqual(dataSource.tableView(tableView, numberOfRowsInSection: 0), 3) - XCTAssertEqual(dataSource.tableView(tableView, cellForRowAt: IndexPath(row: 1, section: 0)).textLabel?.text, "t2") - - } - - func testDelegate() { - - let tableView = UITableView() - - let rowToSelect = 2 - - let hitsDataSource = TestHitsSource(hits: ["t1", "t2", "t3"]) - - let exp = expectation(description: "Hit selection") - - let delegate = HitsTableViewDelegate { (_, hit, _) in - XCTAssertEqual(hit, hitsDataSource.hits[rowToSelect]) - exp.fulfill() - } - - delegate.hitsSource = hitsDataSource - - delegate.tableView(tableView, didSelectRowAt: IndexPath(row: rowToSelect, section: 0)) - - waitForExpectations(timeout: 1, handler: .none) - - } - -// func testWidget() { -// -// let tableView = UITableView() -// -// let vm = HitsInteractor() -// -// let dataSource = HitsTableViewDataSource> { (_, hit, _) -> UITableViewCell in -// let cell = UITableViewCell() -// cell.textLabel?.text = hit -// return cell -// } -// -// dataSource.hitsSource = vm -// -// let delegate = HitsTableViewDelegate> { (_, _, _) in } -// -// delegate.hitsSource = vm -// -// let widget = HitsTableController>(tableView: tableView) -// -// widget.dataSource = dataSource -// widget.delegate = delegate - -// XCTAssertTrue(tableView.delegate === delegate) -// XCTAssertTrue(tableView.dataSource === dataSource) - -// } - -} diff --git a/Tests/TableViewMultiIndexHitsControllerTests.swift b/Tests/TableViewMultiIndexHitsControllerTests.swift deleted file mode 100644 index 9ef28ee3..00000000 --- a/Tests/TableViewMultiIndexHitsControllerTests.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// TableViewMultiHitsWidgetTests.swift -// InstantSearch -// -// Created by Vladislav Fitc on 27/03/2019. -// - -@testable import InstantSearch -import InstantSearchCore -import Foundation -import XCTest - -class TableViewMultiIndexHitsControllerTests: XCTestCase { - - func testDataSource() { - - let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) - - let dataSource = MultiIndexHitsTableViewDataSource() - - dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) -> UITableViewCell in - let cell = UITableViewCell() - cell.textLabel?.text = h - return cell - } - - dataSource.hitsSource = hitsSource - - let tableView = UITableView() - - XCTAssertEqual(dataSource.numberOfSections(in: tableView), 2) - XCTAssertEqual(dataSource.tableView(tableView, numberOfRowsInSection: 0), 2) - - } - - func testDelegate() { - - let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) - - let delegate = MultiIndexHitsTableViewDelegate() - delegate.hitsSource = hitsSource - - } - - func testMissingHitsSource() { - - let tableView = UITableView() - - let dataSource = MultiIndexHitsTableViewDataSource() - - dataSource.setCellConfigurator(forSection: 0) { (_, h: String, _) -> UITableViewCell in - let cell = UITableViewCell() - cell.textLabel?.text = h - return cell - } - - let delegate = MultiIndexHitsTableViewDelegate() - - delegate.setClickHandler(forSection: 0) { (_, h: String, _) in - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = dataSource.numberOfSections(in: tableView) - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = dataSource.tableView(tableView, numberOfRowsInSection: 0) - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - _ = dataSource.tableView(tableView, cellForRowAt: IndexPath(item: 0, section: 0)) - } - - expectLog(expectedMessage: "Missing hits source", expectedLevel: .warning) { - delegate.tableView(tableView, didSelectRowAt: IndexPath(item: 0, section: 0)) - } - - } - - - func testMissingCellHandler() { - - let tableView = UITableView() - - let dataSource = MultiIndexHitsTableViewDataSource() - - let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) - - dataSource.hitsSource = hitsSource - - expectLog(expectedMessage: "No cell configurator found for section 0", expectedLevel: .warning) { - _ = dataSource.tableView(tableView, cellForRowAt: IndexPath(item: 0, section: 0)) - } - - } - - func testMissingClickHandler() { - - let tableView = UITableView() - - let delegate = MultiIndexHitsTableViewDelegate() - - let hitsSource = TestMultiHitsDataSource(hitsBySection: [["t11", "t12"], ["t21", "t22", "t23"]]) - - delegate.hitsSource = hitsSource - - expectLog(expectedMessage: "No click handler found for section 0", expectedLevel: .warning) { - _ = delegate.tableView(tableView, didSelectRowAt: IndexPath(row: 0, section: 0)) - } - - } - -} diff --git a/Tools/deploy-surge.sh b/Tools/deploy-surge.sh deleted file mode 100644 index ad5e6286..00000000 --- a/Tools/deploy-surge.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -cd ../docgen -yarn build -cd .. -jazzy --clean -cd docs -surge --domain http://instantsearch-ios-docs.surge.sh/ --project ../docs \ No newline at end of file diff --git a/carthage-prebuild b/carthage-prebuild new file mode 100644 index 00000000..f0ed66bb --- /dev/null +++ b/carthage-prebuild @@ -0,0 +1,12 @@ +packages=( + algoliasearch-client-swift + swift-log + instantsearch-ios +) +cd Carthage/Checkouts/ +for package in "${packages[@]}"; do + cd ./$package + swift package generate-xcodeproj + cd .. +done +cd ../../.. diff --git a/config/.swiftlint.yml b/config/.swiftlint.yml deleted file mode 100644 index 7fbac628..00000000 --- a/config/.swiftlint.yml +++ /dev/null @@ -1,27 +0,0 @@ -disabled_rules: # rule identifiers to exclude from running - - trailing_whitespace - - force_cast - - todo - - weak_delegate - - class_delegate_protocol - - type_body_length - - file_length - - identifier_name - - implicit_getter - - control_statement - - cyclomatic_complexity - - trailing_comma - - block_based_kvo -opt_in_rules: # some rules are only opt-in - - empty_count - # Find all the available rules by running: - # swiftlint rules -included: # paths to include during linting. `--path` is ignored if present. - - Sources - -line_length: 300 -file_length: 600 -function_body_length: 50 -type_body_length: 250 -identifier_name: - min_length: 2 \ No newline at end of file diff --git a/config/Fastfile b/config/Fastfile deleted file mode 100644 index fe17eac9..00000000 --- a/config/Fastfile +++ /dev/null @@ -1,34 +0,0 @@ -# More documentation about how to customize your build -# can be found here: -# https://docs.fastlane.tools -fastlane_version "1.109.0" - -lane :run_swift_lint do - swiftlint( - mode: :lint, # SwiftLint mode: :lint (default) or :autocorrect - config_file: "config/.swiftlint.yml", # Custom configuration file of SwiftLint (optional) - # output_file: "swiftlint.result.json", # The path of the output file (optional) - # config_file: ".swiftlint-ci.yml", # The path of the configuration file (optional) - strict: false, # Fail on warnings? (true/false) - executable: "config/swiftlint" - ) -end - -lane :check_alpha_beta_dependencies do - podfilePath = Dir.pwd + "/../Podfile" - cartfilePath = Dir.pwd + "/../Cartfile" - - - - if ( - (File.file?(podfilePath) && File.foreach(podfilePath).grep(/alpha/).any?) || - (File.file?(podfilePath) && File.foreach(podfilePath).grep(/beta/).any?) || - (File.file?(cartfilePath) && File.foreach(cartfilePath).grep(/beta/).any?) || - (File.file?(cartfilePath) && File.foreach(cartfilePath).grep(/alpha/).any?) - ) - raise "Error: There is an alpha or beta dependency in your Podfile. It is dangerous to deploy a new version of the library with unstable dependencies." - else - puts "No Alpha or Beta dependencies found in Podfile, proceeding..." - end - -end \ No newline at end of file diff --git a/config/LICENSE b/config/LICENSE deleted file mode 100644 index 64d6d384..00000000 --- a/config/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Algolia - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/config/README.md b/config/README.md deleted file mode 100644 index c66e210c..00000000 --- a/config/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# instantsearch-ios-config -Centralised config files shared to the Algolia iOS SDK (Client, Core, IS) - -**It is used as a git subtree in our iOS SDK.** - -## Usage - -### To add the config folder in a brand new project in - -It will be added as a `config` folder with a remote called `config - -``` -git remote add config git@github.com:algolia/instantsearch-ios-config.git - -git fetch config - -git subtree add --prefix=config config/master --squash - -``` - -### Contribution Daily work - -This is when you already have your config folder (subtree) added to your project. - -In order to get the latest changes from the main config repo, then you should do a pull like so: -``` -git subtree pull --prefix=config config master --squash -``` - -If you want to contribute to the main config repo, then after pulling the latest changes, you can push your changes like so (it will automatically detect the commits that have changes in the `config` folder, and will push them to main config repo. - -``` -git subtree push --prefix=config config master -``` - -## Content - -This config repo contains the following functionalities: - -### Swiftlint - -It contains the swiftlint executable so that everyone that pulls the container repos (Client, Core, IS) do not need to download swiftlint on their machine. This also applies to our CI bitrise that will be able to run swiftlint from Fastlane without any problems. It also has the `.swiftlint.yml` that specifies all rules to be followed by all the libraries. - -### Fastlane - -It contains a Fastfile that has common functionality shared accross all container repos. The container repos import this Fastfile, and can use the methods and lanes that are available there. diff --git a/config/swiftlint b/config/swiftlint deleted file mode 100755 index 8833d7ee..00000000 Binary files a/config/swiftlint and /dev/null differ diff --git a/docgen/.babelrc b/docgen/.babelrc deleted file mode 100755 index 28300315..00000000 --- a/docgen/.babelrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "presets": [ - ["env", { "targets": { "browsers": ["last 2 versions", "ie >= 9"] } }], - "latest", - "stage-2" - ], - "env": { - "development": { - "plugins": [ - ] - } - } -} diff --git a/docgen/.eslintrc.js b/docgen/.eslintrc.js deleted file mode 100755 index bee5bd0c..00000000 --- a/docgen/.eslintrc.js +++ /dev/null @@ -1,12 +0,0 @@ -var join = require('path').join; - -module.exports = { - "extends": join(__dirname, "../.eslintrc.js"), - "settings": { - "import/resolver": { - "webpack": { - "config": join(__dirname, "webpack.config.babel.js") - } - } - }, -}; diff --git a/docgen/.gitignore b/docgen/.gitignore deleted file mode 100755 index 3c3629e6..00000000 --- a/docgen/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/docgen/.netlify b/docgen/.netlify deleted file mode 100755 index 91f2299e..00000000 --- a/docgen/.netlify +++ /dev/null @@ -1 +0,0 @@ -{"site_id":"ccaa6cd8-3550-436b-bc9d-fd827f355282","path":"preview.zip"} \ No newline at end of file diff --git a/docgen/assets/getting-started-boilerplate.zip b/docgen/assets/getting-started-boilerplate.zip deleted file mode 100755 index 10687192..00000000 Binary files a/docgen/assets/getting-started-boilerplate.zip and /dev/null differ diff --git a/docgen/assets/img/InstantSearch-Android.svg b/docgen/assets/img/InstantSearch-Android.svg deleted file mode 100644 index a40419e8..00000000 --- a/docgen/assets/img/InstantSearch-Android.svg +++ /dev/null @@ -1 +0,0 @@ -InstantSearch-Android \ No newline at end of file diff --git a/docgen/assets/img/InstantSearch-Angular.svg b/docgen/assets/img/InstantSearch-Angular.svg deleted file mode 100644 index e3f14616..00000000 --- a/docgen/assets/img/InstantSearch-Angular.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - InstantSearch-Angular - Created with Sketch. - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/assets/img/InstantSearch-Generic.svg b/docgen/assets/img/InstantSearch-Generic.svg deleted file mode 100644 index 2e5c51a2..00000000 --- a/docgen/assets/img/InstantSearch-Generic.svg +++ /dev/null @@ -1 +0,0 @@ -InstantSearch-Generic \ No newline at end of file diff --git a/docgen/assets/img/InstantSearch-JavaScript.svg b/docgen/assets/img/InstantSearch-JavaScript.svg deleted file mode 100644 index aeff2ec3..00000000 --- a/docgen/assets/img/InstantSearch-JavaScript.svg +++ /dev/null @@ -1 +0,0 @@ -InstantSearch-JavaScript \ No newline at end of file diff --git a/docgen/assets/img/InstantSearch-React.svg b/docgen/assets/img/InstantSearch-React.svg deleted file mode 100644 index a274af7e..00000000 --- a/docgen/assets/img/InstantSearch-React.svg +++ /dev/null @@ -1 +0,0 @@ -InstantSearch-React \ No newline at end of file diff --git a/docgen/assets/img/InstantSearch-Vue.svg b/docgen/assets/img/InstantSearch-Vue.svg deleted file mode 100644 index 3fe7fb7b..00000000 --- a/docgen/assets/img/InstantSearch-Vue.svg +++ /dev/null @@ -1 +0,0 @@ -InstantSearch-Vue \ No newline at end of file diff --git a/docgen/assets/img/InstantSearch-iOS.svg b/docgen/assets/img/InstantSearch-iOS.svg deleted file mode 100644 index 9f9107f0..00000000 --- a/docgen/assets/img/InstantSearch-iOS.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - InstantSearch-iOS - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/assets/img/Instantsearch-iOS-medal.svg b/docgen/assets/img/Instantsearch-iOS-medal.svg deleted file mode 100644 index 33603b25..00000000 --- a/docgen/assets/img/Instantsearch-iOS-medal.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docgen/assets/img/Movies.gif b/docgen/assets/img/Movies.gif deleted file mode 100644 index 0ca700a2..00000000 Binary files a/docgen/assets/img/Movies.gif and /dev/null differ diff --git a/docgen/assets/img/algolia-community-dark.svg b/docgen/assets/img/algolia-community-dark.svg deleted file mode 100644 index c05413e7..00000000 --- a/docgen/assets/img/algolia-community-dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docgen/assets/img/algolia-logo-darkbg.svg b/docgen/assets/img/algolia-logo-darkbg.svg deleted file mode 100755 index a7d32ea2..00000000 --- a/docgen/assets/img/algolia-logo-darkbg.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docgen/assets/img/algolia-logo-whitebg.svg b/docgen/assets/img/algolia-logo-whitebg.svg deleted file mode 100644 index e745de35..00000000 --- a/docgen/assets/img/algolia-logo-whitebg.svg +++ /dev/null @@ -1,2 +0,0 @@ - \ No newline at end of file diff --git a/docgen/assets/img/android-instantsearch-logo-3.svg b/docgen/assets/img/android-instantsearch-logo-3.svg deleted file mode 100755 index 49ea9966..00000000 --- a/docgen/assets/img/android-instantsearch-logo-3.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - diff --git a/docgen/assets/img/cancel-icon.svg b/docgen/assets/img/cancel-icon.svg deleted file mode 100755 index 965d4174..00000000 --- a/docgen/assets/img/cancel-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docgen/assets/img/demo-aeki.svg b/docgen/assets/img/demo-aeki.svg deleted file mode 100644 index 2f43d88e..00000000 --- a/docgen/assets/img/demo-aeki.svg +++ /dev/null @@ -1,122 +0,0 @@ - - - - comp_mockups - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/assets/img/ecommerce.png b/docgen/assets/img/ecommerce.png deleted file mode 100644 index 3eb021d6..00000000 Binary files a/docgen/assets/img/ecommerce.png and /dev/null differ diff --git a/docgen/assets/img/example-e-commerce.svg b/docgen/assets/img/example-e-commerce.svg deleted file mode 100644 index eb9347f0..00000000 --- a/docgen/assets/img/example-e-commerce.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docgen/assets/img/favicon.png b/docgen/assets/img/favicon.png deleted file mode 100644 index 3993788b..00000000 Binary files a/docgen/assets/img/favicon.png and /dev/null differ diff --git a/docgen/assets/img/getting-started/guide-highlighting.png b/docgen/assets/img/getting-started/guide-highlighting.png deleted file mode 100644 index 1af6a647..00000000 Binary files a/docgen/assets/img/getting-started/guide-highlighting.png and /dev/null differ diff --git a/docgen/assets/img/getting-started/guide-hits.png b/docgen/assets/img/getting-started/guide-hits.png deleted file mode 100644 index e4c388ec..00000000 Binary files a/docgen/assets/img/getting-started/guide-hits.png and /dev/null differ diff --git a/docgen/assets/img/getting-started/guide-refinementlist.png b/docgen/assets/img/getting-started/guide-refinementlist.png deleted file mode 100644 index 6a0597c0..00000000 Binary files a/docgen/assets/img/getting-started/guide-refinementlist.png and /dev/null differ diff --git a/docgen/assets/img/getting-started/guide-searchbar.png b/docgen/assets/img/getting-started/guide-searchbar.png deleted file mode 100644 index 5e9d7618..00000000 Binary files a/docgen/assets/img/getting-started/guide-searchbar.png and /dev/null differ diff --git a/docgen/assets/img/getting-started/xcode-configurewidgets.png b/docgen/assets/img/getting-started/xcode-configurewidgets.png deleted file mode 100644 index 6e5fa80b..00000000 Binary files a/docgen/assets/img/getting-started/xcode-configurewidgets.png and /dev/null differ diff --git a/docgen/assets/img/getting-started/xcode-hits.png b/docgen/assets/img/getting-started/xcode-hits.png deleted file mode 100644 index 8ecfe77c..00000000 Binary files a/docgen/assets/img/getting-started/xcode-hits.png and /dev/null differ diff --git a/docgen/assets/img/getting-started/xcode-newproject.png b/docgen/assets/img/getting-started/xcode-newproject.png deleted file mode 100644 index 9f992593..00000000 Binary files a/docgen/assets/img/getting-started/xcode-newproject.png and /dev/null differ diff --git a/docgen/assets/img/getting-started/xcode-refinementlist.png b/docgen/assets/img/getting-started/xcode-refinementlist.png deleted file mode 100644 index 82e6d3c3..00000000 Binary files a/docgen/assets/img/getting-started/xcode-refinementlist.png and /dev/null differ diff --git a/docgen/assets/img/getting-started/xcode-searchbar.png b/docgen/assets/img/getting-started/xcode-searchbar.png deleted file mode 100644 index 906e32a6..00000000 Binary files a/docgen/assets/img/getting-started/xcode-searchbar.png and /dev/null differ diff --git a/docgen/assets/img/github-icon.svg b/docgen/assets/img/github-icon.svg deleted file mode 100755 index 97e24e11..00000000 --- a/docgen/assets/img/github-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docgen/assets/img/icebnb.gif b/docgen/assets/img/icebnb.gif deleted file mode 100644 index 673b0e8e..00000000 Binary files a/docgen/assets/img/icebnb.gif and /dev/null differ diff --git a/docgen/assets/img/ikea.gif b/docgen/assets/img/ikea.gif deleted file mode 100644 index 22252c6a..00000000 Binary files a/docgen/assets/img/ikea.gif and /dev/null differ diff --git a/docgen/assets/img/infinite-components.svg b/docgen/assets/img/infinite-components.svg deleted file mode 100644 index 29e6b0ff..00000000 --- a/docgen/assets/img/infinite-components.svg +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docgen/assets/img/instantsearch-ios-components.svg b/docgen/assets/img/instantsearch-ios-components.svg deleted file mode 100644 index d0bde377..00000000 --- a/docgen/assets/img/instantsearch-ios-components.svg +++ /dev/null @@ -1,151 +0,0 @@ - - - - instantsearch-ios-components - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/assets/img/logo-algolia.svg b/docgen/assets/img/logo-algolia.svg deleted file mode 100755 index a7d32ea2..00000000 --- a/docgen/assets/img/logo-algolia.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docgen/assets/img/logo-community-xs-darkbg.svg b/docgen/assets/img/logo-community-xs-darkbg.svg deleted file mode 100755 index 9ce9ec8b..00000000 --- a/docgen/assets/img/logo-community-xs-darkbg.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - Group 3 - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/assets/img/logo-community.svg b/docgen/assets/img/logo-community.svg deleted file mode 100755 index 3896a686..00000000 --- a/docgen/assets/img/logo-community.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docgen/assets/img/objc-swift-agnostic.svg b/docgen/assets/img/objc-swift-agnostic.svg deleted file mode 100644 index 8fd2716d..00000000 --- a/docgen/assets/img/objc-swift-agnostic.svg +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docgen/assets/img/open-doc-menu_icon.svg b/docgen/assets/img/open-doc-menu_icon.svg deleted file mode 100755 index 03c87e76..00000000 --- a/docgen/assets/img/open-doc-menu_icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - Combined Shape - Created with Sketch. - - - - - \ No newline at end of file diff --git a/docgen/assets/img/single-index.png b/docgen/assets/img/single-index.png deleted file mode 100644 index 77c97ed6..00000000 Binary files a/docgen/assets/img/single-index.png and /dev/null differ diff --git a/docgen/assets/img/suggestion.gif b/docgen/assets/img/suggestion.gif deleted file mode 100644 index ad347ae6..00000000 Binary files a/docgen/assets/img/suggestion.gif and /dev/null differ diff --git a/docgen/assets/img/widgets/highlighting.png b/docgen/assets/img/widgets/highlighting.png deleted file mode 100644 index 3b6f2228..00000000 Binary files a/docgen/assets/img/widgets/highlighting.png and /dev/null differ diff --git a/docgen/assets/img/widgets/hits.png b/docgen/assets/img/widgets/hits.png deleted file mode 100644 index 09c6f302..00000000 Binary files a/docgen/assets/img/widgets/hits.png and /dev/null differ diff --git a/docgen/assets/img/widgets/refinement-list.png b/docgen/assets/img/widgets/refinement-list.png deleted file mode 100644 index 0b3c0e96..00000000 Binary files a/docgen/assets/img/widgets/refinement-list.png and /dev/null differ diff --git a/docgen/assets/img/widgets/searchbox.png b/docgen/assets/img/widgets/searchbox.png deleted file mode 100644 index 9bd7f5c5..00000000 Binary files a/docgen/assets/img/widgets/searchbox.png and /dev/null differ diff --git a/docgen/assets/img/widgets/stats.png b/docgen/assets/img/widgets/stats.png deleted file mode 100644 index dc1d0e1f..00000000 Binary files a/docgen/assets/img/widgets/stats.png and /dev/null differ diff --git a/docgen/assets/js/activateClipboard.js b/docgen/assets/js/activateClipboard.js deleted file mode 100755 index 69159701..00000000 --- a/docgen/assets/js/activateClipboard.js +++ /dev/null @@ -1,40 +0,0 @@ -import Clipboard from 'clipboard'; - -export default function activateClipboard(codeSamples) { - codeSamples.forEach(codeSample => { - const cleanAfter = 800; - let timeout; - const copyToClipboard = document.createElement('button'); - - const setup = () => { - clearTimeout(timeout); - copyToClipboard.innerHTML = ''; - copyToClipboard.setAttribute('title', 'copy') - copyToClipboard.classList.remove('clipboard-done'); - copyToClipboard.classList.add('clipboard'); - }; - - const done = () => { - copyToClipboard.classList.add('clipboard-done'); - copyToClipboard.textContent = 'Copied!'; - }; - - const clipboard = new Clipboard(copyToClipboard, { - text: () => codeSample.querySelector('code').textContent, - }); - - setup(); - - const heading = document.createElement('div'); - heading.className = 'heading'; - heading.innerHTML = 'Code'; - heading.appendChild(copyToClipboard); - codeSample.parentNode.insertBefore(heading, codeSample); - - copyToClipboard.addEventListener('mouseleave', setup, true); - clipboard.on('success', () => { - done(); - timeout = setTimeout(setup, cleanAfter); - }); - }); -} diff --git a/docgen/assets/js/api.js b/docgen/assets/js/api.js deleted file mode 100755 index d8ca7303..00000000 --- a/docgen/assets/js/api.js +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable no-console */ - -console.log('api'); diff --git a/docgen/assets/js/bindRunExamples.js b/docgen/assets/js/bindRunExamples.js deleted file mode 100755 index 5c05235b..00000000 --- a/docgen/assets/js/bindRunExamples.js +++ /dev/null @@ -1,150 +0,0 @@ -import instantsearch from "../../../index.js"; -import capitalize from "lodash/capitalize"; - -window.instantsearch = instantsearch; -window.search = instantsearch({ - appId: "latency", - apiKey: "6be0576ff61c053d5f9a3225e2a90f76", - indexName: "instant_search", - urlSync: false, - searchParameters: { - hitsPerPage: 3 - } -}); - -const el = html => { - const div = document.createElement("div"); - div.innerHTML = html; - return div; -}; - -function initWidgetExample(codeSample, index) { - const state = { IS_RUNNING: false }; - - const [, widgetName] = /widgets.(\S+)\(/g.exec( - codeSample.lastChild.innerText - ); - - // container for code sample live example - const liveExampleContainer = createLiveExampleContainer( - widgetName, - "widget", - index - ); - - const runExample = function() { - if (!state.IS_RUNNING) { - state.IS_RUNNING = true; - - // append widget container before running code - codeSample.after(liveExampleContainer); - - // replace `container` option with the generated one - const codeToEval = codeSample.lastChild.innerText.replace( - /container: \S+,?/, - `container: "#live-example-${index}",` - ); - - // execute code, display widget - window.eval(codeToEval); - appendDefaultSearchWidgets(index); - } - }; - - appendRunButton(codeSample, runExample); -} - -function initConnectorExample(codeSample, index) { - const state = { IS_RUNNING: false }; - - const [, widgetName] = /the custom (\S+) widget/g.exec( - codeSample.lastChild.innerText - ); - - const liveExampleContainer = createLiveExampleContainer( - widgetName, - "connector", - index - ); - - const runExample = () => { - if (!state.IS_RUNNING) { - state.IS_RUNNING = true; - - codeSample.after(liveExampleContainer); - - const codeToEval = codeSample.lastChild.innerText.replace( - /containerNode: \S+,?/, - `containerNode: $("#live-example-${index}"),` - ); - - window.eval(codeToEval); - appendDefaultSearchWidgets(index); - } - }; - - appendRunButton(codeSample, runExample); -} - -function createLiveExampleContainer(name, type, index) { - return el(` -

-

${capitalize(name)} ${type} example

-
- -
-

SearchBox & Hits

-
-
-
-
- `); -} - -function appendDefaultSearchWidgets(index) { - // add default searchbox & hits - search.addWidget( - instantsearch.widgets.searchBox({ - container: `#search-box-container-${index}`, - placeholder: "Search for products", - autofocus: false - }) - ); - - search.addWidget( - instantsearch.widgets.hits({ - container: `#hits-container-${index}`, - templates: { - empty: "No results", - item: "Hit {{objectID}}: {{{_highlightResult.name.value}}}" - } - }) - ); - - search.start(); -} - -function appendRunButton(codeSample, handler) { - const runBtn = document.createElement("button"); - runBtn.textContent = "Run"; - runBtn.style.marginRight = "10px"; - runBtn.onclick = handler; - - codeSample.previousSibling.appendChild(runBtn); -} - -export default function bindRunExamples(codeSamples) { - codeSamples.forEach((codeSample, index) => { - const exampleContent = codeSample.lastChild.innerText; - - // initialize examples for widget - if (exampleContent.indexOf("search.addWidget") === 0) { - initWidgetExample(codeSample, index); - } - - // initialize examples for connector, check we have the matching pattern - if (/function renderFn\(\S+(, isFirstRendering)?\) {/g.test(exampleContent)) { - initConnectorExample(codeSample, index); - } - }); -} diff --git a/docgen/assets/js/dropdowns.js b/docgen/assets/js/dropdowns.js deleted file mode 100755 index 7d18b426..00000000 --- a/docgen/assets/js/dropdowns.js +++ /dev/null @@ -1,39 +0,0 @@ -function dropdowns() { - const openDropdown = document.querySelectorAll('[data-toggle-dropdown]'); - const otherDropdown = document.querySelectorAll('.simple-dropdown'); - - for (let i = 0; i < openDropdown.length; i++) { - toggleDropdown(openDropdown[i]); - } - - function toggleDropdown(element) { - const dropdown = element.getAttribute('data-toggle-dropdown'); - const theDropdown = document.getElementById(dropdown); - element.addEventListener('click', () => { - if (!theDropdown.classList.contains('opened')) { - for (let i = 0; i < otherDropdown.length; i++) { - otherDropdown[i].classList.remove('opened'); - } - - theDropdown.classList.add('opened'); - theDropdown.setAttribute('aria-expanded', 'true'); - theDropdown.setAttribute('aria-expanded', 'true'); - } else { - theDropdown.classList.remove('opened'); - theDropdown.setAttribute('aria-expanded', 'false'); - theDropdown.setAttribute('aria-expanded', 'false'); - } - }); - - // When there is a click event - // Check if the clicked element is the - // dropdown toggler, if not, close the dropdown - document.body.addEventListener('click', e => { - if (e.target !== element) { - theDropdown.classList.remove('opened'); - } - }); - } -} - -export default dropdowns; diff --git a/docgen/assets/js/fix-sidebar.js b/docgen/assets/js/fix-sidebar.js deleted file mode 100644 index 37600460..00000000 --- a/docgen/assets/js/fix-sidebar.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Fixes a sidebar in the boundaries of its parent - * @param {object} $0 options to init the fixed sidebar - * @param {HTMLElement} $0.sidebarContainer the holder of the menu - * @param {topOffset} $0.topOffset an optional top offset for sticky menu - */ -export function fixSidebar({sidebarContainer, topOffset}) { - const siderbarParent = sidebarContainer.parentElement; - const boundaries = getStartStopBoundaries(siderbarParent, sidebarContainer, topOffset); - - siderbarParent.style.position = 'relative'; - - const positionSidebar = () => { - const currentScroll = window.pageYOffset; - const {start, stop} = boundaries; - if(currentScroll > boundaries.start) { - if(currentScroll > boundaries.stop) { - sidebarContainer.style.position = 'absolute'; - sidebarContainer.style.bottom = `0`; - sidebarContainer.classList.remove('fixed'); - } else { - sidebarContainer.style.position = null; - sidebarContainer.style.bottom = null; - sidebarContainer.classList.add('fixed'); - } - } else { - sidebarContainer.classList.remove('fixed'); - } - }; - - window.addEventListener('load', positionSidebar); - document.addEventListener('DOMContentLoaded', positionSidebar); - document.addEventListener('scroll', positionSidebar); -} - -/** - * Defines the limits where to start or stop the stickiness - * @param {HTMLElement} parent the outer container of the sidebar - * @param {HTMLElement} sidebar the sidebar - * @param {number} topOffset an optional top offset for sticky menu - */ -function getStartStopBoundaries(parent, sidebar, topOffset) { - const bbox = parent.getBoundingClientRect(); - const sidebarBbox = sidebar.getBoundingClientRect(); - const bodyBbox = document.body.getBoundingClientRect(); - - const containerAbsoluteTop = bbox.top - bodyBbox.top; - const sidebarAbsoluteTop = sidebarBbox.top - bodyBbox.top; - const marginTop = sidebarAbsoluteTop - containerAbsoluteTop; - const start = containerAbsoluteTop - topOffset; - const stop = bbox.height + containerAbsoluteTop - sidebarBbox.height - marginTop - topOffset; - - return { - start, - stop, - }; -} - -export function followSidebarNavigation(sidebarLinks, contentHeaders) { - const links = [...sidebarLinks]; - const headers = [...contentHeaders]; - - const setActiveSidebarLink = header => { - links.forEach(item => { - const currentHref = item.getAttribute('href'); - const anchorToFind = `#${header.getAttribute('id')}`; - const isCurrentHeader = - currentHref.indexOf(anchorToFind) != -1; - if (isCurrentHeader) { - item.classList.add('navItem-active'); - } else { - item.classList.remove('navItem-active'); - } - }); - }; - - const findActiveSidebarLink = () => { - const highestVisibleHeaders = headers - .map(header => ({element: header, rect: header.getBoundingClientRect()})) - .filter(({rect}) => - rect.top < window.innerHeight / 3 && rect.bottom < window.innerHeight - // top element relative viewport position should be at least 1/3 viewport - // and element should be in viewport - ) - // then we take the closest to this position as reference - .sort((header1, header2) => Math.abs(header1.rect.top) < Math.abs(header2.rect.top) ? -1 : 1); - - if (headers[0] && highestVisibleHeaders.length === 0) { - setActiveSidebarLink(headers[0]); - return; - } - - if (highestVisibleHeaders[0]) { - setActiveSidebarLink(highestVisibleHeaders[0].element); - } - }; - - findActiveSidebarLink(); - window.addEventListener('load', findActiveSidebarLink); - document.addEventListener('DOMContentLoaded', findActiveSidebarLink); - document.addEventListener('scroll', findActiveSidebarLink); -} diff --git a/docgen/assets/js/main.js b/docgen/assets/js/main.js deleted file mode 100755 index 5d2e3d23..00000000 --- a/docgen/assets/js/main.js +++ /dev/null @@ -1,33 +0,0 @@ -import sidebar from './sidebar.js'; -import dropdowns from './dropdowns.js'; -import move from './mover.js'; -import activateClipboard from './activateClipboard.js'; -// import bindRunExamples from './bindRunExamples.js'; -import {fixSidebar, followSidebarNavigation} from './fix-sidebar.js'; - -var alg = require('algolia-frontend-components/javascripts.js'); - -const docSearch = { - apiKey: 'e47d19f42b12dcc310b1eb10a524379d', - indexName: 'instantsearch-ios', - inputSelector: '#searchbox', -}; - -const header = new alg.communityHeader(docSearch); - -const container = document.querySelector('.documentation-container'); -const codeSamples = document.querySelectorAll('.code-sample'); - -dropdowns(); -move(); -activateClipboard(codeSamples); - - -const sidebarContainer = document.querySelector('.sidebar'); -if(sidebarContainer) { - const headerHeight = document.querySelector('.algc-navigation').getBoundingClientRect().height; - const contentContainer = document.querySelector('.documentation-container'); - fixSidebar({sidebarContainer, topOffset: headerHeight}); - followSidebarNavigation(sidebarContainer.querySelectorAll('a'), contentContainer.querySelectorAll('h2')); -} - diff --git a/docgen/assets/js/mover.js b/docgen/assets/js/mover.js deleted file mode 100755 index 199dbae8..00000000 --- a/docgen/assets/js/mover.js +++ /dev/null @@ -1,52 +0,0 @@ -function move() { - let mover = (args) => { - let item = args.element; - let threesold = item.dataset.threesold; - let axis = item.dataset.move; - let factor = item.dataset.factor; - let xtraTransform = item.dataset.xtraTransform || null; - let start = null; - - function moveEl(timestamp, axis) { - - if (!start) start = timestamp; - var progress = timestamp - start; - - let value = window.scrollY; - - if ( value <= ((threesold * factor) / 2)){ - if (axis === '-y'){ - xtraTransform ? item.style.cssText = `transform: translateY(-${(value/factor) * (threesold/factor)}px) ${xtraTransform}` : item.style.cssText = `transform: translateY(-${(value/factor) * (threesold/factor)}px)`; - } else if (axis === '-x') { - xtraTransform ? item.style.cssText = `transform: translateX(-${(value/factor) * (threesold/factor)}px) ${xtraTransform}` : item.style.cssText = `transform: translateX(-${(value/factor) * (threesold/factor)}px)`; - } else if (axis === '+y') { - xtraTransform ? item.style.cssText = `transform: translateY(${(value/factor) * (threesold/factor)}px) ${xtraTransform}` : item.style.cssText = `transform: translateY(${(value/factor) * (threesold/factor)}px)`; - } else if (axis === '+x') { - xtraTransform ? item.style.cssText = `transform: translateX(${(value/factor) * (threesold/factor)}px) ${xtraTransform}` : item.style.cssText = `transform: translateX(${(value/factor) * (threesold/factor)}px)`; - } - } - - if (progress < 2000) { - window.requestAnimationFrame(moveEl); - } - } - - window.addEventListener('scroll', e => { - window.requestAnimationFrame(function(timestamp) { - moveEl(timestamp, axis) - }); - - }) - - } - - - let animatedElement = document.querySelectorAll('[data-move]'); - animatedElement.forEach( (e,s) => { - mover({ - element: animatedElement[s] - }); - }) -} - -export default move; diff --git a/docgen/assets/js/sidebar.js b/docgen/assets/js/sidebar.js deleted file mode 100755 index bcff00ab..00000000 --- a/docgen/assets/js/sidebar.js +++ /dev/null @@ -1,137 +0,0 @@ -export default function sidebar(options) { - const {headersContainer, sidebarContainer} = options; - - const list = document.createElement('ul'); - list.classList.add('no-mobile'); - - sidebarContainer.appendChild(list); - sidebarFollowScroll(sidebarContainer.firstChild); - activeLinks(sidebarContainer); - scrollSpy(sidebarContainer, headersContainer); -} - -function sidebarFollowScroll(sidebarContainer) { - const linksContainer = sidebarContainer.querySelector('ul'); - const {height, navHeight, footerHeight, menuHeight, sidebarTop, titleHeight} = - getPositionsKeyElements(sidebarContainer); - const positionSidebar = () => { - const currentScroll = window.pageYOffset; - if (currentScroll > sidebarTop - navHeight) { - const fold = height - footerHeight - menuHeight - navHeight; - if (currentScroll > fold) { - sidebarContainer.style.top = `${(fold - currentScroll + navHeight ) - 200}px`; - } else { - sidebarContainer.style.top = null; - } - sidebarContainer.classList.add('fixed'); - linksContainer.style.maxHeight = `calc(100vh - ${titleHeight + navHeight}px)`; - } else { - sidebarContainer.classList.remove('fixed'); - linksContainer.style.maxHeight = ''; - } - }; - - window.addEventListener('load', positionSidebar); - document.addEventListener('DOMContentLoaded', positionSidebar); - document.addEventListener('scroll', positionSidebar); -} - -function scrollSpy(sidebarContainer, headersContainer) { - const headers = [...headersContainer.querySelectorAll('h2')]; - - const setActiveSidebarLink = header => { - [...sidebarContainer.querySelectorAll('a')].forEach(item => { - const currentHref = item.getAttribute('href'); - const anchorToFind = `#${header.getAttribute('id')}`; - const isCurrentHeader = - currentHref.indexOf(anchorToFind) === - currentHref.length - anchorToFind.length; - if (isCurrentHeader) { - item.classList.add('active'); - } else { - item.classList.remove('active'); - } - }); - }; - - const findActiveSidebarLink = () => { - const highestVisibleHeaders = headers - .map(header => ({element: header, rect: header.getBoundingClientRect()})) - .filter(({rect}) => - rect.top < window.innerHeight / 3 && rect.bottom < window.innerHeight - // top element relative viewport position should be at least 1/3 viewport - // and element should be in viewport - ) - // then we take the closest to this position as reference - .sort((header1, header2) => Math.abs(header1.rect.top) < Math.abs(header2.rect.top) ? -1 : 1); - - if (headers[0] && highestVisibleHeaders.length === 0) { - setActiveSidebarLink(headers[0]); - return; - } - - if (highestVisibleHeaders[0]) { - setActiveSidebarLink(highestVisibleHeaders[0].element); - } - }; - - findActiveSidebarLink(); - window.addEventListener('load', findActiveSidebarLink); - document.addEventListener('DOMContentLoaded', findActiveSidebarLink); - document.addEventListener('scroll', findActiveSidebarLink); -} - -// The Following code is used to set active items -// On the documentation sidebar depending on the -// clicked item -function activeLinks(sidebarContainer) { - const linksContainer = sidebarContainer.querySelector('ul'); - - linksContainer.addEventListener('click', e => { - if (e.target.tagName === 'A') { - [...linksContainer.querySelectorAll('a')].forEach(item => item.classList.remove('active')); - e.target.classList.add('active'); - } - }); -} -// The Following function will make the '.sidebar-opener' -// clickable and it will open/close the sidebar on the -// documentations - -function toggleDocumentationSidebar() { - const sidebarNav = document.querySelector('nav.sidebar'); - const trigger = document.querySelector('.sidebar-opener'); - - function init() { - const bodySize = document.body.clientWidth; - if (bodySize <= 960 && sidebarNav) { - trigger.addEventListener('click', () => { - sidebarNav.classList.toggle('Showed'); - trigger.classList.toggle('Showed'); - }); - } - } - init(); -} -toggleDocumentationSidebar(); - -window.addEventListener('resize', () => { - toggleDocumentationSidebar(); -}); - -function getPositionsKeyElements($sidebar) { - const sidebarBBox = $sidebar.getBoundingClientRect(); - const title = $sidebar.querySelector('.sidebar-header'); - const bodyBBox = document.body.getBoundingClientRect(); - const sidebarTop = sidebarBBox.top - bodyBBox.top; - const footer = document.querySelector('#footer'); - const navigation = document.querySelector('.algc-navigation'); - const menu = document.querySelector('.sidebar-container'); - const height = document.querySelector('html').getBoundingClientRect().height; - const navHeight = navigation.offsetHeight; - const footerHeight = footer.offsetHeight; - const menuHeight = menu.offsetHeight; - const titleHeight = title.offsetHeight; - - return {sidebarTop, height, navHeight, footerHeight, menuHeight, titleHeight}; -} diff --git a/docgen/build.js b/docgen/build.js deleted file mode 100755 index 24a945bd..00000000 --- a/docgen/build.js +++ /dev/null @@ -1,10 +0,0 @@ -import builder from './builder.js'; -import {build as middlewares} from './middlewares'; - -builder({ - middlewares, -}, err => { - if (err) { - throw err; - } -}); diff --git a/docgen/builder.js b/docgen/builder.js deleted file mode 100755 index 68500b98..00000000 --- a/docgen/builder.js +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable no-console */ -import metalsmith from 'metalsmith'; -import config from './config.js'; - - -export default function builder({ - clean = true, - middlewares, -}, cb) { - console.time('metalsmith build'); - // default source directory is join(__dirname, 'src'); - // https://github.com/metalsmith/metalsmith#sourcepath - metalsmith(__dirname) - .metadata(config) - .clean(clean) - .destination(config.docsDist) - .use(middlewares) - .build(err => { - console.timeEnd('metalsmith build'); - cb(err); - }); -} diff --git a/docgen/config.js b/docgen/config.js deleted file mode 100755 index cc927624..00000000 --- a/docgen/config.js +++ /dev/null @@ -1,20 +0,0 @@ -var algoliaComponents = require('algolia-frontend-components'); -var fs = require('fs'); - -import {rootPath} from './path'; - -const prod = process.env.NODE_ENV === 'production'; - -var content = JSON.parse(fs.readFileSync('./src/data/communityHeader.json', 'utf8').toString()); -var headerAlgoliaLogo = fs.readFileSync('assets/img/algolia-logo-whitebg.svg', 'utf8').toString(); -var headerCommunityLogo = fs.readFileSync('assets/img/algolia-community-dark.svg', 'utf8').toString(); -var header = algoliaComponents.communityHeader(content, { - algoliaLogo: headerAlgoliaLogo, - communityLogo: headerCommunityLogo -}); - -export default { - docsDist: rootPath('docs'), - publicPath: prod ? '/instantsearch-ios/' : '/', - header: header -}; diff --git a/docgen/devServer.js b/docgen/devServer.js deleted file mode 100755 index 921c3763..00000000 --- a/docgen/devServer.js +++ /dev/null @@ -1,44 +0,0 @@ -// this file will start a browsersync server that will serve /docs -// it will automatically inject any css -// it will also use webpack and watch/build/hot reload - -import webpack from 'webpack'; -import browserSync from 'browser-sync'; -import webpackConfig from './webpack.config.start.babel'; -import webpackDevMiddleware from 'webpack-dev-middleware'; -import webpackHotMiddleware from 'webpack-hot-middleware'; -import compression from 'compression'; -import config from './config.js'; - -export default function() { - const compiler = webpack(webpackConfig); - const bs = browserSync.create(); - bs.init({ - server: config.docsDist, - open: false, - files: `${config.docsDist}/**/*`, - watchOptions: { - ignored: [ - /\.js$/, // any change to a JavaScript file must be ignored, webpack handles it - /\.css\.map$/, // no need to reload the whole page for CSS source maps - ], - awaitWriteFinish: { - stabilityThreshold: 150, // wait 150ms for the filesize to be stable (= write finished) - }, - }, - notify: { - styles: { - bottom: 0, - top: 'auto', - }, - }, - middleware: [ - compression(), - webpackDevMiddleware(compiler, { - noInfo: true, - publicPath: webpackConfig.output.publicPath, - }), - webpackHotMiddleware(compiler), - ], - }); -} diff --git a/docgen/jazzy/index.md b/docgen/jazzy/index.md deleted file mode 100644 index ef5d89b4..00000000 --- a/docgen/jazzy/index.md +++ /dev/null @@ -1,7 +0,0 @@ - - -# Reference - -This is the reference documentation for the **InstantSearch for iOS** library. - -It has been automatically generated from the sources using [Jazzy](https://github.com/realm/jazzy). You can always browse the latest official version [online](https://community.algolia.com/instantsearch-ios). diff --git a/docgen/layouts/archetypes/content-with-menu.pug b/docgen/layouts/archetypes/content-with-menu.pug deleted file mode 100644 index e25dfc3d..00000000 --- a/docgen/layouts/archetypes/content-with-menu.pug +++ /dev/null @@ -1,18 +0,0 @@ -include ../common/meta.pug -body.documentation - include ../common/header.pug - include ../common/hero-doc.pug - - section.documentation-section - .container - nav.sidebar.pos-abt - block navigation - a.sidebar-opener - .documentation-container - if(editable) - a.editThisPage(href=`http://github.com/algolia/instantsearch-ios/edit/master/${githubSource}`) Edit this page - block content - - include ../common/footer.pug - script(src=webpack.assets['js/main.js']) - diff --git a/docgen/layouts/archetypes/single-column-formatted.pug b/docgen/layouts/archetypes/single-column-formatted.pug deleted file mode 100644 index 9e8ea44c..00000000 --- a/docgen/layouts/archetypes/single-column-formatted.pug +++ /dev/null @@ -1,11 +0,0 @@ -include ../common/meta.pug -body.documentation - include ../common/header.pug - include ../common/hero-doc.pug - - section.examples-section - .container - block content - include ../common/footer.pug - script(src=webpack.assets['js/main.js']) - diff --git a/docgen/layouts/archetypes/single-column.pug b/docgen/layouts/archetypes/single-column.pug deleted file mode 100755 index 4ae4ee7c..00000000 --- a/docgen/layouts/archetypes/single-column.pug +++ /dev/null @@ -1,10 +0,0 @@ -include ../common/meta.pug -body - include ../common/header.pug - include ../common/hero.pug - - div.home.content - block content - include ../common/footer.pug - script(src=webpack.assets['js/main.js']) - diff --git a/docgen/layouts/common/footer.pug b/docgen/layouts/common/footer.pug deleted file mode 100755 index 5553b01b..00000000 --- a/docgen/layouts/common/footer.pug +++ /dev/null @@ -1,59 +0,0 @@ -section.footer-new-cta.footer-new.h300.pos-rel - .container.color-white.stellar-container.vh-center - .col-md-5 - .spacer120.hidden-sm - .spacer32.visible-xs - header - h2.text-normal.m-t-none - | Start creating stellar search, - span.cf.hidden-xs - | no strings attached. - p - | Dive into Algolia with our 14-day trial - No credit card required. Plenty of time to see how Algolia can change your business. - .col-md-7.pos-rel.z-10 - .spacer120.inline.hidden-sm - .spacer32.inline.hidden-sm - .spacer16.visible-sm - .button-holder.h200.p-r-large - .spacer16.hidden-md.hidden-sm - span.inline.pos-rel - a.btn.btn-static-primary.btn-static-shadow-dark(href='https://algolia.com/users/sign_up') - | Get Started - svg.arrow-icon(width="22") - use(xlink:href="#arrow-right") - - svg.search-icon(width="22") - use(xlink:href="#search-icon") - - -#footer - .credits - .container.pos-rel - .row - - .col-md-12.text-center - a(data-no-turbolink='true', href='/') - img(width='40', src='https://www.algolia.com/static_assets/images/flat2/algolia/algolia-logo_badge-598a1fe6.svg') - .spacer40 - - -svg(style="display: none") - symbol(width='40', height='40', viewbox='0 0 40 40', xmlns='http://www.w3.org/2000/svg', id="search-icon") - path(d='M26.806 29.012a16.312 16.312 0 0 1-10.427 3.746C7.33 32.758 0 25.425 0 16.378 0 7.334 7.333 0 16.38 0c9.045 0 16.378 7.333 16.378 16.38 0 3.96-1.406 7.593-3.746 10.426L39.547 37.34c.607.608.61 1.59-.004 2.203a1.56 1.56 0 0 1-2.202.004L26.808 29.012zm-10.427.627c7.32 0 13.26-5.94 13.26-13.26 0-7.325-5.94-13.26-13.26-13.26-7.325 0-13.26 5.935-13.26 13.26 0 7.32 5.935 13.26 13.26 13.26z', fill-rule='evenodd') - - symbol(width='46', height='38', viewbox='0 0 46 38', xmlns='http://www.w3.org/2000/svg', id="arrow-right") - path(d='M34.852 15.725l-8.624-9.908L24.385 3.7 28.62.014l1.84 2.116 13.1 15.05 1.606 1.846-1.61 1.844-13.1 15.002-1.845 2.114-4.23-3.692 1.85-2.114 9.465-10.84h-24.66v-5.615h23.817zm-26.774 0h-.002 2.96v5.614H0v-5.615h8.078z', fill-rule='evenodd') - - symbol(xmlns='http://www.w3.org/2000/svg', viewbox='0 0 708.8 717', id="icon-copy") - path(d='M658.8 158H490.2c-13.3 0-26 5.3-35.4 14.6l-4.6 4.6V25c0-13.8-11.2-25-25-25H235.6c-6.6 0-13 2.6-17.7 7.3L7.3 218C2.6 222.6 0 229 0 235.6V541c0 13.8 11.2 25 25 25h227.8v101c0 27.6 22.4 50 50 50h356c27.6 0 50-22.4 50-50V208c0-27.6-22.4-50-50-50zm-204 85.4V360H338.2l116.6-116.6zm-253-149.2V209H87L201.8 94.2zM50 516V259h176.8c13.8 0 25-11.2 25-25V50h148.4v177.3L267.5 360c-9.4 9.4-14.6 22.1-14.6 35.4V516H50zm608.8 151h-356V410h177c13.8 0 25-11.2 25-25V208h154v459z') - - - -noscript. - -script. - (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': - new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], - j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= - '//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); - })(window,document,'script','dataLayer','GTM-N8JP8G'); diff --git a/docgen/layouts/common/header.pug b/docgen/layouts/common/header.pug deleted file mode 100755 index 534ed58c..00000000 --- a/docgen/layouts/common/header.pug +++ /dev/null @@ -1 +0,0 @@ -div !{header} diff --git a/docgen/layouts/common/hero-doc.pug b/docgen/layouts/common/hero-doc.pug deleted file mode 100644 index e7e7d9f9..00000000 --- a/docgen/layouts/common/hero-doc.pug +++ /dev/null @@ -1,16 +0,0 @@ - -script(src="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js") - -section.hero.hero-doc.fill-titan - .spacer120 - .container - .col-md-12.h200.text-left - if mainTitle - h1.text-heading.color-white - | !{'' + mainTitle + ' '} - span.text-normal=title - else - h1.text-heading.text-thin.color-white=title - - - diff --git a/docgen/layouts/common/hero.pug b/docgen/layouts/common/hero.pug deleted file mode 100755 index 78a7d5f7..00000000 --- a/docgen/layouts/common/hero.pug +++ /dev/null @@ -1,21 +0,0 @@ -script(src="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js") - -section.hero.fill-titan - .container.z-10 - .spacer80 - h1.heading-text Building blocks for search on iOS - h2.color-logan.text-lg.text-thin InstantSearch iOS makes it easy to design the perfect search experience using prepackaged search widgets or creating your own! - .spacer32 - a.btn.btn-static-theme.elevation0(href="https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/"). - Get started - - - .spacer80.hidden-sm - .row.z-10.m-l-none.m-r-none - .spacer40 - .spacer40.hidden-md - figure - img(src="assets/img/Instantsearch-iOS-medal.svg") - .spacer40.hidden-sm - .spacer120 - .spacer40.hidden-sm diff --git a/docgen/layouts/common/meta.pug b/docgen/layouts/common/meta.pug deleted file mode 100755 index bfa52882..00000000 --- a/docgen/layouts/common/meta.pug +++ /dev/null @@ -1,36 +0,0 @@ -- var description = "Building blocks for search on iOS" -- var project = "InstantSearch iOS" -- var image = "http://res.cloudinary.com/hilnmyskv/image/upload/v1500283388/OG_Image_iOS.png" -- var url = "https://community.algolia.com/instantsearch-ios/" -- var project_desc = project + " - " + description - - - -head - base(href=publicPath) - meta(content='IE=edge', http-equiv='X-UA-Compatible') - meta(charset='utf-8') - meta(content='width=device-width, initial-scale=1', name='viewport') - link(rel='icon', href="assets/img/favicon.png") - meta(content='IE=edge,chrome=1', http-equiv='X-UA-Compatible') - meta(content=project_desc, name='description') - meta(content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0', name='viewport') - // / Twitter card - meta(content='summary_large_image', name='twitter:card') - meta(content=url, name='twitter:site') - meta(content='Algolia', name='twitter:creator') - meta(content=project, name='twitter:title') - meta(content=description, name='twitter:description') - meta(content=image, name='twitter:image') - // / OG meta - meta(content=url, property='og:url') - meta(content=project, property='og:title') - meta(content=image, property='og:image') - meta(content='website', property='og:type') - meta(content=description, property='og:description') - meta(content=project, property='og:site_name') - - title #{project} | #{title} - link(rel='stylesheet', href="stylesheets/style.css") - link(rel="stylesheet", href="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css") - link(rel="stylesheet", href="//cdn.jsdelivr.net/npm/instantsearch.js@2.0.0-beta.1/dist/instantsearch.min.css") diff --git a/docgen/layouts/example.pug b/docgen/layouts/example.pug deleted file mode 100755 index 31ab311c..00000000 --- a/docgen/layouts/example.pug +++ /dev/null @@ -1,12 +0,0 @@ -doctype html -head - meta(charset='UTF-8') - title=title - link(rel='stylesheet', href='https://cdn.jsdelivr.net/bootstrap/3.3.5/css/bootstrap.min.css') - link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css') - link(href='https://fonts.googleapis.com/css?family=Roboto', rel='stylesheet', type='text/css') - link(rel='stylesheet', href='instantsearch.css') - link(rel='stylesheet', type='text/css', href='main.css') - script(src='instantsearch.js') -body - | !{contents} diff --git a/docgen/layouts/examples.pug b/docgen/layouts/examples.pug deleted file mode 100755 index fd15b3de..00000000 --- a/docgen/layouts/examples.pug +++ /dev/null @@ -1,9 +0,0 @@ -extends archetypes/content-with-menu.pug -include mixins/nav.pug - -block navigation - +nav(navPath, navigation, mainTitle || title, withHeadings && headings || []) - -block content - div(id="examples") - .content!=contents \ No newline at end of file diff --git a/docgen/layouts/index.pug b/docgen/layouts/index.pug deleted file mode 100644 index 4d2c428c..00000000 --- a/docgen/layouts/index.pug +++ /dev/null @@ -1,170 +0,0 @@ -extends archetypes/single-column.pug - -block content - section.row.fill-white.p-xlarge.m-l-none.m-r-none.section-best-practices.m-t-n-large - .container.m-t-n-large - .col-md-12.z-10.text-center.pos-rel.screen-mockup.m-t-n-large - .spacer120 - .spacer120 - .spacer120.visible-sm - .row.m600.text-center.m-l-r-auto - h3.no-p-t.m-t-none.text-thin Declarative - .spacer16.visible-sm - .spacer24 - p.color-logan.text-regular.text-normal Create beautiful search experiences with customisable UI components. You simply specify parameters on your UI components and InstantSearch takes care of the rest. - - .col-md-12.z-10 - .col-md-6.hidden-sm - img.section-illustration(src="assets/img/instantsearch-ios-components.svg", alt="InstantSearch components", width="525", height="254.67") - .col-md-6.p-l-xlarge - header - h3.no-p-t.m-t-none.text-thin Design Best Practices - .spacer16.visible-sm - img.section-illustration.visible-sm(src="assets/img/instantsearch-ios-components.svg", alt="InstantSearch components", width="525") - .spacer24 - p.color-logan.text-regular.text-normal InstantSearch iOS comes with various widgets to cover a wide variety of search use cases. They have been designed to bring search UI best practices to your application. - .spacer24 - a.btn.btn-static-theme.b-n.elevation0(href="https://www.algolia.com/doc/guides/building-search-ui/widgets/showcase/ios/", title="instantSearch for iOS widgets"). - Discover widgets - - - .inline.spacer80 - .inline.spacer120 - - section.row.fill-titan.p-xlarge.m-l-none.m-r-none.pos-rel.section-infinite-possibilities - .container - .inline.spacer40 - .inline.spacer40.hidden-sm - .col-md-12.z-10 - .col-md-6.p-r-xlarge - header - h3.no-p-t.m-t-none.text-thin Infinite possibilities - .spacer16.visible-sm - img.section-illustration.visible-sm(src="assets/img/infinite-components.svg", alt="Illustration infinite-possibilities", width="525") - .spacer24 - p.color-logan.text-regular.text-normal Build the ideal search experience you’ve imagined for your users by customizing our existing widgets or building your own. - .spacer24 - a.btn.btn-static-theme.b-n.elevation0(href="https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/ios/", title="instantSearch for iOS examples"). - Learn more - - - .col-md-6.hidden-sm - img.section-illustration(src="assets/img/infinite-components.svg", alt="Illustration infinite-possibilities", width="525") - - .inline.spacer80 - .inline.spacer40 - .inline.spacer40.hidden-sm - - section.row.fill-white.p-xlarge.m-l-none.m-r-none.pos-rel.section-framework - .container - .inline.spacer40 - .inline.spacer40.hidden-sm - .col-md-12.z-10 - .col-md-6.p-r-xlarge - .spacer16.hidden-sm - img.section-illustration(src="assets/img/objc-swift-agnostic.svg", alt="Illustration framework-agnostic", width="510") - .col-md-6 - header - h3.no-p-t.m-t-none.text-thin Swift and Objective-C - .spacer24 - p.color-logan.text-regular.text-normal Whether you speak Swift or Objective-C, you can use InstantSearch iOS with the language of your choice. - .spacer24 - a.btn.btn-static-theme.b-n.elevation0(href="https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/", title="Get started with instantSearch for iOS"). - Get started - - .inline.spacer40 - .inline.spacer40.hidden-sm - .inline.spacer120 - - section.row.fill-titan.p-xlarge.live-section.m-l-none.m-r-none.pos-rel - .container - .inline.spacer40 - .inline.spacer40.hidden-sm - .col-md-12.z-10.text-center.row - header - h3.no-p-t.m-t-none.text-thin See it live - .spacer40 - .row.h300 - .col-md-6.pos-rel.col-md-offset-3 - a(href="https://github.com/algolia/instantsearch-swift-examples/tree/master/ecommerce%20Ikea") - .card.fill-white.elevation1.radius6.p-large.h300.flex-container.flex-dir-row - .spacer16 - img.flex-it-1.h80(src="assets/img/example-e-commerce.svg", alt="E-commerce icon") - h3.text-lg.text-demi.m-t-none.no-p-t.flex-it-1.color-bunting.no-decoration E-commerce - span.flex-it-1 See it live - - .inline.spacer40 - .inline.spacer40.hidden-sm - .inline.spacer80 - - section.row.fill-white.p-xlarge.discover-section.m-l-none.m-r-none.pos-rel - .container - .inline.spacer80 - .col-md-12.z-10.text-center.row - header - h3.no-p-t.m-t-none.text-thin Discover the
InstantSearch family - - .spacer80 - .row.h100.logos-container - .col-md-12 - //- .col-md-4.col-md-offset-4 - //- .card.p-large.h100.fill-white.elevation1.radius6.text-left - //- a(href="#") - //- figure - //- img(src="assets/img/InstantSearch-Generic.svg", alt="Logo InstantSearch") - //- figcaption InstantSearch - - .spacer32.inline - .col-md-12 - .col-md-6.pos-rel.p-b-large.col-md-offset-2 - div.heading.w100p.text-left - small.color-logan InstantSearch for native - .col-md-12.mobile-projects - .col-md-4.col-md-offset-2 - .card.fill-white.elevation1.radius6.p-large.h100.text-left - a(href="https://community.algolia.com/instantsearch-android/") - figure - img(src="assets/img/InstantSearch-Android.svg", alt="Logo InstantSearch Android") - figcaption InstantSearch for Android - .col-md-4 - .card.fill-white.elevation1.radius6.p-large.h100.text-left - a(href="#") - figure - img(src="assets/img/InstantSearch-iOS.svg", alt="Logo InstantSearch iOS") - figcaption InstantSearch for iOS - - .spacer40.inline - .row.desktop-projects - .col-md-10.p-b-large - div.heading.w100p.text-left.m-l-large - small.color-logan InstantSearch for web - .col-md-12 - .col-md-3.col-sm-6 - .card.fill-white.elevation1.radius6.p-large.h100.text-left - a(href="https://community.algolia.com/instantsearch.js/v2") - figure - img.logo-is.w100p(src="assets/img/InstantSearch-JavaScript.svg", alt="Logo InstantSearch Javascript") - figcaption InstantSearch for Javascript - .col-md-3.col-sm-6 - .card.fill-white.elevation1.radius6.p-large.h100.text-left - a(href="https://community.algolia.com/react-instantsearch/") - figure - img.logo-is.w100p(src="assets/img/InstantSearch-React.svg", alt="Logo InstantSearch React") - figcaption InstantSearch for React - .col-md-3.col-sm-6 - .card.fill-white.elevation1.radius6.p-large.h100.text-left - a(href="https://community.algolia.com/vue-instantsearch") - figure - img.logo-is.w100p(src="assets/img/InstantSearch-Vue.svg", alt="Logo InstantSearch Vue") - figcaption InstantSearch for Vue.js - .col-md-3.col-sm-6 - .card.fill-white.elevation1.radius6.p-large.h100.text-left - a(href="https://community.algolia.com/angular-instantsearch") - figure - img.logo-is.w100p(src="assets/img/InstantSearch-Angular.svg", alt="Logo InstantSearch Angular") - figcaption InstantSearch for Angular - - .inline.spacer80 - .inline.spacer80 - - diff --git a/docgen/layouts/main.pug b/docgen/layouts/main.pug deleted file mode 100755 index 255cd3bf..00000000 --- a/docgen/layouts/main.pug +++ /dev/null @@ -1,8 +0,0 @@ -extends archetypes/content-with-menu.pug -include mixins/nav.pug - -block navigation - +nav(navPath, navigation, mainTitle || title, withHeadings && headings || []) - -block content - .content!=contents diff --git a/docgen/layouts/mixins/documentationjs/connector-usage.pug b/docgen/layouts/mixins/documentationjs/connector-usage.pug deleted file mode 100755 index b35a4f78..00000000 --- a/docgen/layouts/mixins/documentationjs/connector-usage.pug +++ /dev/null @@ -1,68 +0,0 @@ -mixin connectorUsage(fnSymbol) - - var rendererOptionsT = fnSymbol.params[0].type.params[0].name; - - var widgetOptionT = fnSymbol.returns[0].type.params ? fnSymbol.returns[0].type.params[0].name : 'undefined'; - - var widgetName = rendererOptionsT.replace('RenderingOptions', '') - pre.CodeMirror.cm-s-mdn-like - code - span.cm-keyword const - span.cm-def search - | = - span.cm-variable instantsearch - | ( - span.cm-comment /* parameters */ - | ); - br - br - span.cm-keyword const - span.cm-variable make!{widgetName} - span.cm-operator = - span.cm-variable instantsearch - | . - span.cm-variable connectors - | . - span.cm-variable-2=fnSymbol.name - | ( - br - span.cm-keyword=' function ' - span.cm-variable renderFn - | ( - br - span.cm-variable=' renderOpts' - | : - span.cm-def - a.typed-link(href=`${navPath}#struct-${rendererOptionsT}`)=rendererOptionsT - | , - br - span.cm-variable=' isFirstRendering' - | : - span.cm-def boolean - | ) { - br - span.cm-comment=` // render the custom ${widgetName} widget` - br - | } - | ) - span.cm-operator ; - br - br - | - span.cm-keyword const - span.cm-variable custom!{widgetName} - span.cm-operator = - span.cm-variable-2 make!{widgetName} - | ( - span.cm-variable instanceOpts - | : - span.cm-def - a.typed-link(href=`${navPath}#struct-${widgetOptionT}`)=widgetOptionT - | ) - span.cm-operator ; - br - | - span.cm-variable search - | . - span.cm-property addWidget - | ( - span.cm-variable custom!{widgetName} - | ) - span.cm-operator ; diff --git a/docgen/layouts/mixins/documentationjs/description.pug b/docgen/layouts/mixins/documentationjs/description.pug deleted file mode 100755 index d57c086f..00000000 --- a/docgen/layouts/mixins/documentationjs/description.pug +++ /dev/null @@ -1,2 +0,0 @@ -mixin description(description) - | !{description} diff --git a/docgen/layouts/mixins/documentationjs/event.pug b/docgen/layouts/mixins/documentationjs/event.pug deleted file mode 100755 index f1330898..00000000 --- a/docgen/layouts/mixins/documentationjs/event.pug +++ /dev/null @@ -1,16 +0,0 @@ -include description.pug -include type.pug - -mixin event(t) - h3(id=`struct-${t.name}`)=t.name - a.anchor(href=`${navPath}#struct-${t.name}`) - p!=t.description - ul.struct-def - if t.params - each param in t.params - li.type(id=`param-${t.name}-${param.name}`) - strong=param.name - a.anchor(href=`${navPath}#param-${t.name}-${param.name}`) - code - +type(param.type) - +description(param.description) diff --git a/docgen/layouts/mixins/documentationjs/instantsearch-usage.pug b/docgen/layouts/mixins/documentationjs/instantsearch-usage.pug deleted file mode 100755 index 54fec5c4..00000000 --- a/docgen/layouts/mixins/documentationjs/instantsearch-usage.pug +++ /dev/null @@ -1,33 +0,0 @@ -mixin instantsearchUsage(fnSymbol) - - const paramTag = fnSymbol.tags.find(tag => tag.title === 'param' && tag.name === '$0'); - if paramTag && paramTag.type - - const paramTypeName = paramTag.type.name; - - const paramsType = fnSymbol.relatedTypes && fnSymbol.relatedTypes.find(t => t.name === paramTypeName); - - if(!paramsType) console.log(fnSymbol.relatedTypes.map(t => t.name)); - - const properties = '\n' + paramsType.properties.map(p => ` ${p.name}: ${p.type.name}`).join(',\n'); - pre.CodeMirror.cm-s-mdn-like - code - span.cm-keyword const - span.cm-def search - span.cm-operator = - span.cm-variable=fnSymbol.name - | ({ !{'\n'} - for property in paramsType.properties - span.cm-property=' ' - a.typed-link(href=`${navPath}#struct-${paramTypeName}-${property.name}`)=property.name - | : - span.cm-def - +type(property.type, fnSymbol.relatedTypes) - | , !{'\n'} - | }: - span.cm-def - +type(paramsType, fnSymbol.relatedTypes) - | ); - | - br - span.cm-variable search - |. - span.cm-property addWidget - | ( - span.cm-comment /* A widget instance here */ - | ); diff --git a/docgen/layouts/mixins/documentationjs/method.pug b/docgen/layouts/mixins/documentationjs/method.pug deleted file mode 100755 index dc4d60fa..00000000 --- a/docgen/layouts/mixins/documentationjs/method.pug +++ /dev/null @@ -1,22 +0,0 @@ -include description.pug -include type.pug - -mixin method(t, relatedTypes) - h3(id=`struct-${t.name}`)=`${t.name}(` - if t.params - each param, idx in t.params - a(href=`${navPath}#param-${t.name}-${param.name}`)=param.name - if idx < t.params.length - 1 - |, - |) - a.anchor(href=`${navPath}#struct-${t.name}`) - p!=t.description - ul.struct-def - if t.params - each param in t.params - li.type(id=`param-${t.name}-${param.name}`) - strong=param.name - a.anchor(href=`${navPath}#param-${t.name}-${param.name}`) - code - +type(param.type, relatedTypes) - +description(param.description) diff --git a/docgen/layouts/mixins/documentationjs/struct.pug b/docgen/layouts/mixins/documentationjs/struct.pug deleted file mode 100755 index 7586d911..00000000 --- a/docgen/layouts/mixins/documentationjs/struct.pug +++ /dev/null @@ -1,18 +0,0 @@ -include description.pug -include type.pug - -mixin struct(t, relatedTypes) - if t.properties - h3(id=`struct-${t.name}`)=t.name - a.anchor(href=`${navPath}#struct-${t.name}`) - ul.struct-def - each property in t.properties - - var tag = t.tags.find(tag => tag.name === property.name) - li.type(id=`struct-${t.name}-${property.name}`) - strong=property.name - a.anchor(href=`${navPath}#param-${t.name}-${property.name}`) - code - +type(property.type, relatedTypes) - if tag && tag.default - code=`(default: ${tag.default})` - +description(property.description) diff --git a/docgen/layouts/mixins/documentationjs/type.pug b/docgen/layouts/mixins/documentationjs/type.pug deleted file mode 100755 index 25c689be..00000000 --- a/docgen/layouts/mixins/documentationjs/type.pug +++ /dev/null @@ -1,53 +0,0 @@ -mixin type(t, relatedTypes) - if !t - else if t.type && t.type === 'FunctionType' - | ( - each p, index in t.params - +type(p, relatedTypes) - if index < t.params.length - 1 - |, - | ) => - if t.result === null - | undefined - else - +type(t.result, relatedTypes) - else if t.type && t.type === 'TypeApplication' - +type(t.expression, relatedTypes) - | < - each p, index in t.applications - +type(p, relatedTypes) - if index < t.applications.length - 1 - |, - | > - else if t.type && t.type === 'StringLiteralType' - | "!{t.value}" - else if t.type && t.type === 'OptionalType' - | [ - +type(t.expression, relatedTypes) - | ] - else if t.type && t.type === 'RecordType' - |{ - for field, idxField in t.fields - +type(field, relatedTypes) - if idxField < t.fields.length - 1 - |, - |} - else if t.type && t.type === 'FieldType' - | !{t.key}: - +type(t.value, relatedTypes) - else if t.type && t.type === 'UnionType' - each p, index in t.elements - +type(p, relatedTypes) - if index < t.elements.length - 1 - || - else if t.name - if t.name !== 'Object' && t.name !== 'Array' && t.name[0] === t.name[0].toUpperCase() - - const symbol = relatedTypes && relatedTypes.find(t2 => t2 && t.name === t2.name) || {}; - - const isExternal = symbol.kind === 'external' - if isExternal - - const firstSee = symbol.tags.find(t => t.title === 'see') || {}; - a.typed-link(href=firstSee.description, target='_blank')=t.name - else - a.typed-link(href=`${navPath}#struct-${t.name}`)=t.name - else - | !{t.name} diff --git a/docgen/layouts/mixins/documentationjs/widget-usage.pug b/docgen/layouts/mixins/documentationjs/widget-usage.pug deleted file mode 100755 index dff48cc8..00000000 --- a/docgen/layouts/mixins/documentationjs/widget-usage.pug +++ /dev/null @@ -1,45 +0,0 @@ -mixin widgetUsage(fnSymbol) - - const paramTag = fnSymbol.tags.find(tag => tag.title === 'param' && tag.name === '$0'); - if paramTag && paramTag.type - - const paramTypeName = paramTag.type.name; - - const paramsType = fnSymbol.relatedTypes && fnSymbol.relatedTypes.find(t => t.name === paramTypeName); - - if(!paramsType) console.log(fnSymbol.relatedTypes.map(t => t.name)); - pre.CodeMirror.cm-s-mdn-like - code - span.cm-keyword const - span.cm-variable search - | = - span.cm-def instantsearch - | ( - span.cm-comment /* parameters */ - | ); - br - br - span.cm-keyword const - span.cm-def widget - span.cm-operator = - span.cm-variable instantsearch - | . - span.cm-variable widgets - | . - span.cm-variable=fnSymbol.name - | ({ !{'\n'} - for property in paramsType.properties - span.cm-property=' ' - a.typed-link(href=`${navPath}#struct-${paramTypeName}-${property.name}`)=property.name - | : - span.cm-def - +type(property.type, fnSymbol.relatedTypes) - | , !{'\n'} - | }: - span.cm-def - +type(paramsType, fnSymbol.relatedTypes) - | ); - | - br - span.cm-variable search - |. - span.cm-property addWidget - | ( - span.cm-variable widget - | ); diff --git a/docgen/layouts/mixins/nav-main.pug b/docgen/layouts/mixins/nav-main.pug deleted file mode 100755 index 8d16ace1..00000000 --- a/docgen/layouts/mixins/nav-main.pug +++ /dev/null @@ -1,16 +0,0 @@ -mixin nav(currentPath, navItems, title, headings) - ul.category-content - if navItems - +navRec(currentPath, navItems, title, 2) - -mixin navRec(currentPath, navItems, title, depth) - for navItem in navItems - - var isCurrentFile = navItem.path === currentPath; - - var className = ['sidebar-element', ('level-h' + depth), (isCurrentFile ? 'navItem-active' : 'navItem')]; - - var activeClass = isCurrentFile ? 'navItem-active' : ''; - li(class=className) - a(href=navItem.path class=activeClass)=navItem.title - if navItem.metadata.source - span - | - - a(href=navItem.metadata.source) source code diff --git a/docgen/layouts/mixins/nav.pug b/docgen/layouts/mixins/nav.pug deleted file mode 100755 index 13cfe105..00000000 --- a/docgen/layouts/mixins/nav.pug +++ /dev/null @@ -1,33 +0,0 @@ -mixin nav(currentPath, navItems, title, headings) - div.sidebar-container - h2.sidebar-header=title - ul.sidebar-elements - if navItems - if navItems.length === 1 - +navContent(headings, 0, currentPath) - else - +navRec(currentPath, navItems, title, 2, headings) - -mixin navRec(currentPath, navItems, title, depth, headings) - for navItem in navItems - - if(navItem.metadata.showInNav !== false) - - var isCurrentFile = navItem.path === currentPath; - - var className = ['sidebar-element', ('level-h' + depth), (isCurrentFile ? 'navItem-active' : 'navItem'), currentPath.split('/')[0]]; - - var activeClass = isCurrentFile ? 'navItem-active' : ''; - li(class=className) - if navItem.title === 'InstantSearch' - a(href=navItem.path class=activeClass)='' - else - a(href=navItem.path class=activeClass)=navItem.title - ul - if(isCurrentFile) - +navContent(headings, depth, currentPath) - -mixin navContent(headings, currentDepth, currentPath) - if headings && headings.length > 1 - each item in headings - - var level = parseFloat(/h([0-9]+)/.exec(item.tag)[1]); - - var actualLevel = level + currentDepth - 1; - - var className = ['sidebar-element', 'level-h' + actualLevel] - li(class=className) - a(href=`${currentPath}#${item.id}`)= item.text diff --git a/docgen/maintenance.md b/docgen/maintenance.md deleted file mode 100644 index 61f7dfd5..00000000 --- a/docgen/maintenance.md +++ /dev/null @@ -1,79 +0,0 @@ -Maintenance -=========== - -*Note: this documentation is for maintainers only. If you don't plan to hack the source code, you don't need to read this.* - - - -## Objective-C bridgeability - -Because we must guarantee that every feature is usable from Objective-C, and because not every Swift construct can be mapped to an Objective-C type, we must impose ourselves some restrictions: - -- **Value types** cannot be used; or they must be provided as syntactic sugar (i.e. there must be another way to access the feature from Objective-C). - -- **Default argument values** cannot be used; or an alternative form of the method must be provided for use from Objective-C. - - - -## Deployment - -### Deployment process - -- Update the **version number** in: - - - `InstantSearch-ios.podspec` - - - `Sources/Info.plist` (or via Xcode, in the project settings) - -- Update the **change log** (`ChangeLog.md`) - -- **Dry-run the pod spec**: `pod lib lint` to check that everything's fine. - -- Check-out the `gh-pages` branch into the `doc/build` directory: - - WARNING: CocoaPods is a very susceptible beast, and Xcode (used behind the scenes for building) add its own layer - of trouble on top of it. If you get weird, inexplicable behavior, try: - - - clearing Cocapods temporary directory (usually displayed in the error messages), which appears to be reused - between two invocations of CocoaPods; - - - clearing Xcode derived data in `~/Library/Developer/Xcode/DerivedData`. - - -- **Push to GitHub**: - - ``` - git commit -m "Version X.Y.Z" - git tag X.Y.Z - git push --tags origin master - ``` - -- Make sure you have a **CocoaPods session** open: `pod trunk me`. If you have no active session, use - `pod trunk register EMAIL` to create one. (If you have never registered to CocoaPods, you need to contact one of - the pod's owners; `pod trunk info` is your friend.) - -- **Publish the pod:** - - - `pod trunk push InstantSearch-Core-Swift.podspec` - - - `pod trunk push InstantSearch-Core-Offline-Swift.podspec` - -- Edit the **release notes**: in GitHub, edit the tag and copy-paste the Change Log section for this release. - -- **Publish the documentation** to the community website. - -- Update the **external documentation** if necessary: - - - [Guides](https://www.algolia.com/doc/guides) - - [FAQ](https://www.algolia.com/doc/faq) - - -### Documentation - -Documentation is generated by [Jazzy](https://github.com/realm/jazzy). Of course you will need to install it first: `sudo gem install jazzy` should do the trick. - -Generating the documentation is as simple as running: - -``` -jazzy --clean -``` diff --git a/docgen/mdRenderer.js b/docgen/mdRenderer.js deleted file mode 100755 index 712769ad..00000000 --- a/docgen/mdRenderer.js +++ /dev/null @@ -1,22 +0,0 @@ -import MarkdownIt from 'markdown-it'; -import markdownItAnchor from 'markdown-it-anchor'; - -import highlight from './syntaxHighlighting.js'; - -const md = - new MarkdownIt('default', { - highlight: (str, lang) => highlight(str, lang), - linkify: true, - typographer: true, - html: true, - }) - .use(markdownItAnchor, { - permalink: true, - permalinkClass: 'anchor', - permalinkSymbol: '', - // generate proper https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/#install hrefs since we are - // using the base href trick to handle different base urls (dev, prod) - permalinkHref: (slug, {env: {path}}) => `${path}#${slug}`, - }); - -export default md; diff --git a/docgen/middlewares.js b/docgen/middlewares.js deleted file mode 100755 index 4cbf7b9f..00000000 --- a/docgen/middlewares.js +++ /dev/null @@ -1,112 +0,0 @@ -import headings from 'metalsmith-headings'; -import layouts from 'metalsmith-layouts'; -import msWebpack from 'ms-webpack'; -import navigation from 'metalsmith-navigation'; -import nav from './plugins/navigation.js'; -import sass from 'metalsmith-sass'; - -import assets from './plugins/assets.js'; -import helpers from './plugins/helpers.js'; -import ignore from './plugins/ignore.js'; -import markdown from './plugins/markdown.js'; -import onlyChanged from './plugins/onlyChanged.js'; -import webpackEntryMetadata from './plugins/webpackEntryMetadata.js'; -import autoprefixer from './plugins/autoprefixer.js'; - -// performance and debug info for metalsmith, when needed see usage below -// import {start as perfStart, stop as perfStop} from './plugins/perf.js'; - -import webpackStartConfig from './webpack.config.start.babel.js'; -import webpackBuildConfig from './webpack.config.build.babel'; - -import {rootPath} from './path.js'; - -const common = [ - helpers, - assets({ - source: './assets/', - destination: './assets/', - }), - ignore(fileName => { - // This is a fix for VIM swp files inside src/, - // We could also configure VIM to store swp files somewhere else - // http://stackoverflow.com/questions/1636297/how-to-change-the-folder-path-for-swp-files-in-vim - if (/\.swp$/.test(fileName)) return true; - - // if it's a build js file, keep it (`build`) - if (/-build\.js$/.test(fileName)) return false; - - // if it's an example JavaScript file, keep it - if (/examples\/(.*)?\.js$/.test(fileName)) return false; - - // if it's any other JavaScript file, ignore it, it's handled by build files above - if (/\.js$/.test(fileName)) return true; - - // ignore scss partials, only include scss entrypoints - if (/_.*\.s[ac]ss/.test(fileName)) return true; - - // otherwise, keep file - return false; - }), - markdown, - headings('h2'), - nav(), - // After markdown, so that paths point to the correct HTML file - navigation({ - core: { - sortBy: 'nav_sort', - filterProperty: 'nav_groups', - }, - widget: { - sortBy: 'nav_sort', - filterProperty: 'nav_groups', - }, - connector: { - sortBy: 'nav_sort', - filterProperty: 'nav_groups', - }, - examples: { - sortBy: 'nav_sort', - filterProperty: 'nav_groups', - }, - gettingstarted: { - sortBy: 'nav_sort', - filterProperty: 'nav_groups', - }, - }, { - navListProperty: 'navs', - }), - // perfStart(), - sass({ - sourceMap: true, - sourceMapContents: true, - outputStyle: 'nested', - }), - // since we use @import, autoprefixer is used after sass - autoprefixer, - // perfStop(), -]; - -// development mode -export const start = [ - webpackEntryMetadata(webpackStartConfig), - ...common, - onlyChanged, - layouts('pug'), -]; - -export const build = [ - msWebpack({ - ...webpackBuildConfig, - stats: { - chunks: false, - modules: false, - chunkModules: false, - reasons: false, - cached: false, - cachedAssets: false, - }, - }), - ...common, - layouts('pug'), -]; diff --git a/docgen/package-lock.json b/docgen/package-lock.json deleted file mode 100644 index 983f691f..00000000 --- a/docgen/package-lock.json +++ /dev/null @@ -1,13010 +0,0 @@ -{ - "name": "instantsearch-ios-docgen", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.49.tgz", - "integrity": "sha1-vs2AVIJzREDJ0TfkbXc0DmTX9Rs=", - "dev": true, - "requires": { - "@babel/highlight": "7.0.0-beta.49" - } - }, - "@babel/generator": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.49.tgz", - "integrity": "sha1-6c/9qROZaszseTu8JauRvBnQv3o=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49", - "jsesc": "^2.5.1", - "lodash": "^4.17.5", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=", - "dev": true - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0-beta.49.tgz", - "integrity": "sha1-fZAF1U/nrWy4dnkCUedVdUGRhuk=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.0.0-beta.49.tgz", - "integrity": "sha1-xi3VBCtUpZDV5x5gIMRrkdbGyHU=", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-call-delegate": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.0.0-beta.49.tgz", - "integrity": "sha1-S11BeCpoPV3GSXg0oyMQqNAqOvk=", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "7.0.0-beta.49", - "@babel/traverse": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-define-map": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.0.0-beta.49.tgz", - "integrity": "sha1-TqBnqnIJNyQN85XNBzwk/K2cKzs=", - "dev": true, - "requires": { - "@babel/helper-function-name": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49", - "lodash": "^4.17.5" - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.0.0-beta.49.tgz", - "integrity": "sha1-K/uV337BMHNb9lXkSiF6cNOxPpM=", - "dev": true, - "requires": { - "@babel/traverse": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-function-name": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.49.tgz", - "integrity": "sha1-olwRGbnwNSeGcBJuAiXAMEHI3jI=", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "7.0.0-beta.49", - "@babel/template": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.49.tgz", - "integrity": "sha1-z1Aj8y0q2S0Ic3STnOwJUby1FEE=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0-beta.49.tgz", - "integrity": "sha1-2XQGUck7tPp5wba6xjQFH8TQP/U=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0-beta.49.tgz", - "integrity": "sha1-L2QrAD1FFV4KnnpK0OaI2Ru8FYM=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-module-imports": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.49.tgz", - "integrity": "sha1-QdfVmJEBbEk0MqRvdGREZVKJDHU=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49", - "lodash": "^4.17.5" - } - }, - "@babel/helper-module-transforms": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.0.0-beta.49.tgz", - "integrity": "sha1-/GYL2p1kl0EuGHdqca7ZqeLl960=", - "dev": true, - "requires": { - "@babel/helper-module-imports": "7.0.0-beta.49", - "@babel/helper-simple-access": "7.0.0-beta.49", - "@babel/helper-split-export-declaration": "7.0.0-beta.49", - "@babel/template": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49", - "lodash": "^4.17.5" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0-beta.49.tgz", - "integrity": "sha1-qYtDw6bFS+9I+HsQ3EVo3sC0G/c=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0-beta.49.tgz", - "integrity": "sha1-Dp/LuDT4eLs2XSqOqQ7uIbo8zSM=", - "dev": true - }, - "@babel/helper-regex": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.0.0-beta.49.tgz", - "integrity": "sha1-/yRPGcKi8Wf/SzFlpjawj9ZBgWs=", - "dev": true, - "requires": { - "lodash": "^4.17.5" - } - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.0.0-beta.49.tgz", - "integrity": "sha1-s/2qtBJ4TX6GV7rKsoaSPvyUmLg=", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "7.0.0-beta.49", - "@babel/helper-wrap-function": "7.0.0-beta.49", - "@babel/template": "7.0.0-beta.49", - "@babel/traverse": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-replace-supers": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.0.0-beta.49.tgz", - "integrity": "sha1-50RMcYBX9qCjZFyvjnj7VG/7DZ8=", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "7.0.0-beta.49", - "@babel/helper-optimise-call-expression": "7.0.0-beta.49", - "@babel/traverse": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-simple-access": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.0.0-beta.49.tgz", - "integrity": "sha1-l6QeJ4mpv4psMFNqJYt550RMXYI=", - "dev": true, - "requires": { - "@babel/template": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49", - "lodash": "^4.17.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.49.tgz", - "integrity": "sha1-QNeO2glo0BGxxShm5XRs+yPldUg=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-wrap-function": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.0.0-beta.49.tgz", - "integrity": "sha1-OFWRRgtNk++W7jgZU5wM3Ju9R1g=", - "dev": true, - "requires": { - "@babel/helper-function-name": "7.0.0-beta.49", - "@babel/template": "7.0.0-beta.49", - "@babel/traverse": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/highlight": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.49.tgz", - "integrity": "sha1-lr3GtD4TSCASumaRsQGEktOWIsw=", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.49.tgz", - "integrity": "sha1-lE0MW6KBK7FZ7b0iZ0Ov0mUXm9w=", - "dev": true - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.0.0-beta.49.tgz", - "integrity": "sha1-h2Gl4ti1JR5w3yj00KpkqiillrE=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/helper-remap-async-to-generator": "7.0.0-beta.49", - "@babel/plugin-syntax-async-generators": "7.0.0-beta.49" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0-beta.49.tgz", - "integrity": "sha1-bQzWD3p718REo3HE6UcL/wL1d3w=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/plugin-syntax-object-rest-spread": "7.0.0-beta.49" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.0.0-beta.49.tgz", - "integrity": "sha1-H1PTZ4UQHV60tV1laGqis5+iHEs=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/plugin-syntax-optional-catch-binding": "7.0.0-beta.49" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.0.0-beta.49.tgz", - "integrity": "sha1-DvX7mr2pgM0Vhe9Mjo9oC2MmPHI=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/helper-regex": "7.0.0-beta.49", - "regexpu-core": "^4.1.4" - }, - "dependencies": { - "regexpu-core": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.1.5.tgz", - "integrity": "sha512-3xo5pFze1F8oR4F9x3aFbdtdxAxQ9WBX6gXfLgeBt7KpDI0+oDF7WVntnhsPKqobU/GAYc2pmx+y3z0JI1+z3w==", - "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^6.0.0", - "regjsgen": "^0.4.0", - "regjsparser": "^0.3.0", - "unicode-match-property-ecmascript": "^1.0.3", - "unicode-match-property-value-ecmascript": "^1.0.1" - } - }, - "regjsgen": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.4.0.tgz", - "integrity": "sha512-X51Lte1gCYUdlwhF28+2YMO0U6WeN0GLpgpA7LK7mbdDnkQYiwvEpmpe0F/cv5L14EbxgrdayAG3JETBv0dbXA==", - "dev": true - }, - "regjsparser": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.3.0.tgz", - "integrity": "sha512-zza72oZBBHzt64G7DxdqrOo/30bhHkwMUoT0WqfGu98XLd7N+1tsy5MJ96Bk4MD0y74n629RhmrGW6XlnLLwCA==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - } - } - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.0.0-beta.49.tgz", - "integrity": "sha1-UO6UMAKu3JqzqNEikr013Z7bHfg=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.0.0-beta.49.tgz", - "integrity": "sha1-R4SziAgj/xLnQsJrQemFf3AdY54=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.0.0-beta.49.tgz", - "integrity": "sha1-Ph3T1drrQnDk7khjZB1Pqga7zRE=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0-beta.49.tgz", - "integrity": "sha1-3ThFtjxoPRh9UYbuDogsQEbE8OM=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.0.0-beta.49.tgz", - "integrity": "sha1-kRpA65MEAYbOtpMQXKdt73/pfQM=", - "dev": true, - "requires": { - "@babel/helper-module-imports": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/helper-remap-async-to-generator": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.0.0-beta.49.tgz", - "integrity": "sha1-eqn0b9+HO3IRqqLrDTfEw3Ghq9I=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.0.0-beta.49.tgz", - "integrity": "sha1-3Vqd3ZhndciyDPW2EGWvs92eqsk=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "lodash": "^4.17.5" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.0.0-beta.49.tgz", - "integrity": "sha1-U0JHHS5qMzczLqJGtGwL3fX8VE0=", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "7.0.0-beta.49", - "@babel/helper-define-map": "7.0.0-beta.49", - "@babel/helper-function-name": "7.0.0-beta.49", - "@babel/helper-optimise-call-expression": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/helper-replace-supers": "7.0.0-beta.49", - "@babel/helper-split-export-declaration": "7.0.0-beta.49", - "globals": "^11.1.0" - }, - "dependencies": { - "globals": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.5.0.tgz", - "integrity": "sha512-hYyf+kI8dm3nORsiiXUQigOU62hDLfJ9G01uyGMxhc6BKsircrUhC4uJPQPUSuq2GrTmiiEt7ewxlMdBewfmKQ==", - "dev": true - } - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.0.0-beta.49.tgz", - "integrity": "sha1-uCWdF0vwerS1ZWZWK0buZSDD39I=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.0.0-beta.49.tgz", - "integrity": "sha1-Q2Y5LJyC0SMQVsHQApQ4pg02K4I=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.0.0-beta.49.tgz", - "integrity": "sha1-Na4rwYe+51LQ93hdJwTlK4c3c2k=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/helper-regex": "7.0.0-beta.49", - "regexpu-core": "^4.1.3" - }, - "dependencies": { - "regexpu-core": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.1.5.tgz", - "integrity": "sha512-3xo5pFze1F8oR4F9x3aFbdtdxAxQ9WBX6gXfLgeBt7KpDI0+oDF7WVntnhsPKqobU/GAYc2pmx+y3z0JI1+z3w==", - "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^6.0.0", - "regjsgen": "^0.4.0", - "regjsparser": "^0.3.0", - "unicode-match-property-ecmascript": "^1.0.3", - "unicode-match-property-value-ecmascript": "^1.0.1" - } - }, - "regjsgen": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.4.0.tgz", - "integrity": "sha512-X51Lte1gCYUdlwhF28+2YMO0U6WeN0GLpgpA7LK7mbdDnkQYiwvEpmpe0F/cv5L14EbxgrdayAG3JETBv0dbXA==", - "dev": true - }, - "regjsparser": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.3.0.tgz", - "integrity": "sha512-zza72oZBBHzt64G7DxdqrOo/30bhHkwMUoT0WqfGu98XLd7N+1tsy5MJ96Bk4MD0y74n629RhmrGW6XlnLLwCA==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - } - } - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.0.0-beta.49.tgz", - "integrity": "sha1-+sJEgJ3ey/CV43VVjMtxbaEEIxY=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.0.0-beta.49.tgz", - "integrity": "sha1-RXstCQBHlGhKpuGwQBUIC4CgihQ=", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0-beta.49.tgz", - "integrity": "sha1-PscnJr8diaDU1RG+epVJBm9Xqt4=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.0.0-beta.49.tgz", - "integrity": "sha1-rzn2Dnrvzpsl60rc7dBNUIZs4hg=", - "dev": true, - "requires": { - "@babel/helper-function-name": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.0.0-beta.49.tgz", - "integrity": "sha1-B8g4JU1l5oZ+hlE+sPItXyawpWo=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.0.0-beta.49.tgz", - "integrity": "sha1-FtB0gJVLBBXqcPHsPtvQWXvT3f4=", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.0.0-beta.49.tgz", - "integrity": "sha1-Cfs0XVknwro72J582xOlUGftOaA=", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/helper-simple-access": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.0.0-beta.49.tgz", - "integrity": "sha1-aCJaOuExJ3G8Wjb3H/ENAsEkPZ8=", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.0.0-beta.49.tgz", - "integrity": "sha1-cEjKWncYlwb0s+luS5luswWQ3WM=", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0-beta.49.tgz", - "integrity": "sha1-wv/vHruvckqeWN3hFOV+Pmhkpec=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.0.0-beta.49.tgz", - "integrity": "sha1-swL1VwKEc0PBD/T7hDXMNXR1X+M=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/helper-replace-supers": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.0.0-beta.49.tgz", - "integrity": "sha1-HK1xoqMygeXvuxpGI6lkwHPOmi0=", - "dev": true, - "requires": { - "@babel/helper-call-delegate": "7.0.0-beta.49", - "@babel/helper-get-function-arity": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0-beta.49.tgz", - "integrity": "sha1-1O15ZwM/T1tJNjwgNQOJm4NXyuI=", - "dev": true, - "requires": { - "regenerator-transform": "^0.12.3" - }, - "dependencies": { - "regenerator-transform": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.12.4.tgz", - "integrity": "sha512-p2I0fY+TbSLD2/VFTFb/ypEHxs3e3AjU0DzttdPqk2bSmDhfSh5E54b86Yc6XhUa5KykK1tgbvZ4Nr82oCJWkQ==", - "dev": true, - "requires": { - "private": "^0.1.6" - } - } - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0-beta.49.tgz", - "integrity": "sha1-SfE0295PZVg0whUk6eYaWNTheQA=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.0.0-beta.49.tgz", - "integrity": "sha1-arqwX8DMqCmq+eKoUES3l2Pmgco=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.0.0-beta.49.tgz", - "integrity": "sha1-CMxbZM9qWUKoe92bSkgY1MuhLfM=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/helper-regex": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0-beta.49.tgz", - "integrity": "sha1-5gmu1rj8x+HrzKzyITimRyApQKI=", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.0.0-beta.49.tgz", - "integrity": "sha1-NlFBujVb9znu/Wwrud8cO3FG5FA=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.0.0-beta.49.tgz", - "integrity": "sha1-w3XbVwl1diFSPUGstiqavw1DdLg=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/helper-regex": "7.0.0-beta.49", - "regexpu-core": "^4.1.3" - }, - "dependencies": { - "regexpu-core": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.1.5.tgz", - "integrity": "sha512-3xo5pFze1F8oR4F9x3aFbdtdxAxQ9WBX6gXfLgeBt7KpDI0+oDF7WVntnhsPKqobU/GAYc2pmx+y3z0JI1+z3w==", - "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^6.0.0", - "regjsgen": "^0.4.0", - "regjsparser": "^0.3.0", - "unicode-match-property-ecmascript": "^1.0.3", - "unicode-match-property-value-ecmascript": "^1.0.1" - } - }, - "regjsgen": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.4.0.tgz", - "integrity": "sha512-X51Lte1gCYUdlwhF28+2YMO0U6WeN0GLpgpA7LK7mbdDnkQYiwvEpmpe0F/cv5L14EbxgrdayAG3JETBv0dbXA==", - "dev": true - }, - "regjsparser": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.3.0.tgz", - "integrity": "sha512-zza72oZBBHzt64G7DxdqrOo/30bhHkwMUoT0WqfGu98XLd7N+1tsy5MJ96Bk4MD0y74n629RhmrGW6XlnLLwCA==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - } - } - } - }, - "@babel/preset-env": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.0.0-beta.49.tgz", - "integrity": "sha1-SoqLkhOfUfovkPv28frXWXUyrrw=", - "dev": true, - "requires": { - "@babel/helper-module-imports": "7.0.0-beta.49", - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/plugin-proposal-async-generator-functions": "7.0.0-beta.49", - "@babel/plugin-proposal-object-rest-spread": "7.0.0-beta.49", - "@babel/plugin-proposal-optional-catch-binding": "7.0.0-beta.49", - "@babel/plugin-proposal-unicode-property-regex": "7.0.0-beta.49", - "@babel/plugin-syntax-async-generators": "7.0.0-beta.49", - "@babel/plugin-syntax-object-rest-spread": "7.0.0-beta.49", - "@babel/plugin-syntax-optional-catch-binding": "7.0.0-beta.49", - "@babel/plugin-transform-arrow-functions": "7.0.0-beta.49", - "@babel/plugin-transform-async-to-generator": "7.0.0-beta.49", - "@babel/plugin-transform-block-scoped-functions": "7.0.0-beta.49", - "@babel/plugin-transform-block-scoping": "7.0.0-beta.49", - "@babel/plugin-transform-classes": "7.0.0-beta.49", - "@babel/plugin-transform-computed-properties": "7.0.0-beta.49", - "@babel/plugin-transform-destructuring": "7.0.0-beta.49", - "@babel/plugin-transform-dotall-regex": "7.0.0-beta.49", - "@babel/plugin-transform-duplicate-keys": "7.0.0-beta.49", - "@babel/plugin-transform-exponentiation-operator": "7.0.0-beta.49", - "@babel/plugin-transform-for-of": "7.0.0-beta.49", - "@babel/plugin-transform-function-name": "7.0.0-beta.49", - "@babel/plugin-transform-literals": "7.0.0-beta.49", - "@babel/plugin-transform-modules-amd": "7.0.0-beta.49", - "@babel/plugin-transform-modules-commonjs": "7.0.0-beta.49", - "@babel/plugin-transform-modules-systemjs": "7.0.0-beta.49", - "@babel/plugin-transform-modules-umd": "7.0.0-beta.49", - "@babel/plugin-transform-new-target": "7.0.0-beta.49", - "@babel/plugin-transform-object-super": "7.0.0-beta.49", - "@babel/plugin-transform-parameters": "7.0.0-beta.49", - "@babel/plugin-transform-regenerator": "7.0.0-beta.49", - "@babel/plugin-transform-shorthand-properties": "7.0.0-beta.49", - "@babel/plugin-transform-spread": "7.0.0-beta.49", - "@babel/plugin-transform-sticky-regex": "7.0.0-beta.49", - "@babel/plugin-transform-template-literals": "7.0.0-beta.49", - "@babel/plugin-transform-typeof-symbol": "7.0.0-beta.49", - "@babel/plugin-transform-unicode-regex": "7.0.0-beta.49", - "browserslist": "^3.0.0", - "invariant": "^2.2.2", - "semver": "^5.3.0" - } - }, - "@babel/template": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.49.tgz", - "integrity": "sha1-44q+ghfLl5P0YaUwbXrXRdg+HSc=", - "dev": true, - "requires": { - "@babel/code-frame": "7.0.0-beta.49", - "@babel/parser": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49", - "lodash": "^4.17.5" - } - }, - "@babel/traverse": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.49.tgz", - "integrity": "sha1-TypzaCoYM07WYl0QCo0nMZ98LWg=", - "dev": true, - "requires": { - "@babel/code-frame": "7.0.0-beta.49", - "@babel/generator": "7.0.0-beta.49", - "@babel/helper-function-name": "7.0.0-beta.49", - "@babel/helper-split-export-declaration": "7.0.0-beta.49", - "@babel/parser": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49", - "debug": "^3.1.0", - "globals": "^11.1.0", - "invariant": "^2.2.0", - "lodash": "^4.17.5" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "globals": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.5.0.tgz", - "integrity": "sha512-hYyf+kI8dm3nORsiiXUQigOU62hDLfJ9G01uyGMxhc6BKsircrUhC4uJPQPUSuq2GrTmiiEt7ewxlMdBewfmKQ==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.49.tgz", - "integrity": "sha1-t+Oxw/TUz+Eb34yJ8e/V4WF7h6Y=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.5", - "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - } - } - }, - "@types/babel-types": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.2.tgz", - "integrity": "sha512-ylggu8DwwxT6mk3jVoJeohWAePWMNWEYm06MSoJ19kwp3hT9eY2Z4NNZn3oevzgFmClgNQ2GQF500hPDvNsGHg==" - }, - "@types/babylon": { - "version": "6.16.2", - "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.2.tgz", - "integrity": "sha512-+Jty46mPaWe1VAyZbfvgJM4BAdklLWxrT5tc/RjvCgLrtk6gzRY6AOnoWFv4p6hVxhJshDdr2hGVn56alBp97Q==", - "requires": { - "@types/babel-types": "*" - } - }, - "@types/node": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.1.4.tgz", - "integrity": "sha512-GpQxofkdlHYxjHad98UUdNoMO7JrmzQZoAaghtNg14Gwg7YkohcrCoJEcEMSgllx4VIZ+mYw7ZHjfaeIagP/rg==", - "optional": true - }, - "CSSselect": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/CSSselect/-/CSSselect-0.4.1.tgz", - "integrity": "sha1-+Kt+H4QYzmPNput713ioXX7EkrI=", - "requires": { - "CSSwhat": "0.4", - "domutils": "1.4" - } - }, - "CSSwhat": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/CSSwhat/-/CSSwhat-0.4.7.tgz", - "integrity": "sha1-hn2g/zn3eGEyQsRM/qg/CqTr35s=" - }, - "JSONStream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", - "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "absolute": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/absolute/-/absolute-0.0.1.tgz", - "integrity": "sha1-wigi+H4ck59XmIdQTZwQnEFzgp0=" - }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" - } - }, - "acorn": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", - "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==" - }, - "acorn-dynamic-import": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", - "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", - "requires": { - "acorn": "^4.0.3" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - } - } - }, - "acorn-globals": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", - "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", - "requires": { - "acorn": "^4.0.4" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - } - } - }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" - }, - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - }, - "dependencies": { - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - } - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=" - }, - "algolia-frontend-components": { - "version": "0.0.24", - "resolved": "https://registry.npmjs.org/algolia-frontend-components/-/algolia-frontend-components-0.0.24.tgz", - "integrity": "sha1-Vlomivax8L9JHwA5Z39HtYuFhzU=", - "requires": { - "babel-cli": "^6.23.0", - "babel-core": "^6.23.1", - "babel-polyfill": "^6.23.0", - "babel-preset-env": "^1.1.9", - "babel-preset-es2015": "^6.22.0", - "chalk": "^1.1.3", - "deepmerge": "^1.3.2", - "glob": "^7.1.1", - "node-sass": "^4.5.0", - "path": "^0.12.7", - "progress": "^1.1.8", - "webpack": "^2.2.1", - "webpack-dev-server": "^2.4.1" - }, - "dependencies": { - "babel-cli": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz", - "integrity": "sha1-UCq1SHTX24itALiHoGODzgPQAvE=", - "requires": { - "babel-core": "^6.26.0", - "babel-polyfill": "^6.26.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "chokidar": "^1.6.1", - "commander": "^2.11.0", - "convert-source-map": "^1.5.0", - "fs-readdir-recursive": "^1.0.0", - "glob": "^7.1.2", - "lodash": "^4.17.4", - "output-file-sync": "^1.1.2", - "path-is-absolute": "^1.0.1", - "slash": "^1.0.0", - "source-map": "^0.5.6", - "v8flags": "^2.1.1" - } - }, - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" - }, - "json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==" - }, - "node-sass": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz", - "integrity": "sha512-QFHfrZl6lqRU3csypwviz2XLgGNOoWQbo2GOvtsfQqOfL4cy1BtWnhx/XUeAO9LT3ahBzSRXcEO6DdvAH9DzSg==", - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^3.0.0", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "in-publish": "^2.0.0", - "lodash.assign": "^4.2.0", - "lodash.clonedeep": "^4.3.2", - "lodash.mergewith": "^4.6.0", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "nan": "^2.10.0", - "node-gyp": "^3.3.1", - "npmlog": "^4.0.0", - "request": "~2.79.0", - "sass-graph": "^2.2.4", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" - }, - "dependencies": { - "nan": { - "version": "2.10.0", - "resolved": "git+https://github.com/nodejs/nan.git#77d0fcaba3305d05176a9ad95d8e5101e8f2a283" - } - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "requires": { - "has-flag": "^1.0.0" - } - }, - "webpack": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-2.7.0.tgz", - "integrity": "sha512-MjAA0ZqO1ba7ZQJRnoCdbM56mmFpipOPUv/vQpwwfSI42p5PVDdoiuK2AL2FwFUVgT859Jr43bFZXRg/LNsqvg==", - "requires": { - "acorn": "^5.0.0", - "acorn-dynamic-import": "^2.0.0", - "ajv": "^4.7.0", - "ajv-keywords": "^1.1.1", - "async": "^2.1.2", - "enhanced-resolve": "^3.3.0", - "interpret": "^1.0.0", - "json-loader": "^0.5.4", - "json5": "^0.5.1", - "loader-runner": "^2.3.0", - "loader-utils": "^0.2.16", - "memory-fs": "~0.4.1", - "mkdirp": "~0.5.0", - "node-libs-browser": "^2.0.0", - "source-map": "^0.5.3", - "supports-color": "^3.1.0", - "tapable": "~0.2.5", - "uglify-js": "^2.8.27", - "watchpack": "^1.3.1", - "webpack-sources": "^1.0.1", - "yargs": "^6.0.0" - } - }, - "yargs": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", - "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^4.2.0" - } - }, - "yargs-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", - "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", - "requires": { - "camelcase": "^3.0.0" - } - } - } - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" - }, - "ansi-html": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", - "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=" - }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" - }, - "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "requires": { - "micromatch": "^2.1.5", - "normalize-path": "^2.0.0" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "requires": { - "arr-flatten": "^1.0.1" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" - }, - "array-flatten": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", - "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=" - }, - "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" - } - }, - "array-iterate": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-1.1.2.tgz", - "integrity": "sha512-1hWSHTIlG/8wtYD+PPX5AOBtKWngpDFjrsrHgZpe+JdgNGz0udYu6ZIkAa/xuenIUEqFv7DvE2Yr60jxweJSrQ==" - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" - }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", - "requires": { - "util": "0.10.3" - } - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "^4.17.10" - } - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" - }, - "async-each-series": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", - "integrity": "sha1-dhfBkXQB/Yykooqtzj266Yr+tDI=" - }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" - }, - "autoprefixer": { - "version": "6.7.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", - "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", - "requires": { - "browserslist": "^1.7.6", - "caniuse-db": "^1.0.30000634", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^5.2.16", - "postcss-value-parser": "^3.2.3" - }, - "dependencies": { - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", - "requires": { - "caniuse-db": "^1.0.30000639", - "electron-to-chromium": "^1.2.7" - } - } - } - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" - }, - "axios": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.17.1.tgz", - "integrity": "sha1-LY4+XQvb1zJ/kbyBT1xXZg+Bgk0=", - "requires": { - "follow-redirects": "^1.2.5", - "is-buffer": "^1.1.5" - } - }, - "babel-cli": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz", - "integrity": "sha1-UCq1SHTX24itALiHoGODzgPQAvE=", - "requires": { - "babel-core": "^6.26.0", - "babel-polyfill": "^6.26.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "chokidar": "^1.6.1", - "commander": "^2.11.0", - "convert-source-map": "^1.5.0", - "fs-readdir-recursive": "^1.0.0", - "glob": "^7.1.2", - "lodash": "^4.17.4", - "output-file-sync": "^1.1.2", - "path-is-absolute": "^1.0.1", - "slash": "^1.0.0", - "source-map": "^0.5.6", - "v8flags": "^2.1.1" - } - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - } - }, - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "requires": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" - } - } - }, - "babel-helper-bindify-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", - "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", - "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", - "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", - "requires": { - "babel-helper-explode-assignable-expression": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-builder-react-jsx": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", - "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", - "requires": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "esutils": "^2.0.2" - } - }, - "babel-helper-call-delegate": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", - "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-define-map": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", - "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", - "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", - "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-explode-class": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", - "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", - "requires": { - "babel-helper-bindify-decorators": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", - "requires": { - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-get-function-arity": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-hoist-variables": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-optimise-call-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-regex": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", - "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", - "requires": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-helper-remap-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-replace-supers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", - "requires": { - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-loader": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-6.4.1.tgz", - "integrity": "sha1-CzQRLVsHSKjc2/Uaz2+b1C1QuMo=", - "requires": { - "find-cache-dir": "^0.1.1", - "loader-utils": "^0.2.16", - "mkdirp": "^0.5.1", - "object-assign": "^4.0.1" - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-check-es2015-constants": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", - "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=" - }, - "babel-plugin-syntax-async-generators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", - "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=" - }, - "babel-plugin-syntax-class-constructor-call": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", - "integrity": "sha1-nLnTn+Q8hgC+yBRkVt3L1OGnZBY=" - }, - "babel-plugin-syntax-class-properties": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", - "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=" - }, - "babel-plugin-syntax-decorators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", - "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=" - }, - "babel-plugin-syntax-do-expressions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", - "integrity": "sha1-V0d1YTmqJtOQ0JQQsDdEugfkeW0=" - }, - "babel-plugin-syntax-dynamic-import": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", - "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=" - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", - "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=" - }, - "babel-plugin-syntax-export-extensions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", - "integrity": "sha1-cKFITw+QiaToStRLrDU8lbmxJyE=" - }, - "babel-plugin-syntax-flow": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", - "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=" - }, - "babel-plugin-syntax-function-bind": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", - "integrity": "sha1-SMSV8Xe98xqYHnMvVa3AvdJgH0Y=" - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=" - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", - "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=" - }, - "babel-plugin-system-import-transformer": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-system-import-transformer/-/babel-plugin-system-import-transformer-3.1.0.tgz", - "integrity": "sha1-038Mro5h7zkGAggzHZMbXmMNfF8=", - "requires": { - "babel-plugin-syntax-dynamic-import": "^6.18.0" - } - }, - "babel-plugin-transform-async-generator-functions": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", - "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", - "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-generators": "^6.5.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", - "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", - "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-functions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-class-constructor-call": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", - "integrity": "sha1-gNwoVQWsBn3LjWxl4vbxGrd2Xvk=", - "requires": { - "babel-plugin-syntax-class-constructor-call": "^6.18.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-class-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", - "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-plugin-syntax-class-properties": "^6.8.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", - "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", - "requires": { - "babel-helper-explode-class": "^6.24.1", - "babel-plugin-syntax-decorators": "^6.13.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-decorators-legacy": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators-legacy/-/babel-plugin-transform-decorators-legacy-1.3.4.tgz", - "integrity": "sha1-dBtY9sW86eYCfgiC2cmU8E82aSU=", - "requires": { - "babel-plugin-syntax-decorators": "^6.1.18", - "babel-runtime": "^6.2.0", - "babel-template": "^6.3.0" - } - }, - "babel-plugin-transform-do-expressions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", - "integrity": "sha1-KMyvkoEtlJws0SgfaQyP3EaK6bs=", - "requires": { - "babel-plugin-syntax-do-expressions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", - "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", - "requires": { - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-plugin-transform-es2015-classes": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", - "requires": { - "babel-helper-define-map": "^6.24.1", - "babel-helper-function-name": "^6.24.1", - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-helper-replace-supers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-duplicate-keys": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", - "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-modules-amd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", - "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", - "requires": { - "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", - "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", - "requires": { - "babel-plugin-transform-strict-mode": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-types": "^6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-systemjs": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", - "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", - "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-modules-umd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", - "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", - "requires": { - "babel-plugin-transform-es2015-modules-amd": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", - "requires": { - "babel-helper-replace-supers": "^6.24.1", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", - "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", - "requires": { - "babel-helper-call-delegate": "^6.24.1", - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", - "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", - "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-typeof-symbol": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", - "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", - "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", - "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "regexpu-core": "^2.0.0" - } - }, - "babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", - "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", - "requires": { - "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", - "babel-plugin-syntax-exponentiation-operator": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-export-extensions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", - "integrity": "sha1-U3OLR+deghhYnuqUbLvTkQm75lM=", - "requires": { - "babel-plugin-syntax-export-extensions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-flow-strip-types": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", - "requires": { - "babel-plugin-syntax-flow": "^6.18.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-function-bind": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", - "integrity": "sha1-xvuOlqwpajELjPjqQBRiQH3fapc=", - "requires": { - "babel-plugin-syntax-function-bind": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-object-rest-spread": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", - "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", - "requires": { - "babel-plugin-syntax-object-rest-spread": "^6.8.0", - "babel-runtime": "^6.26.0" - } - }, - "babel-plugin-transform-react-display-name": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", - "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", - "requires": { - "babel-helper-builder-react-jsx": "^6.24.1", - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx-self": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", - "integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=", - "requires": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx-source": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", - "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", - "requires": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-regenerator": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", - "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", - "requires": { - "regenerator-transform": "^0.10.0" - } - }, - "babel-plugin-transform-strict-mode": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" - } - } - }, - "babel-preset-env": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz", - "integrity": "sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==", - "requires": { - "babel-plugin-check-es2015-constants": "^6.22.0", - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-to-generator": "^6.22.0", - "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoping": "^6.23.0", - "babel-plugin-transform-es2015-classes": "^6.23.0", - "babel-plugin-transform-es2015-computed-properties": "^6.22.0", - "babel-plugin-transform-es2015-destructuring": "^6.23.0", - "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0", - "babel-plugin-transform-es2015-for-of": "^6.23.0", - "babel-plugin-transform-es2015-function-name": "^6.22.0", - "babel-plugin-transform-es2015-literals": "^6.22.0", - "babel-plugin-transform-es2015-modules-amd": "^6.22.0", - "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", - "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0", - "babel-plugin-transform-es2015-modules-umd": "^6.23.0", - "babel-plugin-transform-es2015-object-super": "^6.22.0", - "babel-plugin-transform-es2015-parameters": "^6.23.0", - "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0", - "babel-plugin-transform-es2015-spread": "^6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "^6.22.0", - "babel-plugin-transform-es2015-template-literals": "^6.22.0", - "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0", - "babel-plugin-transform-es2015-unicode-regex": "^6.22.0", - "babel-plugin-transform-exponentiation-operator": "^6.22.0", - "babel-plugin-transform-regenerator": "^6.22.0", - "browserslist": "^3.2.6", - "invariant": "^2.2.2", - "semver": "^5.3.0" - } - }, - "babel-preset-es2015": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", - "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", - "requires": { - "babel-plugin-check-es2015-constants": "^6.22.0", - "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoping": "^6.24.1", - "babel-plugin-transform-es2015-classes": "^6.24.1", - "babel-plugin-transform-es2015-computed-properties": "^6.24.1", - "babel-plugin-transform-es2015-destructuring": "^6.22.0", - "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", - "babel-plugin-transform-es2015-for-of": "^6.22.0", - "babel-plugin-transform-es2015-function-name": "^6.24.1", - "babel-plugin-transform-es2015-literals": "^6.22.0", - "babel-plugin-transform-es2015-modules-amd": "^6.24.1", - "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", - "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", - "babel-plugin-transform-es2015-modules-umd": "^6.24.1", - "babel-plugin-transform-es2015-object-super": "^6.24.1", - "babel-plugin-transform-es2015-parameters": "^6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", - "babel-plugin-transform-es2015-spread": "^6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", - "babel-plugin-transform-es2015-template-literals": "^6.22.0", - "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", - "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", - "babel-plugin-transform-regenerator": "^6.24.1" - } - }, - "babel-preset-es2016": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-es2016/-/babel-preset-es2016-6.24.1.tgz", - "integrity": "sha1-+QC/k+LrwNJ235uKtZck6/2Vn4s=", - "dev": true, - "requires": { - "babel-plugin-transform-exponentiation-operator": "^6.24.1" - } - }, - "babel-preset-es2017": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-es2017/-/babel-preset-es2017-6.24.1.tgz", - "integrity": "sha1-WXvq37n38gi8/YoS6bKym4svFNE=", - "dev": true, - "requires": { - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-to-generator": "^6.24.1" - } - }, - "babel-preset-flow": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", - "integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=", - "requires": { - "babel-plugin-transform-flow-strip-types": "^6.22.0" - } - }, - "babel-preset-latest": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-latest/-/babel-preset-latest-6.24.1.tgz", - "integrity": "sha1-Z33gaRVKdIXC0lxXfAL2JLhbheg=", - "dev": true, - "requires": { - "babel-preset-es2015": "^6.24.1", - "babel-preset-es2016": "^6.24.1", - "babel-preset-es2017": "^6.24.1" - } - }, - "babel-preset-react": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", - "integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=", - "requires": { - "babel-plugin-syntax-jsx": "^6.3.13", - "babel-plugin-transform-react-display-name": "^6.23.0", - "babel-plugin-transform-react-jsx": "^6.24.1", - "babel-plugin-transform-react-jsx-self": "^6.22.0", - "babel-plugin-transform-react-jsx-source": "^6.22.0", - "babel-preset-flow": "^6.23.0" - } - }, - "babel-preset-stage-0": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", - "integrity": "sha1-VkLRUEL5E4TX5a+LyIsduVsDnmo=", - "requires": { - "babel-plugin-transform-do-expressions": "^6.22.0", - "babel-plugin-transform-function-bind": "^6.22.0", - "babel-preset-stage-1": "^6.24.1" - } - }, - "babel-preset-stage-1": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", - "integrity": "sha1-dpLNfc1oSZB+auSgqFWJz7niv7A=", - "requires": { - "babel-plugin-transform-class-constructor-call": "^6.24.1", - "babel-plugin-transform-export-extensions": "^6.22.0", - "babel-preset-stage-2": "^6.24.1" - } - }, - "babel-preset-stage-2": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", - "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", - "requires": { - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "babel-plugin-transform-class-properties": "^6.24.1", - "babel-plugin-transform-decorators": "^6.24.1", - "babel-preset-stage-3": "^6.24.1" - } - }, - "babel-preset-stage-3": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", - "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", - "requires": { - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-generator-functions": "^6.24.1", - "babel-plugin-transform-async-to-generator": "^6.24.1", - "babel-plugin-transform-exponentiation-operator": "^6.24.1", - "babel-plugin-transform-object-rest-spread": "^6.22.0" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "requires": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "babelify": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", - "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", - "requires": { - "babel-core": "^6.0.14", - "object-assign": "^4.0.0" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" - }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" - }, - "bail": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz", - "integrity": "sha512-1X8CnjFVQ+a+KW36uBNMTU5s8+v5FzeqrP7hTG5aTb4aPreSbZJlhwPon9VKMuEVgV++JM+SQrALY3kr7eswdg==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "base64id": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", - "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" - }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=" - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "requires": { - "callsite": "1.0.0" - } - }, - "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==" - }, - "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" - }, - "blob": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", - "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "~2.0.0" - } - }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" - }, - "body": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", - "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", - "requires": { - "continuable-cache": "^0.3.1", - "error": "^7.0.0", - "raw-body": "~1.1.0", - "safe-json-parse": "~1.0.1" - }, - "dependencies": { - "bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=" - }, - "raw-body": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", - "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=", - "requires": { - "bytes": "1", - "string_decoder": "0.10" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.1", - "http-errors": "~1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "~2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "~1.6.15" - }, - "dependencies": { - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - } - } - }, - "bonjour": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - } - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "requires": { - "hoek": "2.x.x" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browser-resolve": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", - "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - } - } - }, - "browser-sync": { - "version": "2.24.4", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.24.4.tgz", - "integrity": "sha512-qfXv8vQA/Dctub2v44v/vPuvfC4XNd6bn+W5vWZVuhuy6w91lPsdY6qhalT2s2PjnJ3FR6kWq5wkTQgN26eKzA==", - "requires": { - "browser-sync-ui": "v1.0.1", - "bs-recipes": "1.3.4", - "chokidar": "1.7.0", - "connect": "3.5.0", - "connect-history-api-fallback": "^1.5.0", - "dev-ip": "^1.0.1", - "easy-extender": "2.3.2", - "eazy-logger": "3.0.2", - "etag": "^1.8.1", - "fresh": "^0.5.2", - "fs-extra": "3.0.1", - "http-proxy": "1.15.2", - "immutable": "3.8.2", - "localtunnel": "1.9.0", - "micromatch": "2.3.11", - "opn": "4.0.2", - "portscanner": "2.1.1", - "qs": "6.2.3", - "raw-body": "^2.3.2", - "resp-modifier": "6.0.2", - "rx": "4.1.0", - "serve-index": "1.8.0", - "serve-static": "1.13.2", - "server-destroy": "1.0.1", - "socket.io": "2.0.4", - "ua-parser-js": "0.7.17", - "yargs": "6.4.0" - }, - "dependencies": { - "batch": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.5.3.tgz", - "integrity": "sha1-PzQU84AyF0O/wQQvmoP/HVgk1GQ=" - }, - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" - }, - "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "requires": { - "ms": "0.7.1" - } - }, - "eventemitter3": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", - "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=" - }, - "fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^3.0.0", - "universalify": "^0.1.0" - } - }, - "http-errors": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz", - "integrity": "sha1-eIwNLB3iyBuebowBhDtrl+uSB1A=", - "requires": { - "inherits": "2.0.3", - "setprototypeof": "1.0.2", - "statuses": ">= 1.3.1 < 2" - } - }, - "http-proxy": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.15.2.tgz", - "integrity": "sha1-ZC/cr/5S00SNK9o7AHnpQJBk2jE=", - "requires": { - "eventemitter3": "1.x.x", - "requires-port": "1.x.x" - } - }, - "jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" - }, - "opn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz", - "integrity": "sha1-erwi5kTf9jsKltWrfyeQwPAavJU=", - "requires": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - } - }, - "qs": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz", - "integrity": "sha1-HPyyXBCpsrSDBT/zn138kjOQjP4=" - }, - "serve-index": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.8.0.tgz", - "integrity": "sha1-fF2WwT+xMRAfk8HFd0+FFqHnjTs=", - "requires": { - "accepts": "~1.3.3", - "batch": "0.5.3", - "debug": "~2.2.0", - "escape-html": "~1.0.3", - "http-errors": "~1.5.0", - "mime-types": "~2.1.11", - "parseurl": "~1.3.1" - } - }, - "setprototypeof": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz", - "integrity": "sha1-gaVSFB7BBLiOic44MQOtXGZWTQg=" - }, - "window-size": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", - "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=" - }, - "yargs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.4.0.tgz", - "integrity": "sha1-gW4ahm1VmMzzTlWW3c4i2S2kkNQ=", - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "window-size": "^0.2.0", - "y18n": "^3.2.1", - "yargs-parser": "^4.1.0" - } - }, - "yargs-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", - "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", - "requires": { - "camelcase": "^3.0.0" - } - } - } - }, - "browser-sync-ui": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-1.0.1.tgz", - "integrity": "sha512-RIxmwVVcUFhRd1zxp7m2FfLnXHf59x4Gtj8HFwTA//3VgYI3AKkaQAuDL8KDJnE59XqCshxZa13JYuIWtZlKQg==", - "requires": { - "async-each-series": "0.1.1", - "connect-history-api-fallback": "^1.1.0", - "immutable": "^3.7.6", - "server-destroy": "1.0.1", - "socket.io-client": "2.0.4", - "stream-throttle": "^0.1.3" - } - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz", - "integrity": "sha512-zy0Cobe3hhgpiOM32Tj7KQ3Vl91m0njwsjzZQK1L+JDf11dzP9qIvjreVinsvXrgfjhStXwUWAEpB9D7Gwmayw==", - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", - "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", - "requires": { - "caniuse-lite": "^1.0.30000844", - "electron-to-chromium": "^1.3.47" - } - }, - "bs-recipes": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", - "integrity": "sha1-DS1NSKcYyMBEdp/cT4lZLci2lYU=" - }, - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-from": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" - }, - "buffer-indexof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==" - }, - "buffer-shims": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" - }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - } - }, - "caniuse-db": { - "version": "1.0.30000846", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000846.tgz", - "integrity": "sha1-2chvkUc4202gmO7e2ZdBPERWG9I=" - }, - "caniuse-lite": { - "version": "1.0.30000846", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000846.tgz", - "integrity": "sha512-qxUOHr5mTaadWH1ap0ueivHd8x42Bnemcn+JutVr7GWmm2bU4zoBhjuv5QdXgALQnnT626lOQros7cCDf8PwCg==" - }, - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" - }, - "ccount": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.3.tgz", - "integrity": "sha512-Jt9tIBkRc9POUof7QA/VwWd+58fKkEEfI+/t1/eOlxKM7ZhrczNzMFefge7Ai+39y1pR/pP6cI19guHy3FSLmw==" - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "character-entities": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.2.tgz", - "integrity": "sha512-sMoHX6/nBiy3KKfC78dnEalnpn0Az0oSNvqUWYTtYrhRI5iUIYsROU48G+E+kMFQzqXaJ8kHJZ85n7y6/PHgwQ==" - }, - "character-entities-html4": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.2.tgz", - "integrity": "sha512-sIrXwyna2+5b0eB9W149izTPJk/KkJTg6mEzDGibwBUkyH1SbDa+nf515Ppdi3MaH35lW0JFJDWeq9Luzes1Iw==" - }, - "character-entities-legacy": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz", - "integrity": "sha512-9NB2VbXtXYWdXzqrvAHykE/f0QJxzaKIpZ5QzNZrrgQ7Iyxr2vnfS8fCBNVW9nUEZE0lo57nxKRqnzY/dKrwlA==" - }, - "character-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", - "requires": { - "is-regex": "^1.0.3" - } - }, - "character-reference-invalid": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz", - "integrity": "sha512-7I/xceXfKyUJmSAn/jw8ve/9DyOP7XxufNYLI9Px7CmsKgEUaZLUTax6nZxGQtaoiZCjpu6cHPj20xC/vqRReQ==" - }, - "cheerio": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.14.0.tgz", - "integrity": "sha1-IJZI1QGEbelc3KZEDziaf1wp3I8=", - "requires": { - "CSSselect": "~0.4.0", - "entities": "~1.0.0", - "htmlparser2": "~3.7.0", - "lodash": "~2.4.1" - }, - "dependencies": { - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" - }, - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" - } - } - }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "requires": { - "anymatch": "^1.3.0", - "async-each": "^1.0.0", - "fsevents": "^1.0.0", - "glob-parent": "^2.0.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "clean-css": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", - "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", - "requires": { - "source-map": "0.5.x" - } - }, - "clipboard": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz", - "integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=", - "requires": { - "good-listener": "^1.2.2", - "select": "^1.1.2", - "tiny-emitter": "^2.0.0" - } - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=" - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=" - }, - "cloneable-readable": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz", - "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", - "requires": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, - "co": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", - "integrity": "sha1-TqVOpaCJOBUxheFSEMaNkJK8G3g=" - }, - "co-from-stream": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/co-from-stream/-/co-from-stream-0.0.0.tgz", - "integrity": "sha1-GlzYztdyY5RglPo58kmaYyl7yvk=", - "requires": { - "co-read": "0.0.1" - } - }, - "co-fs-extra": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/co-fs-extra/-/co-fs-extra-1.2.1.tgz", - "integrity": "sha1-O2rXfPJhRTD2d7HPYmZPW6dWtyI=", - "requires": { - "co-from-stream": "~0.0.0", - "fs-extra": "~0.26.5", - "thunkify-wrap": "~1.0.4" - } - }, - "co-read": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/co-read/-/co-read-0.0.1.tgz", - "integrity": "sha1-+Bs+uKhmdf7FHj2IOn9WToc8k4k=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "codemirror": { - "version": "5.38.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.38.0.tgz", - "integrity": "sha512-PEPnDg8U3DTGFB/Dn2T/INiRNC9CB5k2vLAQJidYCsHvAgtXbklqnuidEwx7yGrMrdGhl0L0P3iNKW9I07J6tQ==" - }, - "coffee-script": { - "version": "1.12.7", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", - "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==" - }, - "collapse-white-space": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.4.tgz", - "integrity": "sha512-YfQ1tAUZm561vpYD+5eyWN8+UsceQbSrqqlc/6zDY2gtAE+uZLSdkkovhnGpmCThsvKBFakq4EdY/FF93E8XIw==" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "requires": { - "color-name": "^1.1.1" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "comma-separated-tokens": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.5.tgz", - "integrity": "sha512-Cg90/fcK93n0ecgYTAz1jaA3zvnQ0ExlmKY1rdbyHqAx6BHxwoJc+J7HDu0iuQ7ixEs1qaa+WyQ6oeuBpYP1iA==", - "requires": { - "trim": "0.0.1" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" - }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" - }, - "compressible": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz", - "integrity": "sha1-DRAgq5JLL9tNYnmHXH1tq6a6p6k=", - "requires": { - "mime-db": ">= 1.33.0 < 2" - } - }, - "compression": { - "version": "1.7.2", - "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", - "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", - "requires": { - "accepts": "~1.3.4", - "bytes": "3.0.0", - "compressible": "~2.0.13", - "debug": "2.6.9", - "on-headers": "~1.0.1", - "safe-buffer": "5.1.1", - "vary": "~1.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "connect": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.5.0.tgz", - "integrity": "sha1-s1dSWgtMH1BZnNmD4dnv7qlncZg=", - "requires": { - "debug": "~2.2.0", - "finalhandler": "0.5.0", - "parseurl": "~1.3.1", - "utils-merge": "1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "requires": { - "ms": "0.7.1" - } - }, - "finalhandler": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.5.0.tgz", - "integrity": "sha1-6VCKvs6bbbqHGmlCodeRG5GRGsc=", - "requires": { - "debug": "~2.2.0", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "statuses": "~1.3.0", - "unpipe": "~1.0.0" - } - }, - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "utils-merge": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", - "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" - } - } - }, - "connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=" - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "requires": { - "date-now": "^0.1.4" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "consolidate": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.14.5.tgz", - "integrity": "sha1-WiUEe8dvcwcmZ8jLUsmJiI9JTGM=", - "requires": { - "bluebird": "^3.1.1" - } - }, - "constantinople": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", - "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", - "requires": { - "@types/babel-types": "^7.0.0", - "@types/babylon": "^6.16.2", - "babel-types": "^6.26.0", - "babylon": "^6.18.0" - } - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "continuable-cache": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", - "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=" - }, - "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-js": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "requires": { - "boom": "2.x.x" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "requires": { - "array-find-index": "^1.0.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, - "deepmerge": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", - "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==" - }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "requires": { - "foreach": "^2.0.5", - "object-keys": "^1.0.8" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, - "del": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", - "requires": { - "globby": "^6.1.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "p-map": "^1.1.1", - "pify": "^3.0.0", - "rimraf": "^2.2.8" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegate": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "detab": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detab/-/detab-2.0.1.tgz", - "integrity": "sha512-/hhdqdQc5thGrqzjyO/pz76lDZ5GSuAs6goxOaKTsvPk7HNnzAyFN5lyHgqpX4/s1i66K8qMGj+VhA9504x7DQ==", - "requires": { - "repeat-string": "^1.5.4" - } - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "requires": { - "repeating": "^2.0.0" - } - }, - "detect-node": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", - "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=" - }, - "detective": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", - "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", - "requires": { - "acorn": "^5.2.1", - "defined": "^1.0.0" - } - }, - "dev-ip": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", - "integrity": "sha1-p2o+0YVb56ASu4rBbLgPPADcKPA=" - }, - "diff": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", - "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=" - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "disparity": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/disparity/-/disparity-2.0.0.tgz", - "integrity": "sha1-V92stHMkrl9Y0swNqIbbTOnutxg=", - "requires": { - "ansi-styles": "^2.0.1", - "diff": "^1.3.2" - } - }, - "dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=" - }, - "dns-packet": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", - "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", - "requires": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, - "dns-txt": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", - "requires": { - "buffer-indexof": "^1.0.0" - } - }, - "doctrine-temporary-fork": { - "version": "2.0.0-alpha-allowarrayindex", - "resolved": "https://registry.npmjs.org/doctrine-temporary-fork/-/doctrine-temporary-fork-2.0.0-alpha-allowarrayindex.tgz", - "integrity": "sha1-QAFahn6yfnWybIKLcVJPE3+J+fA=", - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - }, - "doctypes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" - }, - "documentation": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/documentation/-/documentation-4.0.0.tgz", - "integrity": "sha1-moqajjiWm/1J008137J/HHW2R8A=", - "requires": { - "ansi-html": "^0.0.7", - "babel-core": "^6.17.0", - "babel-generator": "6.25.0", - "babel-plugin-system-import-transformer": "3.1.0", - "babel-plugin-transform-decorators-legacy": "^1.3.4", - "babel-preset-es2015": "^6.16.0", - "babel-preset-react": "^6.16.0", - "babel-preset-stage-0": "^6.16.0", - "babel-traverse": "^6.16.0", - "babel-types": "^6.16.0", - "babelify": "^7.3.0", - "babylon": "^6.17.2", - "chalk": "^2.0.0", - "chokidar": "^1.2.0", - "concat-stream": "^1.5.0", - "disparity": "^2.0.0", - "doctrine-temporary-fork": "2.0.0-alpha-allowarrayindex", - "get-comments": "^1.0.1", - "get-port": "^3.1.0", - "git-url-parse": "^6.0.1", - "github-slugger": "1.1.3", - "glob": "^7.0.0", - "globals-docs": "^2.3.0", - "highlight.js": "^9.1.0", - "js-yaml": "^3.8.4", - "lodash": "^4.11.1", - "mdast-util-inject": "^1.1.0", - "micromatch": "^3.0.0", - "mime": "^1.3.4", - "module-deps-sortable": "4.0.6", - "parse-filepath": "^1.0.1", - "pify": "^3.0.0", - "read-pkg-up": "^2.0.0", - "remark": "^8.0.0", - "remark-html": "6.0.1", - "remark-toc": "^4.0.0", - "remote-origin-url": "0.4.0", - "shelljs": "^0.7.5", - "stream-array": "^1.1.0", - "strip-json-comments": "^2.0.0", - "tiny-lr": "^1.0.3", - "unist-builder": "^1.0.0", - "unist-util-visit": "^1.0.1", - "vfile": "^2.0.0", - "vfile-reporter": "^4.0.0", - "vfile-sort": "^2.0.0", - "vinyl": "^2.0.0", - "vinyl-fs": "^2.3.1", - "yargs": "^6.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "babel-generator": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.25.0.tgz", - "integrity": "sha1-M6GvcNXyiQrrRlpKd5PB32qeqfw=", - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-types": "^6.25.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.2.0", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "requires": { - "pify": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "yargs": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", - "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^4.2.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "^0.2.0" - } - } - } - }, - "yargs-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", - "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", - "requires": { - "camelcase": "^3.0.0" - } - } - } - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "requires": { - "domelementtype": "~1.1.1", - "entities": "~1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" - } - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" - }, - "domhandler": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.2.1.tgz", - "integrity": "sha1-Wd+dzSJ+gIs2Wuc+H2aErD2Ub8I=", - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", - "integrity": "sha1-CGVRN5bGswYDGFDhdVFrr4C3Km8=", - "requires": { - "domelementtype": "1" - } - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "requires": { - "readable-stream": "^2.0.2" - } - }, - "duplexify": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", - "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "easy-extender": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.2.tgz", - "integrity": "sha1-PTJI/r4rFZYHMW2PnPSRwWZIIh0=", - "requires": { - "lodash": "^3.10.1" - }, - "dependencies": { - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - } - } - }, - "eazy-logger": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-3.0.2.tgz", - "integrity": "sha1-oyWqXlPROiIliJsqxBE7K5Y29Pw=", - "requires": { - "tfunk": "^3.0.1" - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "electron-to-chromium": { - "version": "1.3.48", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz", - "integrity": "sha1-07DYWTgUBE4JLs4hCPw6ya6kuQA=" - }, - "elliptic": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, - "emoji-regex": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.1.1.tgz", - "integrity": "sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4=" - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" - }, - "enable": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/enable/-/enable-1.3.2.tgz", - "integrity": "sha1-nrpoN9FtCYK1n4fYib91REPVKTE=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "engine.io": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.5.tgz", - "integrity": "sha512-D06ivJkYxyRrcEe0bTpNnBQNgP9d3xog+qZlLbui8EsMr/DouQpf5o9FzJnWYHEYE0YsFHllUv2R1dkgYZXHcA==", - "requires": { - "accepts": "~1.3.4", - "base64id": "1.0.0", - "cookie": "0.3.1", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.0", - "uws": "~9.14.0", - "ws": "~3.3.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "engine.io-client": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.6.tgz", - "integrity": "sha512-hnuHsFluXnsKOndS4Hv6SvUrgdYx1pk2NqfaDMW+GWdgfU3+/V25Cj7I8a0x92idSpa5PIhJRKxPvp9mnoLsfg==", - "requires": { - "component-emitter": "1.2.1", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.1", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~3.3.1", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "engine.io-parser": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz", - "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==", - "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.5", - "blob": "0.0.4", - "has-binary2": "~1.0.2" - } - }, - "enhanced-resolve": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", - "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "object-assign": "^4.0.1", - "tapable": "^0.2.7" - } - }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "requires": { - "prr": "~1.0.1" - } - }, - "error": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", - "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=", - "requires": { - "string-template": "~0.2.1", - "xtend": "~4.0.0" - } - }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.11.0.tgz", - "integrity": "sha512-ZnQrE/lXTTQ39ulXZ+J1DTFazV9qBy61x2bY071B+qGco8Z8q1QddsLdt/EF8Ai9hcWH72dWS0kFqXLxOxqslA==", - "requires": { - "es-to-primitive": "^1.1.1", - "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" - } - }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "requires": { - "is-callable": "^1.1.1", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.1" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "eventemitter3": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "eventsource": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz", - "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=", - "requires": { - "original": ">=0.0.5" - } - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "requires": { - "is-posix-bracket": "^0.1.0" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "requires": { - "fill-range": "^2.1.0" - } - }, - "express": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", - "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.3", - "qs": "6.5.1", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - } - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "requires": { - "is-extglob": "^1.0.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" - }, - "filename-reserved-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", - "integrity": "sha1-5hz4BfDeHJhFZ9A4bcXfUO5a9+Q=" - }, - "filenamify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", - "integrity": "sha1-qfL/0RxQO+0wABUCknI3jx8TZaU=", - "requires": { - "filename-reserved-regex": "^1.0.0", - "strip-outer": "^1.0.0", - "trim-repeated": "^1.0.0" - } - }, - "filenamify-url": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/filenamify-url/-/filenamify-url-1.0.0.tgz", - "integrity": "sha1-syvYExnvWGO3MHi+1Q9GpPeXX1A=", - "requires": { - "filenamify": "^1.0.0", - "humanize-url": "^1.0.0" - } - }, - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - } - }, - "find-cache-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", - "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", - "requires": { - "commondir": "^1.0.1", - "mkdirp": "^0.5.1", - "pkg-dir": "^1.0.0" - }, - "dependencies": { - "pkg-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", - "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", - "requires": { - "find-up": "^1.0.0" - } - } - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "first-chunk-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", - "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=" - }, - "follow-redirects": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.0.tgz", - "integrity": "sha512-fdrt472/9qQ6Kgjvb935ig6vJCuofpBUD14f9Vb+SLlm7xIe4Qva5gey8EKtv8lp7ahE1wilg3xL1znpVGtZIA==", - "requires": { - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "requires": { - "for-in": "^1.0.1" - } - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fs-extra": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", - "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" - } - }, - "fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "nan": { - "version": "2.10.0", - "resolved": "git+https://github.com/nodejs/nan.git#77d0fcaba3305d05176a9ad95d8e5101e8f2a283", - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "optional": true, - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "optional": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true - } - } - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "requires": { - "globule": "^1.0.0" - } - }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "requires": { - "is-property": "^1.0.0" - } - }, - "get-caller-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" - }, - "get-comments": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-comments/-/get-comments-1.0.1.tgz", - "integrity": "sha1-GWdZEBu7xPrPEwYMqu3Uhw3uVb4=" - }, - "get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "gh-pages": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-1.2.0.tgz", - "integrity": "sha512-cGLYAvxtlQ1iTwAS4g7FreZPXoE/g62Fsxln2mmR19mgs4zZI+XJ+wVVUhBFCF/0+Nmvbq+abyTWue1m1BSnmg==", - "requires": { - "async": "2.6.1", - "commander": "2.15.1", - "filenamify-url": "^1.0.0", - "fs-extra": "^5.0.0", - "globby": "^6.1.0", - "graceful-fs": "4.1.11", - "rimraf": "^2.6.2" - }, - "dependencies": { - "fs-extra": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - } - } - }, - "git-up": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-2.0.10.tgz", - "integrity": "sha512-2v4UN3qV2RGypD9QpmUjpk+4+RlYpW8GFuiZqQnKmvei08HsFPd0RfbDvEhnE4wBvnYs8ORVtYpOFuuCEmBVBw==", - "requires": { - "is-ssh": "^1.3.0", - "parse-url": "^1.3.0" - } - }, - "git-url-parse": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-6.2.2.tgz", - "integrity": "sha1-vkkCThS4SHVTQ2tFcri0OVMvqHE=", - "requires": { - "git-up": "^2.0.0" - } - }, - "github-slugger": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.1.3.tgz", - "integrity": "sha1-MUpudZoYwrDMV2DVEsy6tUnFSac=", - "requires": { - "emoji-regex": ">=6.0.0 <=6.1.1" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "requires": { - "is-glob": "^2.0.0" - } - }, - "glob-stream": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", - "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", - "requires": { - "extend": "^3.0.0", - "glob": "^5.0.3", - "glob-parent": "^3.0.0", - "micromatch": "^2.3.7", - "ordered-read-streams": "^0.3.0", - "through2": "^0.6.0", - "to-absolute-glob": "^0.1.1", - "unique-stream": "^2.0.2" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "requires": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } - } - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" - }, - "globals-docs": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/globals-docs/-/globals-docs-2.4.0.tgz", - "integrity": "sha512-B69mWcqCmT3jNYmSxRxxOXWfzu3Go8NQXPfl2o0qPd1EEFhwW0dFUg9ztTu915zPQzqwIhWAlw6hmfIcCK4kkQ==" - }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "globule": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", - "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.4", - "minimatch": "~3.0.2" - } - }, - "good-listener": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", - "requires": { - "delegate": "^3.1.2" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "gray-matter": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-2.1.1.tgz", - "integrity": "sha1-MELZrewqHe1qdwep7SOA+KF6Qw4=", - "requires": { - "ansi-red": "^0.1.1", - "coffee-script": "^1.12.4", - "extend-shallow": "^2.0.1", - "js-yaml": "^3.8.1", - "toml": "^2.3.2" - } - }, - "gulp-sourcemaps": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", - "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", - "requires": { - "convert-source-map": "^1.1.1", - "graceful-fs": "^4.1.2", - "strip-bom": "^2.0.0", - "through2": "^2.0.0", - "vinyl": "^1.0.0" - }, - "dependencies": { - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=" - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=" - }, - "vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - } - } - } - }, - "handle-thing": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", - "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=" - }, - "har-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", - "requires": { - "chalk": "^1.1.1", - "commander": "^2.9.0", - "is-my-json-valid": "^2.12.4", - "pinkie-promise": "^2.0.0" - } - }, - "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "requires": { - "function-bind": "^1.0.2" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - } - } - }, - "has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-generators": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-generators/-/has-generators-1.0.1.tgz", - "integrity": "sha1-pqLlVIYBGUBILhPiyTeRxEms9Ek=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "hash.js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.0" - } - }, - "hast-util-is-element": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.0.0.tgz", - "integrity": "sha1-P3IWl4sq4U2YdJh4eCZ18zvjzgA=" - }, - "hast-util-sanitize": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-1.1.2.tgz", - "integrity": "sha1-0QvWdXoh5ZwTq8iuNTDdO219Z54=", - "requires": { - "xtend": "^4.0.1" - } - }, - "hast-util-to-html": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-3.1.0.tgz", - "integrity": "sha1-iCyZhJ5AEw6ZHAQuRW1FPZXDbP8=", - "requires": { - "ccount": "^1.0.0", - "comma-separated-tokens": "^1.0.1", - "hast-util-is-element": "^1.0.0", - "hast-util-whitespace": "^1.0.0", - "html-void-elements": "^1.0.0", - "kebab-case": "^1.0.0", - "property-information": "^3.1.0", - "space-separated-tokens": "^1.0.0", - "stringify-entities": "^1.0.1", - "unist-util-is": "^2.0.0", - "xtend": "^4.0.1" - } - }, - "hast-util-whitespace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-1.0.0.tgz", - "integrity": "sha1-vQlpGWJdKTbh/xe8Tff9cn8X7Ok=" - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "requires": { - "boom": "2.x.x", - "cryptiles": "2.x.x", - "hoek": "2.x.x", - "sntp": "1.x.x" - } - }, - "highlight.js": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", - "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=" - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" - } - }, - "hosted-git-info": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==" - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "html-entities": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", - "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" - }, - "html-void-elements": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.3.tgz", - "integrity": "sha512-SaGhCDPXJVNrQyKMtKy24q6IMdXg5FCPN3z+xizxw9l+oXQw5fOoaj/ERU5KqWhSYhXtW5bWthlDbTDLBhJQrA==" - }, - "htmlparser2": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.7.3.tgz", - "integrity": "sha1-amTHdjfAjG8w7CqBV6UzM758sF4=", - "requires": { - "domelementtype": "1", - "domhandler": "2.2", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - }, - "dependencies": { - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "http-parser-js": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", - "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=" - }, - "http-proxy": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", - "requires": { - "eventemitter3": "^3.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz", - "integrity": "sha1-ZC6ISIUdZvCdTxJJEoRtuutBuDM=", - "requires": { - "http-proxy": "^1.16.2", - "is-glob": "^3.1.0", - "lodash": "^4.17.2", - "micromatch": "^2.3.11" - }, - "dependencies": { - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "requires": { - "assert-plus": "^0.2.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" - }, - "humanize-url": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/humanize-url/-/humanize-url-1.0.1.tgz", - "integrity": "sha1-9KuZ4NKIF0yk4eUEB8VfuuRk7/8=", - "requires": { - "normalize-url": "^1.0.0", - "strip-url-auth": "^1.0.0" - } - }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "ieee754": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz", - "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==" - }, - "immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" - }, - "import-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", - "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", - "requires": { - "pkg-dir": "^2.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "in-publish": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", - "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "requires": { - "repeating": "^2.0.0" - } - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "internal-ip": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-1.2.0.tgz", - "integrity": "sha1-rp+/k7mEh4eF1QqN4bNWlWBYz1w=", - "requires": { - "meow": "^3.3.0" - } - }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" - }, - "ipaddr.js": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", - "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" - }, - "is": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", - "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=" - }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-alphabetical": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.2.tgz", - "integrity": "sha512-V0xN4BYezDHcBSKb1QHUFMlR4as/XEuCZBzMJUU4n7+Cbt33SmUnSol+pnXFvLxSHNq2CemUXNdaXV6Flg7+xg==" - }, - "is-alphanumeric": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", - "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=" - }, - "is-alphanumerical": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz", - "integrity": "sha512-pyfU/0kHdISIgslFfZN9nfY1Gk3MquQgUm1mJTjdkEPpkAKNWuBTSqFwewOpR7N351VkErCiyV71zX7mlQQqsg==", - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "requires": { - "builtin-modules": "^1.0.0" - } - }, - "is-callable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", - "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" - }, - "is-decimal": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.2.tgz", - "integrity": "sha512-TRzl7mOCchnhchN+f3ICUCzYvL9ul7R+TYOsZ8xia++knyZAJfv/uA1FvQXsAnYIl1T3B2X5E/J7Wb1QXiIBXg==" - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "requires": { - "is-primitive": "^2.0.0" - } - }, - "is-expression": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", - "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", - "requires": { - "acorn": "~4.0.2", - "object-assign": "^4.0.1" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "requires": { - "is-extglob": "^1.0.0" - } - }, - "is-hexadecimal": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz", - "integrity": "sha512-but/G3sapV3MNyqiDBLrOi4x8uCIw0RY3o/Vb5GT0sMFHrVV7731wFSVy41T5FO1og7G0gXLJh0MkgPRouko/A==" - }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==" - }, - "is-my-json-valid": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-number-like": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", - "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", - "requires": { - "lodash.isfinite": "^3.3.2" - } - }, - "is-odd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", - "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", - "requires": { - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" - } - } - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=" - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "requires": { - "is-path-inside": "^1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "requires": { - "has": "^1.0.1" - } - }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "requires": { - "is-unc-path": "^1.0.0" - } - }, - "is-ssh": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.0.tgz", - "integrity": "sha1-6+oRaaJhTaOSpjdANmw84EnY3/Y=", - "requires": { - "protocols": "^1.1.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "requires": { - "unc-path-regex": "^0.1.2" - } - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" - }, - "is-valid-glob": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", - "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=" - }, - "is-whitespace-character": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz", - "integrity": "sha512-SzM+T5GKUCtLhlHFKt2SDAX2RFzfS6joT91F2/WSi9LxgFdsnhfPK/UIA+JhRR2xuyLdrCys2PiFDrtn1fU5hQ==" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "is-word-character": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.2.tgz", - "integrity": "sha512-T3FlsX8rCHAH8e7RE7PfOPZVFQlcV3XRF9eOOBQ1uf70OxO7CjjSOjeImMPCADBdYWcStAbVbYvJ1m2D3tb+EA==" - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "js-base64": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz", - "integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ==" - }, - "js-stringify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" - }, - "js-yaml": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", - "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" - }, - "json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=" - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "jstransformer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", - "requires": { - "is-promise": "^2.0.0", - "promise": "^7.0.1" - } - }, - "kebab-case": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.0.tgz", - "integrity": "sha1-P55JkK3K0MaGwOcB92RYaPdfkes=" - }, - "killable": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", - "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=" - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - }, - "klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", - "requires": { - "graceful-fs": "^4.1.9" - } - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "requires": { - "readable-stream": "^2.0.5" - } - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "^1.0.0" - } - }, - "limiter": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.3.tgz", - "integrity": "sha512-zrycnIMsLw/3ZxTbW7HCez56rcFGecWTx5OZNplzcXUUmJLmoYArC6qdJzmAN5BWiNXGcpjhF9RQ1HSv5zebEw==" - }, - "linkify-it": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz", - "integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=", - "requires": { - "uc.micro": "^1.0.1" - } - }, - "livereload-js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.3.0.tgz", - "integrity": "sha512-j1R0/FeGa64Y+NmqfZhyoVRzcFlOZ8sNlKzHjh4VvLULFACZhn68XrX5DFg2FhMvSMJmROuFxRSa560ECWKBMg==" - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "loader-runner": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", - "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=" - }, - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - }, - "localtunnel": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-1.9.0.tgz", - "integrity": "sha512-wCIiIHJ8kKIcWkTQE3m1VRABvsH2ZuOkiOpZUofUCf6Q42v3VIZ+Q0YfX1Z4sYDRj0muiKL1bLvz1FeoxsPO0w==", - "requires": { - "axios": "0.17.1", - "debug": "2.6.8", - "openurl": "1.1.1", - "yargs": "6.6.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" - }, - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "requires": { - "ms": "2.0.0" - } - }, - "yargs": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", - "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^4.2.0" - } - }, - "yargs-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", - "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", - "requires": { - "camelcase": "^3.0.0" - } - } - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - } - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "lodash._basecallback": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/lodash._basecallback/-/lodash._basecallback-3.3.1.tgz", - "integrity": "sha1-t7K7Q9whYEJKIczybFfkQ3cqjic=", - "requires": { - "lodash._baseisequal": "^3.0.0", - "lodash._bindcallback": "^3.0.0", - "lodash.isarray": "^3.0.0", - "lodash.pairs": "^3.0.0" - } - }, - "lodash._basecompareascending": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/lodash._basecompareascending/-/lodash._basecompareascending-3.0.2.tgz", - "integrity": "sha1-F+JPGB7qntKx+YncgAt2GWROrFM=" - }, - "lodash._baseeach": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz", - "integrity": "sha1-z4cGVyyhROjZ11InyZDamC+TKvM=", - "requires": { - "lodash.keys": "^3.0.0" - } - }, - "lodash._baseisequal": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz", - "integrity": "sha1-2AJfdjOdKTQnZ9zIh85cuVpbUfE=", - "requires": { - "lodash.isarray": "^3.0.0", - "lodash.istypedarray": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "lodash._basesortby": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._basesortby/-/lodash._basesortby-3.0.0.tgz", - "integrity": "sha1-0Kmq1Hu5F8DtkLHiLQOH6hiRKMs=" - }, - "lodash._bindcallback": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", - "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" - }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" - }, - "lodash.isfinite": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", - "integrity": "sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=" - }, - "lodash.istypedarray": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz", - "integrity": "sha1-yaR3SYYHUB2OhJTSg7h8OSgc72I=" - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "lodash.mergewith": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", - "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==" - }, - "lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=" - }, - "lodash.pairs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash.pairs/-/lodash.pairs-3.0.1.tgz", - "integrity": "sha1-u+CNV4bu6qCaFckevw3LfSvjJqk=", - "requires": { - "lodash.keys": "^3.0.0" - } - }, - "lodash.sortby": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-3.1.5.tgz", - "integrity": "sha1-mEA6z3X++yQGk4MfS8DZUflHAbg=", - "requires": { - "lodash._basecallback": "^3.0.0", - "lodash._basecompareascending": "^3.0.0", - "lodash._baseeach": "^3.0.0", - "lodash._basesortby": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash.isarray": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "loglevel": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz", - "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=" - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" - }, - "longest-streak": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.2.tgz", - "integrity": "sha512-TmYTeEYxiAmSVdpbnQDXGtvYOIRsCMg89CVZzwzc2o7GFL1CjoiRPjH5ec0NFAVlAx3fVof9dX/t6KKRAo2OWA==" - }, - "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "requires": { - "js-tokens": "^3.0.0" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "macro-inferno": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/macro-inferno/-/macro-inferno-0.2.1.tgz", - "integrity": "sha512-5fnnX5OJSJq4PFMKiiv+lKJqbxHHQXfvTLbRjV3fHIr86ELpljOzVEjEmsY2VI3R2RwWLUM0NHrygpexdH4S4g==", - "requires": { - "nan": "^2.7.0" - }, - "dependencies": { - "nan": { - "version": "2.10.0", - "resolved": "git+https://github.com/nodejs/nan.git#77d0fcaba3305d05176a9ad95d8e5101e8f2a283" - } - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" - } - }, - "markdown-escapes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.2.tgz", - "integrity": "sha512-lbRZ2mE3Q9RtLjxZBZ9+IMl68DKIXaVAhwvwn9pmjnPLS0h/6kyBMgNhqi1xFJ/2yv6cSyv0jbiZavZv93JkkA==" - }, - "markdown-it": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.1.tgz", - "integrity": "sha512-CzzqSSNkFRUf9vlWvhK1awpJreMRqdCrBvZ8DIoDWTOkESMIF741UPAhuAmbyWmdiFPA6WARNhnu2M6Nrhwa+A==", - "requires": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - }, - "markdown-it-anchor": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-2.7.1.tgz", - "integrity": "sha1-Ny9n2npMRjKtDr5MlpFybv4lNCo=", - "requires": { - "string": "^3.0.1" - } - }, - "markdown-table": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.2.tgz", - "integrity": "sha512-NcWuJFHDA8V3wkDgR/j4+gZx+YQwstPgfQDV8ndUeWWzta3dnDTBxpVzqS9lkmJAuV5YX35lmyojl6HO5JXAgw==" - }, - "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=" - }, - "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "mdast-util-compact": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.1.tgz", - "integrity": "sha1-zbX4TitqLTEU3zO9BdnLMuPECDo=", - "requires": { - "unist-util-modify-children": "^1.0.0", - "unist-util-visit": "^1.1.0" - } - }, - "mdast-util-definitions": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-1.2.2.tgz", - "integrity": "sha512-9NloPSwaB9f1PKcGqaScfqRf6zKOEjTIXVIbPOmgWI/JKxznlgVXC5C+8qgl3AjYg2vJBRgLYfLICaNiac89iA==", - "requires": { - "unist-util-visit": "^1.0.0" - } - }, - "mdast-util-inject": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-inject/-/mdast-util-inject-1.1.0.tgz", - "integrity": "sha1-2wa4tYW+lZotzS+H9HK6m3VvNnU=", - "requires": { - "mdast-util-to-string": "^1.0.0" - } - }, - "mdast-util-to-hast": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-2.5.0.tgz", - "integrity": "sha1-8IeETSVcdUDzaQbaMLoQbA7l7i8=", - "requires": { - "collapse-white-space": "^1.0.0", - "detab": "^2.0.0", - "mdast-util-definitions": "^1.2.0", - "mdurl": "^1.0.1", - "trim": "0.0.1", - "trim-lines": "^1.0.0", - "unist-builder": "^1.0.1", - "unist-util-generated": "^1.1.0", - "unist-util-position": "^3.0.0", - "unist-util-visit": "^1.1.0", - "xtend": "^4.0.1" - } - }, - "mdast-util-to-string": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.0.4.tgz", - "integrity": "sha1-XEVch4yTVfDB5/PotxnPWDaRrPs=" - }, - "mdast-util-toc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-toc/-/mdast-util-toc-2.0.1.tgz", - "integrity": "sha1-sdLLI7+wH4Evp7Vb/+iwqL7fbyE=", - "requires": { - "github-slugger": "^1.1.1", - "mdast-util-to-string": "^1.0.2", - "unist-util-visit": "^1.1.0" - } - }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "requires": { - "readable-stream": "^2.0.1" - } - }, - "metalsmith": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/metalsmith/-/metalsmith-2.3.0.tgz", - "integrity": "sha1-gzr7taKmOF4tmuPZNeOeM+rqUjE=", - "requires": { - "absolute": "0.0.1", - "chalk": "^1.1.3", - "clone": "^1.0.2", - "co-fs-extra": "^1.2.1", - "commander": "^2.6.0", - "gray-matter": "^2.0.0", - "has-generators": "^1.0.1", - "is": "^3.1.0", - "is-utf8": "~0.2.0", - "recursive-readdir": "^2.1.0", - "rimraf": "^2.2.8", - "stat-mode": "^0.2.0", - "thunkify": "^2.1.2", - "unyield": "0.0.1", - "ware": "^1.2.0", - "win-fork": "^1.1.1" - } - }, - "metalsmith-headings": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/metalsmith-headings/-/metalsmith-headings-0.1.0.tgz", - "integrity": "sha1-O/OVJPqbK1pUIlOJHh/xq1djJw0=", - "requires": { - "cheerio": "^0.14.0" - } - }, - "metalsmith-layouts": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/metalsmith-layouts/-/metalsmith-layouts-1.8.1.tgz", - "integrity": "sha1-o2XTmTnZFGzf5R+t7n2HVXP8y9w=", - "requires": { - "async": "^1.3.0", - "consolidate": "^0.14.0", - "debug": "^2.2.0", - "extend": "^3.0.0", - "fs-readdir-recursive": "^1.0.0", - "is-utf8": "^0.2.0", - "lodash.omit": "^4.0.2", - "multimatch": "^2.0.0" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - } - } - }, - "metalsmith-navigation": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/metalsmith-navigation/-/metalsmith-navigation-0.2.9.tgz", - "integrity": "sha1-6m7WU0zu+Fe9rUaaRNG6GWxW8w0=", - "requires": { - "lodash.sortby": "^3.1.1", - "merge": "^1.1.3" - } - }, - "metalsmith-sass": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/metalsmith-sass/-/metalsmith-sass-1.5.1.tgz", - "integrity": "sha1-3eLI1AjMugOJNaSdWErc433MVmw=", - "requires": { - "async": "^2.6.0", - "node-sass": "^4.7.2" - } - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "~1.33.0" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "module-deps-sortable": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/module-deps-sortable/-/module-deps-sortable-4.0.6.tgz", - "integrity": "sha1-ElGkuixEqS32mJvQKdoSGk8hCbA=", - "requires": { - "JSONStream": "^1.0.3", - "browser-resolve": "^1.7.0", - "concat-stream": "~1.5.0", - "defined": "^1.0.0", - "detective": "^4.0.0", - "duplexer2": "^0.1.2", - "inherits": "^2.0.1", - "parents": "^1.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.1.3", - "stream-combiner2": "^1.1.1", - "subarg": "^1.0.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - }, - "dependencies": { - "concat-stream": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", - "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", - "requires": { - "inherits": "~2.0.1", - "readable-stream": "~2.0.0", - "typedarray": "~0.0.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~0.10.x", - "util-deprecate": "~1.0.1" - } - } - } - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "ms-webpack": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ms-webpack/-/ms-webpack-1.0.2.tgz", - "integrity": "sha1-Gc5op/9j3ZNMZS56ojxGoq9c0j4=", - "requires": { - "chalk": "^1.1.3", - "memory-fs": "^0.3.0", - "supports-color": "^3.1.2", - "webpack": "^1.13.2" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" - }, - "memory-fs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.3.0.tgz", - "integrity": "sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=", - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "multicast-dns": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "requires": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - } - }, - "multicast-dns-service-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" - }, - "multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", - "requires": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" - } - }, - "nan": { - "version": "git+https://github.com/nodejs/nan.git#4885e20596e47a4bee8533043d6ea9e7349743d3", - "from": "git+https://github.com/nodejs/nan.git" - }, - "nanomatch": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", - "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-odd": "^2.0.0", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "native-node-utils": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/native-node-utils/-/native-node-utils-0.1.2.tgz", - "integrity": "sha512-eEsxT0In5UY7GhHIxIHviRA45VT0X5NNsjMWr70bYSQZma/059d0G68JwdY7+8ZWG6/z5zDhJK3JfuoOtYjMzg==", - "requires": { - "nan": "^2.8.0" - }, - "dependencies": { - "nan": { - "version": "2.10.0", - "resolved": "git+https://github.com/nodejs/nan.git#77d0fcaba3305d05176a9ad95d8e5101e8f2a283" - } - } - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "neo-async": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.1.tgz", - "integrity": "sha512-3KL3fvuRkZ7s4IFOMfztb7zJp3QaVWnBeGoJlgB38XnCRPj/0tLzzLG5IB8NYOHbJ8g8UGrgZv44GLDk6CxTxA==" - }, - "netlify-cli": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/netlify-cli/-/netlify-cli-1.2.2.tgz", - "integrity": "sha1-4nGZUrzUppj6dWFqsAPhEVQSB+A=", - "requires": { - "chalk": "^0.5.1", - "commander": "^2.9.0", - "github": "^0.2.4", - "home-dir": "^0.2.0", - "inquirer": "^0.11.0", - "netlify": "^1.2.0", - "open": "0.0.5", - "prompt": "^0.2.14", - "update-notifier": "^0.6.0", - "when": "^3.7.5" - }, - "dependencies": { - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" - }, - "ansi-regex": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", - "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=" - }, - "ansi-styles": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", - "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=" - }, - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - }, - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" - }, - "base64-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", - "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=" - }, - "boxen": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-0.3.1.tgz", - "integrity": "sha1-p9iYJDrmIvertrtgTXQKdsalRhs=", - "requires": { - "chalk": "^1.1.1", - "filled-array": "^1.0.0", - "object-assign": "^4.0.1", - "repeating": "^2.0.0", - "string-width": "^1.0.1", - "widest-line": "^1.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz", - "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "brace-expansion": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", - "integrity": "sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk=", - "requires": { - "balanced-match": "^0.4.1", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" - }, - "capture-stack-trace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" - }, - "chalk": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", - "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", - "requires": { - "ansi-styles": "^1.1.0", - "escape-string-regexp": "^1.0.0", - "has-ansi": "^0.1.0", - "strip-ansi": "^0.3.0", - "supports-color": "^0.2.0" - } - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "cli-width": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-1.1.1.tgz", - "integrity": "sha1-pNKT72frt7iNSk1CwMzwDE0eNm0=" - }, - "code-point-at": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.1.tgz", - "integrity": "sha1-EQTNNPm1tF0+uojxurwZJOHONfs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" - }, - "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "requires": { - "graceful-readlink": ">= 1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "configstore": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-2.1.0.tgz", - "integrity": "sha1-c3o6cDbpiGECqmCZ5HuzOrGroaE=", - "requires": { - "dot-prop": "^3.0.0", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "object-assign": "^4.0.1", - "os-tmpdir": "^1.0.0", - "osenv": "^0.1.0", - "uuid": "^2.0.1", - "write-file-atomic": "^1.1.2", - "xdg-basedir": "^2.0.0" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha1-uqy6N9GdEfnRRtNXi8mZWMN4fik=" - } - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cycle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, - "deep-extend": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz", - "integrity": "sha1-7+QRPQgIX05vlod1mBD4B0aeIlM=" - }, - "dot-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", - "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "requires": { - "readable-stream": "^2.0.2" - } - }, - "error-ex": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz", - "integrity": "sha1-5ntD8+gsluo6WE/+4Ln8MyXYAtk=", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=" - }, - "eyes": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "filled-array": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filled-array/-/filled-array-1.1.0.tgz", - "integrity": "sha1-w8T2xmO5I0WamqKZEtLQMfFQf4Q=" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "github": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/github/-/github-0.2.4.tgz", - "integrity": "sha1-JPp/DhP6EblGr5ETTFGYKpHOU4s=", - "requires": { - "mime": "^1.2.11" - } - }, - "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.2", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "got": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-5.6.0.tgz", - "integrity": "sha1-ux1+4WO3gIK7yOuDbz85UATqb78=", - "requires": { - "create-error-class": "^3.0.1", - "duplexer2": "^0.1.4", - "is-plain-obj": "^1.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "node-status-codes": "^1.0.0", - "object-assign": "^4.0.1", - "parse-json": "^2.1.0", - "pinkie-promise": "^2.0.0", - "read-all-stream": "^3.0.0", - "readable-stream": "^2.0.5", - "timed-out": "^2.0.0", - "unzip-response": "^1.0.0", - "url-parse-lax": "^1.0.0" - } - }, - "graceful-fs": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", - "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", - "requires": { - "natives": "^1.1.0" - } - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" - }, - "has-ansi": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", - "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", - "requires": { - "ansi-regex": "^0.2.0" - } - }, - "home-dir": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/home-dir/-/home-dir-0.2.0.tgz", - "integrity": "sha1-dlysM1OEtR8x/QRI5M3YBm4eLI8=" - }, - "i": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/i/-/i-0.3.5.tgz", - "integrity": "sha1-HSuFQVjsgWkRPGy39raAHpniEdU=" - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" - }, - "inquirer": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.11.4.tgz", - "integrity": "sha1-geM3ToNhvq/y2XAWIG01nQsy+k0=", - "requires": { - "ansi-escapes": "^1.1.0", - "ansi-regex": "^2.0.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^1.0.1", - "figures": "^1.3.5", - "lodash": "^3.3.1", - "readline2": "^1.0.1", - "run-async": "^0.1.0", - "rx-lite": "^3.1.2", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz", - "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "latest-version": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-2.0.0.tgz", - "integrity": "sha1-VvjWE5YghHuAF/jx9NeOIRMkFos=", - "requires": { - "package-json": "^2.0.0" - } - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - }, - "lowercase-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=" - }, - "mime": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", - "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" - }, - "minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", - "requires": { - "brace-expansion": "^1.0.0" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=" - }, - "natives": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.0.tgz", - "integrity": "sha1-6f+EFBimsux6SV6TmYT3jxY+bjE=" - }, - "ncp": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz", - "integrity": "sha1-q8xsvT7C7Spyn/bnwfqPAXhKhXQ=" - }, - "netlify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/netlify/-/netlify-1.2.0.tgz", - "integrity": "sha1-6/iblLAbR41TljAXTey0lsK74xU=", - "requires": { - "base64-js": ">=0.0.4", - "glob": ">=3.2.6", - "graceful-fs": "^3.0.4", - "semaphore": "^1.0.5", - "when": "^3.7.5" - } - }, - "node-status-codes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", - "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=" - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "object-assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", - "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" - }, - "open": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", - "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=" - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.3.tgz", - "integrity": "sha1-g88FxtZFj8TVrGNi6jJdkvJ1Qhc=", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "package-json": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", - "integrity": "sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=", - "requires": { - "got": "^5.0.0", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkginfo": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.0.tgz", - "integrity": "sha1-NJ27f/04CB/K3AhT32h/DHdEzWU=" - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "prompt": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/prompt/-/prompt-0.2.14.tgz", - "integrity": "sha1-V3VPZPVD/XsIRXB8gY7OYY8F/9w=", - "requires": { - "pkginfo": "0.x.x", - "read": "1.0.x", - "revalidator": "0.1.x", - "utile": "0.2.x", - "winston": "0.8.x" - } - }, - "rc": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", - "integrity": "sha1-Q2UbdrauU7XIAvEVH6P8OwWZack=", - "requires": { - "deep-extend": "~0.4.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~1.0.4" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", - "requires": { - "mute-stream": "~0.0.4" - } - }, - "read-all-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", - "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", - "requires": { - "pinkie-promise": "^2.0.0", - "readable-stream": "^2.0.0" - } - }, - "readable-stream": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", - "integrity": "sha1-ZvqLcg4UOLNkaB8q0aY8YYRIydA=", - "requires": { - "buffer-shims": "^1.0.0", - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~0.10.x", - "util-deprecate": "~1.0.1" - } - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "mute-stream": "0.0.5" - } - }, - "registry-auth-token": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.1.0.tgz", - "integrity": "sha1-mXwIJW4MeZmDe5DpRNs52KeQJ2s=", - "requires": { - "rc": "^1.1.6" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "requires": { - "rc": "^1.0.1" - } - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "requires": { - "is-finite": "^1.0.0" - } - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "revalidator": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", - "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=" - }, - "rimraf": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", - "integrity": "sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ=", - "requires": { - "glob": "^7.0.5" - } - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "requires": { - "once": "^1.3.0" - } - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=" - }, - "semaphore": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.0.5.tgz", - "integrity": "sha1-tJJXbmavGT25XWXiXsU/Xxl5jWA=" - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "requires": { - "semver": "^5.0.3" - } - }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" - }, - "stack-trace": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=" - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz", - "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "strip-ansi": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", - "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", - "requires": { - "ansi-regex": "^0.2.1" - } - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=" - }, - "supports-color": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", - "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=" - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "timed-out": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", - "integrity": "sha1-84sK6B03R9YoAB9B2vxlKs5nHAo=" - }, - "unzip-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.1.tgz", - "integrity": "sha1-SnOVnymJRw+lA3kc77VOHbvGhBI=" - }, - "update-notifier": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-0.6.3.tgz", - "integrity": "sha1-d23sjaoT6WKjQeih2YNUMGtnrgg=", - "requires": { - "boxen": "^0.3.1", - "chalk": "^1.0.0", - "configstore": "^2.0.0", - "is-npm": "^1.0.0", - "latest-version": "^2.0.0", - "semver-diff": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz", - "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "requires": { - "prepend-http": "^1.0.1" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utile": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz", - "integrity": "sha1-kwyI6ZCY1iIINMNWy9mncFItkNc=", - "requires": { - "async": "~0.2.9", - "deep-equal": "*", - "i": "0.3.x", - "mkdirp": "0.x.x", - "ncp": "0.4.x", - "rimraf": "2.x.x" - } - }, - "uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" - }, - "when": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/when/-/when-3.7.7.tgz", - "integrity": "sha1-q6A/w7tzbWyIsJHQE9io5ZDYRxg=" - }, - "widest-line": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz", - "integrity": "sha1-DAnIXCqUaD0Nfq+O4JfVZL8OEFw=", - "requires": { - "string-width": "^1.0.1" - } - }, - "winston": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-0.8.3.tgz", - "integrity": "sha1-ZLar9M0Brcrv1QCTk7HY6L7BnbA=", - "requires": { - "async": "0.2.x", - "colors": "0.6.x", - "cycle": "1.0.x", - "eyes": "0.1.x", - "isstream": "0.1.x", - "pkginfo": "0.3.x", - "stack-trace": "0.0.x" - }, - "dependencies": { - "pkginfo": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", - "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=" - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.2.0.tgz", - "integrity": "sha1-FMZtTkyzygVlwozzt6bz5NWTj6s=", - "requires": { - "graceful-fs": "^4.1.2", - "imurmurhash": "^0.1.4", - "slide": "^1.1.5" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha1-uqy6N9GdEfnRRtNXi8mZWMN4fik=" - } - } - }, - "xdg-basedir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", - "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", - "requires": { - "os-homedir": "^1.0.0" - } - } - } - }, - "node-forge": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", - "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==" - }, - "node-gyp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", - "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=", - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "2", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" - } - } - }, - "node-libs-browser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", - "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^1.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.0", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.10.3", - "vm-browserify": "0.0.4" - } - }, - "node-sass": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz", - "integrity": "sha512-QFHfrZl6lqRU3csypwviz2XLgGNOoWQbo2GOvtsfQqOfL4cy1BtWnhx/XUeAO9LT3ahBzSRXcEO6DdvAH9DzSg==", - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^3.0.0", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "in-publish": "^2.0.0", - "lodash.assign": "^4.2.0", - "lodash.clonedeep": "^4.3.2", - "lodash.mergewith": "^4.6.0", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "nan": "^2.10.0", - "node-gyp": "^3.3.1", - "npmlog": "^4.0.0", - "request": "~2.79.0", - "sass-graph": "^2.2.4", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" - }, - "dependencies": { - "nan": { - "version": "2.10.0", - "resolved": "git+https://github.com/nodejs/nan.git#77d0fcaba3305d05176a9ad95d8e5101e8f2a283" - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "requires": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" - }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", - "requires": { - "object-assign": "^4.0.1", - "prepend-http": "^1.0.0", - "query-string": "^4.1.0", - "sort-keys": "^1.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" - }, - "object-path": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.9.2.tgz", - "integrity": "sha1-D9mnT8X60a45aLWGvaXGMr1sBaU=" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "requires": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", - "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "opencv-build": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/opencv-build/-/opencv-build-0.0.12.tgz", - "integrity": "sha512-i0dm65Coh+v/rb0VJl8ajY+64A8uKs3qHr0PnBP+TbP0Vlfu8yxVYp2VeoJf8yJMLs0N72HLel6hG7fBvxI7UQ==", - "requires": { - "npmlog": "^4.1.2" - } - }, - "opencv4nodejs": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/opencv4nodejs/-/opencv4nodejs-4.5.1.tgz", - "integrity": "sha512-yx+uRSc5JgQiWp7oNBDwJWubJT4g6I6akeVio9VfpIivwJJCesRJ3n2uxX5QVCRFkk3bedD2vx+xJBSbRVc+zg==", - "requires": { - "@types/node": ">6", - "macro-inferno": "^0.2.1", - "nan": "^2.10.0", - "native-node-utils": "0.1.2", - "opencv-build": "^0.0.12" - }, - "dependencies": { - "nan": { - "version": "2.10.0", - "resolved": "git+https://github.com/nodejs/nan.git#77d0fcaba3305d05176a9ad95d8e5101e8f2a283" - } - } - }, - "openurl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", - "integrity": "sha1-OHW0sO96UsFW8NtB1GCduw+Us4c=" - }, - "opn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", - "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", - "requires": { - "is-wsl": "^1.1.0" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "ordered-read-streams": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", - "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", - "requires": { - "is-stream": "^1.0.1", - "readable-stream": "^2.0.1" - } - }, - "original": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/original/-/original-1.0.1.tgz", - "integrity": "sha512-IEvtB5vM5ULvwnqMxWBLxkS13JIEXbakizMSo3yoPNPCIWzg8TG3Usn/UhXoZFM/m+FuEA20KdzPSFq/0rS+UA==", - "requires": { - "url-parse": "~1.4.0" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "requires": { - "lcid": "^1.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "output-file-sync": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", - "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", - "requires": { - "graceful-fs": "^4.1.4", - "mkdirp": "^0.5.1", - "object-assign": "^4.1.0" - } - }, - "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "pako": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==" - }, - "parents": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", - "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", - "requires": { - "path-platform": "~0.11.15" - } - }, - "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", - "requires": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" - } - }, - "parse-entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.1.2.tgz", - "integrity": "sha512-5N9lmQ7tmxfXf+hO3X6KRG6w7uYO/HL9fHalSySTdyn63C3WNvTM/1R8tn1u1larNcEbo3Slcy2bsVDQqvEpUg==", - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - } - }, - "parse-git-config": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-0.2.0.tgz", - "integrity": "sha1-Jygz/dFf6hRvt10zbSNrljtv9wY=", - "requires": { - "ini": "^1.3.3" - } - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-url": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-1.3.11.tgz", - "integrity": "sha1-V8FUKKuKiSsfQ4aWRccR0OFEtVQ=", - "requires": { - "is-ssh": "^1.3.0", - "protocols": "^1.4.0" - } - }, - "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", - "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=", - "requires": { - "process": "^0.11.1", - "util": "^0.10.3" - } - }, - "path-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=" - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" - }, - "path-platform": { - "version": "0.11.15", - "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", - "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=" - }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", - "requires": { - "path-root-regex": "^0.1.0" - } - }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pbkdf2": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", - "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==", - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "pbkdf2-compat": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz", - "integrity": "sha1-tuDI+plJTZTgURV1gCpZpcFC8og=" - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "requires": { - "find-up": "^2.1.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } - } - } - }, - "portfinder": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz", - "integrity": "sha1-uzLs2HwnEErm7kS1o8y/Drsa7ek=", - "requires": { - "async": "^1.5.2", - "debug": "^2.2.0", - "mkdirp": "0.5.x" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - } - } - }, - "portscanner": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.1.1.tgz", - "integrity": "sha1-6rtAnk3iSVD1oqUW01rnaTQ/u5Y=", - "requires": { - "async": "1.5.2", - "is-number-like": "^1.0.3" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "postcss-scss": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-0.4.1.tgz", - "integrity": "sha1-rXcbgfD3L19IRdCKpg+TVXZT1Uw=", - "requires": { - "postcss": "^5.2.13" - } - }, - "postcss-value-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", - "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=" - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=" - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "requires": { - "asap": "~2.0.3" - } - }, - "property-information": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-3.2.0.tgz", - "integrity": "sha1-/RSDyPusYYCPX+NZ52k6H0ilgzE=" - }, - "protocols": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.6.tgz", - "integrity": "sha1-+LsmPqG1/Xp2BNJri+Ob13Z4v4o=" - }, - "proxy-addr": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", - "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.6.0" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "public-encrypt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", - "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "pug": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.3.tgz", - "integrity": "sha1-ccuoJTfJWl6rftBGluQiH1Oqh44=", - "requires": { - "pug-code-gen": "^2.0.1", - "pug-filters": "^3.1.0", - "pug-lexer": "^4.0.0", - "pug-linker": "^3.0.5", - "pug-load": "^2.0.11", - "pug-parser": "^5.0.0", - "pug-runtime": "^2.0.4", - "pug-strip-comments": "^1.0.3" - } - }, - "pug-attrs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.3.tgz", - "integrity": "sha1-owlflw5kFR972tlX7vVftdeQXRU=", - "requires": { - "constantinople": "^3.0.1", - "js-stringify": "^1.0.1", - "pug-runtime": "^2.0.4" - } - }, - "pug-code-gen": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.1.tgz", - "integrity": "sha1-CVHsgyJddNjPxHan+Zolm199BQw=", - "requires": { - "constantinople": "^3.0.1", - "doctypes": "^1.1.0", - "js-stringify": "^1.0.1", - "pug-attrs": "^2.0.3", - "pug-error": "^1.3.2", - "pug-runtime": "^2.0.4", - "void-elements": "^2.0.1", - "with": "^5.0.0" - } - }, - "pug-error": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz", - "integrity": "sha1-U659nSm7A89WRJOgJhCfVMR/XyY=" - }, - "pug-filters": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.0.tgz", - "integrity": "sha1-JxZVVbwEwjbkqisDZiRt+gIbYm4=", - "requires": { - "clean-css": "^4.1.11", - "constantinople": "^3.0.1", - "jstransformer": "1.0.0", - "pug-error": "^1.3.2", - "pug-walk": "^1.1.7", - "resolve": "^1.1.6", - "uglify-js": "^2.6.1" - } - }, - "pug-lexer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.0.0.tgz", - "integrity": "sha1-IQwYRX7y4XYCQnQMXmR715TOwng=", - "requires": { - "character-parser": "^2.1.1", - "is-expression": "^3.0.0", - "pug-error": "^1.3.2" - } - }, - "pug-linker": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.5.tgz", - "integrity": "sha1-npp65ABWgtAn3uuWsAD4juuDoC8=", - "requires": { - "pug-error": "^1.3.2", - "pug-walk": "^1.1.7" - } - }, - "pug-load": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.11.tgz", - "integrity": "sha1-5kjlftET/iwfRdV4WOorrWvAFSc=", - "requires": { - "object-assign": "^4.1.0", - "pug-walk": "^1.1.7" - } - }, - "pug-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.0.tgz", - "integrity": "sha1-45Stmz/KkxI5QK/4hcBuRKt+aOQ=", - "requires": { - "pug-error": "^1.3.2", - "token-stream": "0.0.1" - } - }, - "pug-runtime": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.4.tgz", - "integrity": "sha1-4XjhvaaKsujArPybztLFT9iM61g=" - }, - "pug-strip-comments": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.3.tgz", - "integrity": "sha1-8VWVkiBu3G+FMQ2s9K+0igJa9Z8=", - "requires": { - "pug-error": "^1.3.2" - } - }, - "pug-walk": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.7.tgz", - "integrity": "sha1-wA1cUSi6xYBr7BXSt+fNq+QlMfM=" - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", - "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=" - }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" - }, - "querystringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", - "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==" - }, - "randomatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", - "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", - "requires": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "unpipe": "1.0.0" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": ">= 1.3.1 < 2" - } - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - } - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "requires": { - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "readable-stream": "^2.0.2", - "set-immediate-shim": "^1.0.1" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "requires": { - "resolve": "^1.1.6" - } - }, - "recursive-readdir": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", - "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", - "requires": { - "minimatch": "3.0.4" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" - }, - "regenerate-unicode-properties": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-6.0.0.tgz", - "integrity": "sha512-BvXxRS7RfVWxtm7vrq+0I0j7sqZ1zeSC+yzf5HS0qLnKcZPX541gFEGB39LvGuKHrkyKXrzXug+oC7xkM1Zovw==", - "dev": true, - "requires": { - "regenerate": "^1.3.3" - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "regenerator-transform": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", - "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", - "requires": { - "babel-runtime": "^6.18.0", - "babel-types": "^6.19.0", - "private": "^0.1.6" - } - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "requires": { - "is-equal-shallow": "^0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "regexpu-core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", - "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", - "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" - } - }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", - "requires": { - "jsesc": "~0.5.0" - } - }, - "remark": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/remark/-/remark-8.0.0.tgz", - "integrity": "sha512-K0PTsaZvJlXTl9DN6qYlvjTkqSZBFELhROZMrblm2rB+085flN84nz4g/BscKRMqDvhzlK1oQ/xnWQumdeNZYw==", - "requires": { - "remark-parse": "^4.0.0", - "remark-stringify": "^4.0.0", - "unified": "^6.0.0" - } - }, - "remark-html": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-6.0.1.tgz", - "integrity": "sha1-UJTSxx95Qf2yroZbrHZid1fOCcE=", - "requires": { - "hast-util-sanitize": "^1.0.0", - "hast-util-to-html": "^3.0.0", - "mdast-util-to-hast": "^2.1.1", - "xtend": "^4.0.1" - } - }, - "remark-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-4.0.0.tgz", - "integrity": "sha512-XZgICP2gJ1MHU7+vQaRM+VA9HEL3X253uwUM/BGgx3iv6TH2B3bF3B8q00DKcyP9YrJV+/7WOWEWBFF/u8cIsw==", - "requires": { - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^1.0.2", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^1.0.0", - "vfile-location": "^2.0.0", - "xtend": "^4.0.1" - } - }, - "remark-slug": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/remark-slug/-/remark-slug-4.2.3.tgz", - "integrity": "sha1-jZh9Dl5j1KSeo3uQ/pmaPc/IG3I=", - "requires": { - "github-slugger": "^1.0.0", - "mdast-util-to-string": "^1.0.0", - "unist-util-visit": "^1.0.0" - } - }, - "remark-stringify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-4.0.0.tgz", - "integrity": "sha512-xLuyKTnuQer3ke9hkU38SUYLiTmS078QOnoFavztmbt/pAJtNSkNtFgR0U//uCcmG0qnyxao+PDuatQav46F1w==", - "requires": { - "ccount": "^1.0.0", - "is-alphanumeric": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "longest-streak": "^2.0.1", - "markdown-escapes": "^1.0.0", - "markdown-table": "^1.1.0", - "mdast-util-compact": "^1.0.0", - "parse-entities": "^1.0.2", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "stringify-entities": "^1.0.1", - "unherit": "^1.0.4", - "xtend": "^4.0.1" - } - }, - "remark-toc": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-toc/-/remark-toc-4.0.1.tgz", - "integrity": "sha1-/zb/beVOoH3Vnj9TNKSjqsHpMYU=", - "requires": { - "mdast-util-toc": "^2.0.0", - "remark-slug": "^4.0.0" - } - }, - "remote-origin-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/remote-origin-url/-/remote-origin-url-0.4.0.tgz", - "integrity": "sha1-TT4pAvNOLTfRwmPYdxC3frQIajA=", - "requires": { - "parse-git-config": "^0.2.0" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "requires": { - "is-finite": "^1.0.0" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" - }, - "request": { - "version": "2.79.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", - "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.11.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", - "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~2.0.6", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "qs": "~6.3.0", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "~0.4.1", - "uuid": "^3.0.0" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "resolve": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", - "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", - "requires": { - "path-parse": "^1.0.5" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "resp-modifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", - "integrity": "sha1-sSTeXE+6/LpUH0j/pzlw9KpFa08=", - "requires": { - "debug": "^2.2.0", - "minimatch": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "requires": { - "align-text": "^0.1.1" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "^7.0.5" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-json-parse": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", - "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } - }, - "sass-graph": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", - "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", - "requires": { - "glob": "^7.0.0", - "lodash": "^4.0.0", - "scss-tokenizer": "^0.2.3", - "yargs": "^7.0.0" - } - }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "requires": { - "js-base64": "^2.1.8", - "source-map": "^0.4.2" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" - }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" - }, - "selfsigned": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.3.tgz", - "integrity": "sha512-vmZenZ+8Al3NLHkWnhBQ0x6BkML1eCP2xEi3JE+f3D9wW9fipD9NNJHYtE9XJM4TsPaHGZJIamrSI6MTg1dU2Q==", - "requires": { - "node-forge": "0.7.5" - } - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - } - }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "server-destroy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", - "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shelljs": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", - "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "requires": { - "hoek": "2.x.x" - } - }, - "socket.io": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.4.tgz", - "integrity": "sha1-waRZDO/4fs8TxyZS8Eb3FrKeYBQ=", - "requires": { - "debug": "~2.6.6", - "engine.io": "~3.1.0", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.0.4", - "socket.io-parser": "~3.1.1" - } - }, - "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" - }, - "socket.io-client": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz", - "integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=", - "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~2.6.4", - "engine.io-client": "~3.1.0", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "~3.1.1", - "to-array": "0.1.4" - } - }, - "socket.io-parser": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.3.tgz", - "integrity": "sha512-g0a2HPqLguqAczs3dMECuA1RgoGFPyvDqcbaDEdCWY9g59kdUAz3YRmaJBNKXflrHNwB7Q12Gkf/0CZXfdHR7g==", - "requires": { - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "has-binary2": "~1.0.2", - "isarray": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - } - } - }, - "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", - "requires": { - "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" - } - }, - "sockjs-client": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz", - "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=", - "requires": { - "debug": "^2.6.6", - "eventsource": "0.1.6", - "faye-websocket": "~0.11.0", - "inherits": "^2.0.1", - "json3": "^3.3.2", - "url-parse": "^1.1.8" - }, - "dependencies": { - "faye-websocket": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", - "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", - "requires": { - "websocket-driver": ">=0.5.1" - } - } - } - }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", - "requires": { - "is-plain-obj": "^1.0.0" - } - }, - "source-list-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "requires": { - "source-map": "^0.5.6" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "space-separated-tokens": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz", - "integrity": "sha512-G3jprCEw+xFEs0ORweLmblJ3XLymGGr6hxZYTYZjIlvDti9vOBUjRQa1Rzjt012aRrocKstHwdNi+F7HguPsEA==", - "requires": { - "trim": "0.0.1" - } - }, - "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==" - }, - "spdy": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", - "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", - "requires": { - "debug": "^2.6.8", - "handle-thing": "^1.2.5", - "http-deceiver": "^1.2.7", - "safe-buffer": "^5.0.1", - "select-hose": "^2.0.0", - "spdy-transport": "^2.0.18" - } - }, - "spdy-transport": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.0.tgz", - "integrity": "sha512-bpUeGpZcmZ692rrTiqf9/2EUakI6/kXX1Rpe0ib/DyOzbiexVfXkw6GnvI9hVGvIwVaUhkaBojjCZwLNRGQg1g==", - "requires": { - "debug": "^2.6.8", - "detect-node": "^2.0.3", - "hpack.js": "^2.1.6", - "obuf": "^1.1.1", - "readable-stream": "^2.2.9", - "safe-buffer": "^5.0.1", - "wbuf": "^1.7.2" - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "tweetnacl": "~0.14.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "stat-mode": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", - "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=" - }, - "state-toggle": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz", - "integrity": "sha512-Qe8QntFrrpWTnHwvwj2FZTgv+PKIsp0B9VxLzLLbSpPXWOgRgc5LVj/aTiSfK1RqIeF9jeC1UeOH8Q8y60A7og==" - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - }, - "stdout-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", - "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", - "requires": { - "readable-stream": "^2.0.1" - } - }, - "stream-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/stream-array/-/stream-array-1.1.2.tgz", - "integrity": "sha1-nl9zRfITfDDuO0mLkRToC1K7frU=", - "requires": { - "readable-stream": "~2.1.0" - }, - "dependencies": { - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "readable-stream": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", - "integrity": "sha1-ZvqLcg4UOLNkaB8q0aY8YYRIydA=", - "requires": { - "buffer-shims": "^1.0.0", - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~0.10.x", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", - "requires": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "stream-http": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.2.tgz", - "integrity": "sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==", - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" - }, - "stream-throttle": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", - "integrity": "sha1-rdV8jXzHOoFjDTHNVdOWHPr7qcM=", - "requires": { - "commander": "^2.2.0", - "limiter": "^1.0.5" - } - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" - }, - "string": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/string/-/string-3.3.3.tgz", - "integrity": "sha1-XqIRzZLSKOGEKUmQpsyXs2anfLA=" - }, - "string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=" - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "stringify-entities": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", - "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", - "requires": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "stringstream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", - "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-bom-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", - "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", - "requires": { - "first-chunk-stream": "^1.0.0", - "strip-bom": "^2.0.0" - } - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "requires": { - "get-stdin": "^4.0.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "strip-outer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", - "requires": { - "escape-string-regexp": "^1.0.2" - } - }, - "strip-url-auth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-url-auth/-/strip-url-auth-1.0.1.tgz", - "integrity": "sha1-IrD6OkE4WzO+PzMVUbu4N/oM164=" - }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "requires": { - "minimist": "^1.1.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "tapable": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", - "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=" - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" - } - }, - "tfunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tfunk/-/tfunk-3.1.0.tgz", - "integrity": "sha1-OORBT8ZJd9h6/apy+sttKfgve1s=", - "requires": { - "chalk": "^1.1.1", - "object-path": "^0.9.0" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - } - }, - "through2-filter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", - "requires": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - }, - "thunkify": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", - "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=" - }, - "thunkify-wrap": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/thunkify-wrap/-/thunkify-wrap-1.0.4.tgz", - "integrity": "sha1-tSvlSN3+/aIOALWMYJZ2K0PdaIA=", - "requires": { - "enable": "1" - } - }, - "thunky": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.2.tgz", - "integrity": "sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=" - }, - "time-stamp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-2.0.0.tgz", - "integrity": "sha1-lcakRTDhW6jW9KPsuMOj+sRto1c=" - }, - "timers-browserify": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", - "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", - "requires": { - "setimmediate": "^1.0.4" - } - }, - "tiny-emitter": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz", - "integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==" - }, - "tiny-lr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", - "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", - "requires": { - "body": "^5.1.0", - "debug": "^3.1.0", - "faye-websocket": "~0.10.0", - "livereload-js": "^2.3.0", - "object-assign": "^4.1.0", - "qs": "^6.4.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - } - } - }, - "to-absolute-glob": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", - "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", - "requires": { - "extend-shallow": "^2.0.1" - } - }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - } - } - } - }, - "token-stream": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", - "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" - }, - "toml": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/toml/-/toml-2.3.3.tgz", - "integrity": "sha512-O7L5hhSQHxuufWUdcTRPfuTh3phKfAZ/dqfxZFoxPCj2RYmpaSGLEIs016FCXItQwNr08yefUB5TSjzRYnajTA==" - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "requires": { - "punycode": "^1.4.1" - } - }, - "trim": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=" - }, - "trim-lines": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-1.1.1.tgz", - "integrity": "sha512-X+eloHbgJGxczUk1WSjIvn7aC9oN3jVE3rQfRVKcgpavi3jxtCn0VVKtjOBj64Yop96UYn/ujJRpTbCdAF1vyg==" - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" - }, - "trim-repeated": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", - "requires": { - "escape-string-regexp": "^1.0.2" - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" - }, - "trim-trailing-lines": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.1.tgz", - "integrity": "sha512-bWLv9BbWbbd7mlqqs2oQYnLD/U/ZqeJeJwbO0FG2zA1aTq+HTvxfHNKFa/HGCVyJpDiioUYaBhfiT6rgk+l4mg==" - }, - "trough": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.2.tgz", - "integrity": "sha512-FHkoUZvG6Egrv9XZAyYGKEyb1JMsFphgPjoczkZC2y6W93U1jswcVURB8MUvtsahEPEVACyxD47JAL63vF4JsQ==" - }, - "true-case-path": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", - "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", - "requires": { - "glob": "^6.0.4" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" - }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "ua-parser-js": { - "version": "0.7.17", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", - "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==" - }, - "uc.micro": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz", - "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==" - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=" - }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" - }, - "unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" - }, - "unherit": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.1.tgz", - "integrity": "sha512-+XZuV691Cn4zHsK0vkKYwBEwB74T3IZIcxrgn2E4rKwTfFyI1zCh7X7grwh9Re08fdPlarIdyWgI8aVB3F5A5g==", - "requires": { - "inherits": "^2.0.1", - "xtend": "^4.0.1" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.3.tgz", - "integrity": "sha512-iG/2t0F2LAU8aZYPkX5gi7ebukHnr3sWFESpb+zPQeeaQwOkfoO6ZW17YX7MdRPNG9pCy+tjzGill+Ah0Em0HA==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.3.tgz", - "integrity": "sha512-nFcaBFcr08UQNF15ZgI5ISh3yUnQm7SJRRxwYrL5VYX46pS+6Q7TCTv4zbK+j6/l7rQt0mMiTL2zpmeygny6rA==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.2", - "unicode-property-aliases-ecmascript": "^1.0.3" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.1.tgz", - "integrity": "sha512-lM8B0FDZQh9yYGgiabRQcyWicB27VLOolSBRIxsO7FeQPtg+79Oe7sC8Mzr8BObDs+G9CeYmC/shHo6OggNEog==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.3.tgz", - "integrity": "sha512-TdDmDOTxEf2ad1g3ZBpM6cqKIb2nJpVlz1Q++casDryKz18tpeMBhSng9hjC1CTQCkOV9Rw2knlSB6iRo7ad1w==", - "dev": true - }, - "unified": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", - "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", - "requires": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^1.1.0", - "trough": "^1.0.0", - "vfile": "^2.0.0", - "x-is-string": "^0.1.0" - } - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "unique-stream": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", - "requires": { - "json-stable-stringify": "^1.0.0", - "through2-filter": "^2.0.0" - } - }, - "unist-builder": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-1.0.2.tgz", - "integrity": "sha1-jDuZA+9kvPsRfdfPal2Y/Bs7J7Y=", - "requires": { - "object-assign": "^4.1.0" - } - }, - "unist-util-generated": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.2.tgz", - "integrity": "sha512-1HcwiEO62dr0XWGT+abVK4f0aAm8Ik8N08c5nAYVmuSxfvpA9rCcNyX/le8xXj1pJK5nBrGlZefeWB6bN8Pstw==" - }, - "unist-util-is": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.2.tgz", - "integrity": "sha512-YkXBK/H9raAmG7KXck+UUpnKiNmUdB+aBGrknfQ4EreE1banuzrKABx3jP6Z5Z3fMSPMQQmeXBlKpCbMwBkxVw==" - }, - "unist-util-modify-children": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-1.1.2.tgz", - "integrity": "sha512-GRi04yhng1WqBf5RBzPkOtWAadcZS2gvuOgNn/cyJBYNxtTuyYqTKN0eg4rC1YJwGnzrqfRB3dSKm8cNCjNirg==", - "requires": { - "array-iterate": "^1.0.0" - } - }, - "unist-util-position": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.0.1.tgz", - "integrity": "sha512-05QfJDPI7PE1BIUtAxeSV+cDx21xP7+tUZgSval5CA7tr0pHBwybF7OnEa1dOFqg6BfYH/qiMUnWwWj+Frhlww==" - }, - "unist-util-remove-position": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.2.tgz", - "integrity": "sha512-XxoNOBvq1WXRKXxgnSYbtCF76TJrRoe5++pD4cCBsssSiWSnPEktyFrFLE8LTk3JW5mt9hB0Sk5zn4x/JeWY7Q==", - "requires": { - "unist-util-visit": "^1.1.0" - } - }, - "unist-util-stringify-position": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", - "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==" - }, - "unist-util-visit": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.3.1.tgz", - "integrity": "sha512-0fdB9EQJU0tho5tK0VzOJzAQpPv2LyLZ030b10GxuzAWEfvd54mpY7BMjQ1L69k2YNvL+SvxRzH0yUIehOO8aA==", - "requires": { - "unist-util-is": "^2.1.1" - } - }, - "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "unyield": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/unyield/-/unyield-0.0.1.tgz", - "integrity": "sha1-FQ5l2kK/d0JEW5WKZOubhdHSsYA=", - "requires": { - "co": "~3.1.0" - } - }, - "upath": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==" - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - } - } - }, - "url-parse": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.0.tgz", - "integrity": "sha512-ERuGxDiQ6Xw/agN4tuoCRbmwRuZP0cJ1lJxJubXr5Q/5cDa78+Dc4wfvtxzhzhkm5VvmW6Mf8EVj9SPGN4l8Lg==", - "requires": { - "querystringify": "^2.0.0", - "requires-port": "^1.0.0" - } - }, - "use": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", - "requires": { - "kind-of": "^6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "user-home": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", - "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=" - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "requires": { - "inherits": "2.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" - }, - "uws": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/uws/-/uws-9.14.0.tgz", - "integrity": "sha512-HNMztPP5A1sKuVFmdZ6BPVpBQd5bUjNC8EFMFiICK+oho/OQsAJy5hnIx4btMHiOk8j04f/DbIlqnEZ9d72dqg==", - "optional": true - }, - "v8flags": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", - "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", - "requires": { - "user-home": "^1.1.1" - } - }, - "vali-date": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", - "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=" - }, - "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "vfile": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", - "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", - "requires": { - "is-buffer": "^1.1.4", - "replace-ext": "1.0.0", - "unist-util-stringify-position": "^1.0.0", - "vfile-message": "^1.0.0" - } - }, - "vfile-location": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.3.tgz", - "integrity": "sha512-zM5/l4lfw1CBoPx3Jimxoc5RNDAHHpk6AM6LM0pTIkm5SUSsx8ZekZ0PVdf0WEZ7kjlhSt7ZlqbRL6Cd6dBs6A==" - }, - "vfile-message": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.0.1.tgz", - "integrity": "sha512-vSGCkhNvJzO6VcWC6AlJW4NtYOVtS+RgCaqFIYUjoGIlHnFL+i0LbtYvonDWOMcB97uTPT4PRsyYY7REWC9vug==", - "requires": { - "unist-util-stringify-position": "^1.1.1" - } - }, - "vfile-reporter": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-4.0.0.tgz", - "integrity": "sha1-6m8K4TQvSEFXOYXgX5QXNvJ96do=", - "requires": { - "repeat-string": "^1.5.0", - "string-width": "^1.0.0", - "supports-color": "^4.1.0", - "unist-util-stringify-position": "^1.0.0", - "vfile-statistics": "^1.1.0" - }, - "dependencies": { - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "requires": { - "has-flag": "^2.0.0" - } - } - } - }, - "vfile-sort": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-2.1.1.tgz", - "integrity": "sha512-+fpTWKkVHwI6VF2xtkDTuCA6cH4UPLAxh+KxfU8g8pC0do5RSZCk1HXTTtMJguW0t5jC0PC19owjUZX9SGQ9tw==" - }, - "vfile-statistics": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-1.1.1.tgz", - "integrity": "sha512-dxUM6IYvGChHuwMT3dseyU5BHprNRXzAV0OHx1A769lVGsTiT50kU7BbpRFV+IE6oWmU+PwHdsTKfXhnDIRIgQ==" - }, - "vinyl": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", - "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "dependencies": { - "clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=" - } - } - }, - "vinyl-fs": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", - "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", - "requires": { - "duplexify": "^3.2.0", - "glob-stream": "^5.3.2", - "graceful-fs": "^4.0.0", - "gulp-sourcemaps": "1.6.0", - "is-valid-glob": "^0.3.0", - "lazystream": "^1.0.0", - "lodash.isequal": "^4.0.0", - "merge-stream": "^1.0.0", - "mkdirp": "^0.5.0", - "object-assign": "^4.0.0", - "readable-stream": "^2.0.4", - "strip-bom": "^2.0.0", - "strip-bom-stream": "^1.0.0", - "through2": "^2.0.0", - "through2-filter": "^2.0.0", - "vali-date": "^1.0.0", - "vinyl": "^1.0.0" - }, - "dependencies": { - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=" - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=" - }, - "vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - } - } - } - }, - "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "requires": { - "indexof": "0.0.1" - } - }, - "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" - }, - "ware": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ware/-/ware-1.3.0.tgz", - "integrity": "sha1-0bFPOdLiy0q4xAmPdW/ksWTkc9Q=", - "requires": { - "wrap-fn": "^0.1.0" - } - }, - "watchpack": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", - "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", - "requires": { - "chokidar": "^2.0.2", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", - "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.1.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^2.1.1", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "webpack": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.15.0.tgz", - "integrity": "sha1-T/MfU9sDM55VFkqdRo7gMklo/pg=", - "requires": { - "acorn": "^3.0.0", - "async": "^1.3.0", - "clone": "^1.0.2", - "enhanced-resolve": "~0.9.0", - "interpret": "^0.6.4", - "loader-utils": "^0.2.11", - "memory-fs": "~0.3.0", - "mkdirp": "~0.5.0", - "node-libs-browser": "^0.7.0", - "optimist": "~0.6.0", - "supports-color": "^3.1.0", - "tapable": "~0.1.8", - "uglify-js": "~2.7.3", - "watchpack": "^0.2.1", - "webpack-core": "~0.6.9" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "browserify-aes": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-0.4.0.tgz", - "integrity": "sha1-BnFJtmjfMcS1hTPgLQHoBthgjiw=", - "requires": { - "inherits": "^2.0.1" - } - }, - "browserify-zlib": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", - "requires": { - "pako": "~0.2.0" - } - }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, - "crypto-browserify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.3.0.tgz", - "integrity": "sha1-ufx1u0oO1h3PHNXa6W6zDJw+UGw=", - "requires": { - "browserify-aes": "0.4.0", - "pbkdf2-compat": "2.0.1", - "ripemd160": "0.2.0", - "sha.js": "2.2.6" - } - }, - "enhanced-resolve": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", - "integrity": "sha1-TW5omzcl+GCQknzMhs2fFjW4ni4=", - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.2.0", - "tapable": "^0.1.8" - }, - "dependencies": { - "memory-fs": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", - "integrity": "sha1-8rslNovBIeORwlIN6Slpyu4KApA=" - } - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" - }, - "https-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", - "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=" - }, - "interpret": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-0.6.6.tgz", - "integrity": "sha1-/s16GOfOXKar+5U+H4YhOknxYls=" - }, - "memory-fs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.3.0.tgz", - "integrity": "sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=", - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "node-libs-browser": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-0.7.0.tgz", - "integrity": "sha1-PicsCBnjCJNeJmdECNevDhSRuDs=", - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.1.4", - "buffer": "^4.9.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "3.3.0", - "domain-browser": "^1.1.1", - "events": "^1.0.0", - "https-browserify": "0.0.1", - "os-browserify": "^0.2.0", - "path-browserify": "0.0.0", - "process": "^0.11.0", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.0.5", - "stream-browserify": "^2.0.1", - "stream-http": "^2.3.1", - "string_decoder": "^0.10.25", - "timers-browserify": "^2.0.2", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.10.3", - "vm-browserify": "0.0.4" - } - }, - "os-browserify": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", - "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=" - }, - "pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" - }, - "ripemd160": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-0.2.0.tgz", - "integrity": "sha1-K/GYveFnys+lHAqSjoS2i74XH84=" - }, - "sha.js": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.2.6.tgz", - "integrity": "sha1-F93t3F9yL7ZlAWWIlUYZd4ZzFbo=" - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "requires": { - "has-flag": "^1.0.0" - } - }, - "tapable": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", - "integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=" - }, - "uglify-js": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.5.tgz", - "integrity": "sha1-RhLAx7qu4rp8SH3kkErhIgefLKg=", - "requires": { - "async": "~0.2.6", - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - } - } - }, - "watchpack": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-0.2.9.tgz", - "integrity": "sha1-Yuqkq15bo1/fwBgnVibjwPXj+ws=", - "requires": { - "async": "^0.9.0", - "chokidar": "^1.0.0", - "graceful-fs": "^4.1.2" - }, - "dependencies": { - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" - } - } - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } - } - }, - "webpack-core": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz", - "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=", - "requires": { - "source-list-map": "~0.1.7", - "source-map": "~0.4.1" - }, - "dependencies": { - "source-list-map": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", - "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=" - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "webpack-dev-middleware": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz", - "integrity": "sha512-FCrqPy1yy/sN6U/SaEZcHKRXGlqU0DUaEBL45jkUYoB8foVb6wCnbIJ1HKIx+qUFTW+3JpVcCJCxZ8VATL4e+A==", - "requires": { - "memory-fs": "~0.4.1", - "mime": "^1.5.0", - "path-is-absolute": "^1.0.0", - "range-parser": "^1.0.3", - "time-stamp": "^2.0.0" - }, - "dependencies": { - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - } - } - }, - "webpack-dev-server": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz", - "integrity": "sha512-zrPoX97bx47vZiAXfDrkw8pe9QjJ+lunQl3dypojyWwWr1M5I2h0VSrMPfTjopHQPRNn+NqfjcMmhoLcUJe2gA==", - "requires": { - "ansi-html": "0.0.7", - "array-includes": "^3.0.3", - "bonjour": "^3.5.0", - "chokidar": "^2.0.0", - "compression": "^1.5.2", - "connect-history-api-fallback": "^1.3.0", - "debug": "^3.1.0", - "del": "^3.0.0", - "express": "^4.16.2", - "html-entities": "^1.2.0", - "http-proxy-middleware": "~0.17.4", - "import-local": "^1.0.0", - "internal-ip": "1.2.0", - "ip": "^1.1.5", - "killable": "^1.0.0", - "loglevel": "^1.4.1", - "opn": "^5.1.0", - "portfinder": "^1.0.9", - "selfsigned": "^1.9.1", - "serve-index": "^1.7.2", - "sockjs": "0.3.19", - "sockjs-client": "1.1.4", - "spdy": "^3.4.1", - "strip-ansi": "^3.0.0", - "supports-color": "^5.1.0", - "webpack-dev-middleware": "1.12.2", - "yargs": "6.6.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" - }, - "chokidar": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", - "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.1.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^2.1.1", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.0" - } - }, - "compression": { - "version": "1.7.2", - "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", - "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", - "requires": { - "accepts": "~1.3.4", - "bytes": "3.0.0", - "compressible": "~2.0.13", - "debug": "2.6.9", - "on-headers": "~1.0.1", - "safe-buffer": "5.1.1", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "webpack-dev-middleware": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz", - "integrity": "sha512-FCrqPy1yy/sN6U/SaEZcHKRXGlqU0DUaEBL45jkUYoB8foVb6wCnbIJ1HKIx+qUFTW+3JpVcCJCxZ8VATL4e+A==", - "requires": { - "memory-fs": "~0.4.1", - "mime": "^1.5.0", - "path-is-absolute": "^1.0.0", - "range-parser": "^1.0.3", - "time-stamp": "^2.0.0" - } - }, - "yargs": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", - "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^4.2.0" - } - }, - "yargs-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", - "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", - "requires": { - "camelcase": "^3.0.0" - } - } - } - }, - "webpack-hot-middleware": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.22.2.tgz", - "integrity": "sha512-uccPS6b/UlXJoNCS+3fuc40z2KZgO0qQhnu+Ne1iZiHTy9s5fMCJAV+Vc8VTVkN203UphsxQmkumxYeHLiQ5jg==", - "requires": { - "ansi-html": "0.0.7", - "html-entities": "^1.2.0", - "querystring": "^0.2.0", - "strip-ansi": "^3.0.0" - } - }, - "webpack-sources": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", - "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "websocket-driver": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", - "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", - "requires": { - "http-parser-js": ">=0.4.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "win-fork": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/win-fork/-/win-fork-1.1.1.tgz", - "integrity": "sha1-j1jgZW/KAK3IyGoriePNLWotXl4=" - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" - }, - "with": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", - "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", - "requires": { - "acorn": "^3.1.0", - "acorn-globals": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" - } - } - }, - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "wrap-fn": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/wrap-fn/-/wrap-fn-0.1.5.tgz", - "integrity": "sha1-8htuQQFv9KfjFyDbxjoJAWvfmEU=", - "requires": { - "co": "3.1.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } - }, - "x-is-string": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", - "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=" - }, - "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" - } - } - }, - "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", - "requires": { - "camelcase": "^3.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" - } - } - }, - "yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" - } - } -} diff --git a/docgen/package.json b/docgen/package.json deleted file mode 100755 index 2f95658c..00000000 --- a/docgen/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "instantsearch-ios-docgen", - "version": "0.1.0", - "main": "index.js", - "license": "MIT", - "scripts": { - "dev": "babel-node start.js", - "build": "babel-node build.js", - "deploy": "gh-pages -d ../docs" - }, - "dependencies": { - "algolia-frontend-components": "0.0.24", - "autoprefixer": "^6.6.0", - "babel-cli": "^6.18.0", - "babel-loader": "^6.2.10", - "babel-preset-env": "^1.7.0", - "browser-sync": "^2.18.5", - "clipboard": "^1.6.1", - "codemirror": "^5.22.0", - "compression": "^1.6.2", - "documentation": "^4.0.0-rc.1", - "gh-pages": "^1.2.0", - "json-loader": "^0.5.4", - "markdown-it": "^8.2.2", - "markdown-it-anchor": "^2.6.0", - "metalsmith": "^2.3.0", - "metalsmith-headings": "^0.1.0", - "metalsmith-layouts": "^1.7.0", - "metalsmith-navigation": "^0.2.9", - "metalsmith-sass": "^1.3.0", - "ms-webpack": "^1.0.2", - "nan": "git+https://github.com/nodejs/nan.git", - "netlify-cli": "^1.1.0", - "node-sass": "^4.1.1", - "opencv4nodejs": "^4.5.1", - "postcss-scss": "^0.4.0", - "pug": "^2.0.0-beta6", - "webpack": "^1.14.0", - "webpack-dev-middleware": "^1.9.0", - "webpack-hot-middleware": "^2.14.0" - }, - "devDependencies": { - "@babel/preset-env": "^7.0.0-beta.49", - "babel-preset-latest": "^6.24.1", - "babel-preset-stage-2": "^6.24.1" - } -} diff --git a/docgen/path.js b/docgen/path.js deleted file mode 100755 index 8f0e23d9..00000000 --- a/docgen/path.js +++ /dev/null @@ -1,3 +0,0 @@ -import {join} from 'path'; - -export const rootPath = (...args) => join(__dirname, '..', ...args); diff --git a/docgen/plugins/assets.js b/docgen/plugins/assets.js deleted file mode 100755 index c123ae12..00000000 --- a/docgen/plugins/assets.js +++ /dev/null @@ -1,79 +0,0 @@ -// extracted from https://github.com/treygriffith/metalsmith-assets -// converted to es6 (http://lebab.io/try-it) -// tweaked to add `stats` to the file object - -import fs from 'fs'; -import path from 'path'; -import readdir from 'recursive-readdir'; -import mode from 'stat-mode'; -import {each} from 'async'; - -/** - * Expose `assets`. - */ - -export default assets; - -/** - * Default plugin options - */ -const defaults = { - source: './public', - destination: '.', -}; - -/** - * Metalsmith plugin to include static assets. - * - * @param {Object} userOptions (optional) - * @property {String} source Path to copy static assets from (relative to working directory). Defaults to './public' - * @property {String} destination Path to copy static assets to (relative to destination directory). Defaults to '.' - * @return {Function} a Metalsmith plugin - */ -function assets(userOptions = {}) { - const options = { - ...defaults, - ...userOptions, - }; - - return (files, metalsmith, cb) => { - const src = metalsmith.path(options.source); - const dest = options.destination; - - // copied almost line for line from https://github.com/segmentio/metalsmith/blob/master/lib/index.js - readdir(src, (readDirError, arr) => { - if (readDirError) { - cb(readDirError); - return; - } - - each(arr, read, err => cb(err, files)); - }); - - function read(file, done) { - const name = path.join(dest, path.relative(src, file)); - fs.stat(file, (statError, stats) => { - if (statError) { - done(statError); - return; - } - - fs.readFile(file, (err, buffer) => { - if (err) { - done(err); - return; - } - - const newFile = {}; - - newFile.contents = buffer; - newFile.stats = stats; - - newFile.mode = mode(stats).toOctal(); - files[name] = newFile; - done(); - }); - }); - } - }; -} diff --git a/docgen/plugins/autoprefixer.js b/docgen/plugins/autoprefixer.js deleted file mode 100755 index 89ff59e1..00000000 --- a/docgen/plugins/autoprefixer.js +++ /dev/null @@ -1,18 +0,0 @@ -import postcss from 'postcss'; -import syntax from 'postcss-scss'; -import autoprefixer from 'autoprefixer'; - -export default function sassAutoprefixer(files, metalsmith, done) { - const processor = postcss([autoprefixer]); - Object - .keys(files) - .filter(file => (/\.css$/).test(file)) - .forEach(file => { - const originalContent = files[file].contents.toString(); - const autoprefixedContent = processor.process(originalContent, {syntax}).css; - files[file].contents = new Buffer(autoprefixedContent); - files[file].stats.mtime = (new Date()).toISOString(); - }); - - done(); -} diff --git a/docgen/plugins/helpers.js b/docgen/plugins/helpers.js deleted file mode 100755 index 3e757363..00000000 --- a/docgen/plugins/helpers.js +++ /dev/null @@ -1,25 +0,0 @@ -import markdown from 'markdown-it'; -import highlight from '../syntaxHighlighting.js'; - -const md = markdown(); - -// this plugin provides ATM one helper to easily compute the publicPath of assets -export default function helpers(filenames, metalsmith, cb) { - metalsmith.metadata().h = { - markdown(src) { - return md.render(src); - }, - highlight(src, opts) { - const lang = opts && opts.lang; - return highlight(src, lang); - }, - maybeActive(navPath, singlePathOrArrayOfPaths) { - const pathsToTest = [].concat(singlePathOrArrayOfPaths); - return pathsToTest.some(pathToTest => navPath.indexOf(pathToTest) === 0) ? - 'active' : - ''; - }, - }; - - cb(); -} diff --git a/docgen/plugins/ignore.js b/docgen/plugins/ignore.js deleted file mode 100755 index 1d29a9a7..00000000 --- a/docgen/plugins/ignore.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function ignore(testFn) { - return (files, metalsmith, cb) => { - Object - .keys(files) - .forEach(fileName => { - if (testFn(fileName) === true) delete files[fileName]; - }); - - cb(null); - }; -} diff --git a/docgen/plugins/markdown.js b/docgen/plugins/markdown.js deleted file mode 100755 index 02eb2f62..00000000 --- a/docgen/plugins/markdown.js +++ /dev/null @@ -1,20 +0,0 @@ -import {basename, dirname, extname} from 'path'; -import md from '../mdRenderer'; - -const isMarkdown = filepath => (/\.md|\.markdown/).test(extname(filepath)); - -export default function markdown(files, metalsmith, done) { - Object.keys(files).forEach(file => { - if (!isMarkdown(file)) return; - const data = files[file]; - const dir = dirname(file); - let html = `${basename(file, extname(file))}.html`; - if (dir !== '.') html = `${dir}/${html}`; - const str = md.render(data.contents.toString(), {path: html}); - data.contents = new Buffer(str); - delete files[file]; - files[html] = data; - }); - - done(); -} diff --git a/docgen/plugins/navigation.js b/docgen/plugins/navigation.js deleted file mode 100755 index a86490f6..00000000 --- a/docgen/plugins/navigation.js +++ /dev/null @@ -1,42 +0,0 @@ -import {forEach} from 'lodash'; - -export default function() { - return function(files, metalsmith, done) { - const categories = {}; - - // First we scann all the HTML files to retrieve all the related documents based - // on the category attribute in the metadata - forEach(files, (data, path) => { - if (!path.match(/\.html$/) || data.tocVisibility === false) return; - const category = data.category || 'other'; - categories[category] = categories[category] || []; - categories[category].push({ - path, - title: data.title, - navWeight: data.navWeight, - metadata: data, - }); - }); - - // Then we go through all the files again to attach in the navigation attribute - // all the related documents - forEach(files, (data, path) => { - if (!path.match(/\.html$/)) return; - const category = data.category || 'other'; - // The navigation is sorted by weight first. A document with weigth is always more important - // than one without. - // Then navigation is sorted by title. - data.navigation = categories[category].sort((a, b) => { - if (a.title && b.title && - a.navWeight === b.navWeight) { - return a.title.localeCompare(b.title); - } else { - return b.navWeight - a.navWeight; - } - }); - data.navPath = path; - }); - - done(); - }; -} diff --git a/docgen/plugins/onlyChanged.js b/docgen/plugins/onlyChanged.js deleted file mode 100755 index 3af05e27..00000000 --- a/docgen/plugins/onlyChanged.js +++ /dev/null @@ -1,83 +0,0 @@ -// This plugin mutates the `files` map of metalsmith -// at build time to only keep what was updated -// We consider any update in layouts/ to trigger an update on every html file -// Otherwise, if it's an asset or a file with no layout then we compare -// the file's last modification time to the process start time - -import {join} from 'path'; - -import {parallel} from 'async'; -import {watch} from 'chokidar'; - -let lastRunTime = false; -let layoutChange = true; -let cssChange = true; -const layoutFiles = join(__dirname, '../layouts/**/*'); -const cssFiles = join(__dirname, '../src/stylesheets/**/*'); -const CSSEntryPoints = ['stylesheets/index.css', 'stylesheets/header.css']; - -export const hasChanged = file => file.stats && file.stats.ctime && file.stats.mtime ? - Date.parse(file.stats.ctime) > lastRunTime || Date.parse(file.stats.mtime) > lastRunTime : - true; - -export default function onlyChanged(files, metalsmith, cb) { - if (lastRunTime === false) { - watch(layoutFiles, {ignoreInitial: true}) - .on('all', () => { layoutChange = true; }) - .on('error', err => { throw err; }); - - watch(cssFiles, {ignoreInitial: true}) - .on('all', () => { cssChange = true; }) - .on('error', err => { throw err; }); - - lastRunTime = Date.now(); - layoutChange = false; - cssChange = false; - cb(null); - return; - } - - parallel( - Object - .entries(files) - .map(([name, file]) => done => { - if (!file.stats) { - done(null); // keep file, we do not know - return; - } - - if (!file.layout) { - const cssEntryPointNeedsUpdate = CSSEntryPoints.indexOf(name) !== -1 && cssChange === true; - - if (!hasChanged(file) && !cssEntryPointNeedsUpdate) { - // file has no layout and was not updated, remove file - // from files to process - delete files[name]; - } - - done(null); - return; - } - - if (/\.html$/.test(name) && layoutChange === true) { - done(null); // html page need rebuild, some layout files changed - return; - } - - if (!hasChanged(file)) { - delete files[name]; // file was not updated, layouts were not updated - } - - done(null); - }), - err => { - if (!err) { - lastRunTime = Date.now(); - layoutChange = false; - cssChange = false; - console.log(`[metalsmith]: onlyChanged | ${Object.keys(files)}`); // eslint-disable-line no-console - } - cb(err); - } - ); -} diff --git a/docgen/plugins/perf.js b/docgen/plugins/perf.js deleted file mode 100755 index 57809ed9..00000000 --- a/docgen/plugins/perf.js +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable no-console */ - -export const start = (label = 'performance') => (files, metalsmith, cb) => { - console.time(label); - console.log(`${Object.entries(files).length} file(s) to process: ${Object.keys(files)}`); - cb(); -}; - -export const stop = (label = 'performance') => (files, metalsmith, cb) => { - console.timeEnd(label); - cb(); -}; diff --git a/docgen/plugins/sources.js b/docgen/plugins/sources.js deleted file mode 100755 index 69ffb1fc..00000000 --- a/docgen/plugins/sources.js +++ /dev/null @@ -1,58 +0,0 @@ -import {glob} from 'glob'; -import {stat} from 'fs'; -import async from 'async'; - -export default function sourcesPlugin(sources, {ignore, computeFilename}) { - return (files, m, pluginDone) => - async.reduce( - sources, - {}, - (sourcesMemo, source, globDone) => { - glob( - source, - {ignore}, - (globErr, filenames) => { - if (globErr) { - globDone(globErr); - return; - } - - async.reduce( - filenames, - {}, - (statMemo, filename, statDone) => { - stat(filename, (statErr, stats) => { - if (statErr) { - pluginDone(statErr); - return; - } - - statDone(null, {...statMemo, [computeFilename(filename)]: {stats}}); - }); - }, - (err, filesnamesWithStat) => { - if (err) { - globDone(err); - return; - } - - globDone(null, { - ...sourcesMemo, - ...filesnamesWithStat, - }); - } - ); - } - ); - }, - (err, newFiles) => { - if (err) { - pluginDone(err); - return; - } - - Object.assign(files, newFiles); - pluginDone(null); - } - ); -} diff --git a/docgen/plugins/webpackEntryMetadata.js b/docgen/plugins/webpackEntryMetadata.js deleted file mode 100755 index 366497d3..00000000 --- a/docgen/plugins/webpackEntryMetadata.js +++ /dev/null @@ -1,16 +0,0 @@ -// this plugin adds the webpack entry points to metadata.webpack.assets -// useful in dev mode when not using ms-webpack -export default function webpackEntryMetadata(webpackConfig) { - return (filenames, metalsmith, cb) => { - metalsmith.metadata().webpack = { - assets: Object - .keys(webpackConfig.entry) - .reduce((memo, entryName) => ({ - ...memo, - [`${entryName}.js`]: `${webpackConfig.output.publicPath}${entryName}.js`, - }), {}), - }; - - cb(); - }; -} diff --git a/docgen/readme-logo.png b/docgen/readme-logo.png deleted file mode 100755 index e842dd21..00000000 Binary files a/docgen/readme-logo.png and /dev/null differ diff --git a/docgen/src/concepts.md b/docgen/src/concepts.md deleted file mode 100644 index bcfc2a21..00000000 --- a/docgen/src/concepts.md +++ /dev/null @@ -1,260 +0,0 @@ ---- -title: Main Concepts -layout: main.pug -name: concepts -category: main -withHeadings: true -navWeight: 10 -editable: true -githubSource: docgen/src/concepts.md ---- - -**InstantSearch iOS** is a declarative UI library providing widgets and helpers for building native, component-driven UIs with Algolia. -It is built on top of Algolia's [iOS API Client](https://github.com/algolia/algoliasearch-client-swift) and [iOS InstantSearch Core Client](https://github.com/algolia/instantsearch-core-swift) to provide you a high-level solution to quickly build various search interfaces. - - -In this guide, you will learn the key concepts of InstantSearch iOS. - - -## InstantSearch Core - -The InstantSearch library is built on top of InstantSearch Core. The essence of InstantSearch Core is the **Searcher**, which will wrap an [Algolia API Client](https://github.com/algolia/algoliasearch-client-swift/blob/master/Source/Client.swift) and provide a level of abstraction over it. You can think of InstantSearch Core as the UIKit agnostic library that takes care of everything related to the search session, while InstantSearch is a UIKit dependent library uses InstantSearch Core to offer ready-made configurable widgets that are "search aware" and automatically react to all kinds of search events. For more info about InstantSearchCore, checkout its [community website](https://community.algolia.com/instantsearch-core-swift/). - -## Widgets - -The core part of InstantSearch iOS are the widgets, which are search-aware UI components that are bound to search events coming from Algolia. We provide some universal widgets such as the [`SearchBar`][widgets-searchbar], the [`Hits`][widgets-hits] or the [`RefinementList`][widgets-refinementlist], and you can easily create new ones by implementing a few protocols (discussed below). - -Widgets inherit from the different UIKit `UIViews`, whether it is an advanced `UICollectionView`, or a simple `UISlider`. They are also customizable by exposing `IBInspectable` parameters that can be set right through Interface Builder. - -The nice thing about InstantSearch iOS is that you don't have to rewrite your existing `UIViews` to start using the library. In fact, the architecture of the library is mostly protocol-oriented, making it extendible and compatible with your existing UI. It follows Plugin Architecture conventions by providing most of the business logic mostly through protocols, and sometimes through base `UIViewController` classes. - -In that way, it is also very easy to create your own search-aware custom widgets. All it takes is implementing one or more protocols depending on the purpose of the widget, and then writing the business logic using the provided properties and methods coming from the protocols. - -## Protocols - -Let's talk about the 4 most important protocols used throughout InstantSearch iOS. This will help you understand how things work under the hood, and help you write your own custom widgets later on. But first, let's see the 2 types of widgets that we have. - -A widget can either be an **input widget** with which the user can interact with (e.g: searchBar), an **output widget** with which the user can see results (e.g: hits), or both (e.g: refinementList). In both cases, all widgets implement the `AlgoliaWidget` protocol, which is an empty marker protocol used to identify all Algolia related widgets in the `UIView's ViewController`. - -```swift -class SliderWidget: UISlider, AlgoliaWidget {} -``` - -An input widget will implement `SearchableViewModel`. By implementing this protocol, the widget will have a reference to `Searcher`, and will therefore be able to use it to trigger methods such as `#search()` (e.g: `UISearchBar`), or `#params.updateNumericRefinement` (e.g: `UISlider`). - -```swift -A implements SearchableViewModel -> searcher.search(query) -B implements SearchableViewModel -> searcher.params.updateNumericRefinement() -``` - -An output widget is basically a delegate that reacts to new "search events" that are triggered by the user interacting with an input widget, whether it is writing in a search bar or selecting a new filter. An output widget can implement one of the 2 following protocols: - -`ResultingDelegate` where the widget can process the results or handle the error of a search request to Algolia. - -```swift - ┌-> A implements ResultingDelegate -searcher.search(query) -> InstantSearch --┤ - └-> B implements ResultingDelegate -``` - -`RefinableDelegate` where the widget receives events when search parameters are being altered in the `Searcher`. - - - -```swift - ┌-> A implements RefinableDelegate -searcher.params.updateNumericRefinement() -> InstantSearch --┤ - └-> B implements RefinableDelegate -``` - - - - -## Add Widgets - -In order to add a widget to InstantSearch, there are 2 methods you can call. First you can call `InstantSearch.registerAllWidgets(in view: UIView)`, which will add all the `AlgoliaWidget` subviews found inside the `view` param. This is usually useful when your widgets are added to your ViewController, so all you have to do inside your ViewController is calling `InstantSearch.registerAllWidgets(in self.view)`. -If you want finer control over what you add, then you can use `InstantSearch.register(widget: AlgoliaWidget)`. - -## Query params - -If you're not using InstantSearch and using either the API client or InstantSearchCore, you're probably familiar with the `Query` or `SearchParameters` classes. These usually specify the query string and the parameters used for the search request to Algolia. - -With InstantSearch widgets, you don't have to worry about these classes anymore. In fact, the widgets will set all the parameters for you behind the scenes, without you having to worry about it. All what you will have to do on your side is specify a few properties linked to a specific widget, and you'll be good to go! - -However, sometimes, you will still want to modify and configure some functionalities available on the `Searcher` level but not on the `InstantSearch` level. In that case, `InstantSearch` still gives you access to a reference to its `Searcher` in order for you to do just that. - -## Configuring InstantSearch - -There are two ways you can configure and initialize InstantSearch in iOS. - -### Singleton - -You can use the singleton shared reference of InstantSearch throughout your app. For that, you can simply use the publicly available `InstantSearch.shared`. - -### Instance - -Another way to deal with InstantSearch is to just instantiate an `InstantSearch` instance with one of its constructors. Note that in this case, you will have to take the responsibility of passing that reference between different screens. - -[widgets-hits]: widgets.html#hits -[widgets-searchbar]: widgets.html#searchbar -[widgets-refinementlist]: widgets.html#refinementlist -[widgets-stats]: widgets.html#stats - -## ViewModels - -As we've seen, InstantSearch iOS provides widgets out of the box. Those are great when you want a default style to be applied and you do not need heavy customization of the behavior of the widget. - -Hence come the idea of ViewModels, which are used when you want to have full control on the rendering without having to reimplement business logic. As soon as you hit a feature wall using our default widgets, you can use ViewModels to have more flexibility. - -At the end of the day, the ViewModels encapsulate the logic for a specific search concept and provide a way to interact with InstantSearch. - -### SearchViewModel - -Use this to customize any kind of search box. - -#### Methods - -- `search(query:)` - -#### Example - -```swift -@IBOutlet weak var searchBar: SearchBarWidget! -var searchViewModel: SearchViewModel! - -override func viewDidLoad() { - super.viewDidLoad() - - searchViewModel = SearchViewModel(view: searchBar) - InstantSearch.shared.register(viewModel: searchViewModel) - - // Now can access access the searchBar's delegate - searchBar.delegate = self - } - - public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { - // Call the search function of the viewModel. It will take care of changing the - // search state and sending search events when new results arrive. - searchViewModel.search(query: searchText) - } -``` - -### HitsViewModel - -Use this to customize a hits view with only 1 index. - -#### Methods - -- `numberOfRows()` -- `hitForRow(at:)` - -#### Example - -```swift -@IBOutlet weak var tableView: HitsTableWidget! -var hitsViewModel: HitsViewModel! - -override func viewDidLoad() { - super.viewDidLoad() - - hitsViewModel = HitsViewModel(view: tableView) - InstantSearch.shared.register(viewModel: hitsViewModel) - - // Now can access access the tableView's delegate and datasource methods. - tableView.dataSource = self - tableView.delegate = self - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return hitsViewModel.numberOfRows() - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) - - let hit = hitsViewModel.hitForRow(at: indexPath) - cell.textLabel?.text = getTextOutOfHit(hit) - - return cell - } -``` - -### MultiHitsViewModel - -Use this to customize a hits view with only multiple indices. - -#### Methods - -- `numberOfRows(in:)` -- `hitForRow(at:)` -- `numberOfSections()` - -#### Example - -```swift -@IBOutlet weak var tableView: MultiHitsTableWidget! -var multiHitsViewModel: MultiHitsViewModel! - -override func viewDidLoad() { - super.viewDidLoad() - - multiHitsViewModel = MultiHitsViewModel(view: tableView) - InstantSearch.shared.register(viewModel: multiHitsViewModel) - - // Now can access access the tableView's delegate and datasource methods. - tableView.dataSource = self - tableView.delegate = self - } - - func numberOfSections(in tableView: UITableView) -> Int { - return multiHitsViewModel.numberOfSections() - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return multiHitsViewModel.numberOfRows(in: section) - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) - let hit = multiHitsViewModel.hitForRow(at: indexPath) - - if indexPath.section == 0 { // First index - cell.textLabel?.text = getTextOutOfHitWithFirstIndex(hit) - } else { // Second index - cell.textLabel?.text = getTextOutOfHitWithSecondIndex(hit) - } - - return cell - } -``` - -### NumericControlViewModel - -Use this to customize any kind of numeric control view. - -#### Methods - -- `updateNumeric(value:doSearch:)` -- `removeNumeric(value:)` - -### FacetControlViewModel - -Use this to customize any kind of facet control view. - -#### Methods - -- `addFacet(value:doSearch:)` -- `updatefacet(oldValue:newValue:doSearch:)` -- `removeFacet(value:)` - -### RefinementMenuViewModel - -Use this to customize any kind of refinement menu view. - -#### Methods - -- `numberOfRows()` -- `facetForRow(at:)` -- `isRefined(at:)` -- `didSelectRow(at:)` - diff --git a/docgen/src/custom-backend.md b/docgen/src/custom-backend.md deleted file mode 100755 index 3641f8ad..00000000 --- a/docgen/src/custom-backend.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: Use a Custom Backend -layout: main.pug -name: custom-backend -category: main -withHeadings: true -navWeight: 100 -editable: true -githubSource: docgen/src/custom-backend.md ---- - - -## Who should use this guide - -Advanced InstantSearch users may have the need to query Algolia’s servers from their backend instead of the frontend, while still being able to reuse InstantSearch widgets. Possible motivations could be for security restrictions, for SEO purposes or to enrich the data sent by the custom server (i.e. fetch Algolia data and data from their own servers). If this sounds appealing to you, feel free to follow this guide. Keep in mind though that we, at Algolia, recommend doing frontend search for performance and high availability reasons. - -By the end of this guide, you will have learned how to leverage InstantSearch with your own backend architecture to query Algolia. Even if you're not using Algolia on your backend and still want to benefit from using InstantSearch, then this guide is also for you. - -## A quick overview on how InstantSearch works - -InstantSearch, as you probably know, offers reactive UI widgets that automatically update when new search events occur. Internally, it uses a `Searchable` interface that takes care of making network calls to get search results. The most important method of that `Searchable` is a simple `search()` function that takes in a parameter that contains all the search query parameters, and then expects a callback to be called with the search results that you get from your backend. Let's see how this works in action - -## A basic implementation of using a custom backend - -The most basic implementation of using a custom backend uses the `DefaultSearchClient` and requires you to implement just one method: `search(query:searchResultsHandler:)`. In this function, you use the query passed to you, make a network request to your backend server, transform the response into a `SearchResults` instance, and then finally call the `searchResultsHandler` callback with the searchResults. In case of error, you call the callback with the error. Here is an example using the Alamofire networking library. - -``` swift -public class DefaultCustomBackend: DefaultSearchClient { - override public func search(_ query: Query, searchResultsHandler: @escaping SearchResultsHandler) { - // 1 - let queryText = query.query ?? "" - - // 2 - Alamofire.request("https://yourbackend.com/search?q=\(queryText)").responseJSON { responseJson in - - if let json = responseJson.result.value as? [String: Any] { - do { - // 3 - let searchResults = try SearchResults(content: json, disjunctiveFacets: []) - - // 4 - searchResultsHandler(searchResults, nil) - } catch let error { - // 4 - searchResultsHandler(nil, error) - } - } - } - } -} -``` - -This is the simplest example and will work only if on your backend, you're calling Algolia and then just forwarding your result to the mobile app without doing any modification to the json data. - -1- Get the query text from the Query parameter that is passed in the method. - -2- Make your request to your backend using the queryText parse in step 1. - -3- Serialise your response into a `SearchResults` instance. In case your response data is different than the original one returned by Algolia, especially in the case where you're not using Algolia at all in your backend, then you can use one of our initialiser of SearchResults such as `SearchResults(nbHits:hits)`. - -4- Call the `searchResultsHandler` function in order to instruct InstantSearch about the new search event, in this case the arrival of new search results, or an error. - -## A more advanced implementation of using a custom backend - -The above snippet only covers the case of doing a basic search of hits, with conjunctive (contrary to disjunctive) filtering. Here, we'll take a look at improving the structure of our custom backend class, as well as supporting disjunctive faceting. - -let's start with the code snippet - -```swift - -// 1 -public struct BackendSearchParameters { - var q: String? - var disjunctiveFacets: [String]? -} -public struct BackendSearchResults { - var total: Int - var hits: [[String: Any]] -} - -// 2 -public class BackendImplementation: SearchClient { - - // 3 - public override func map(query: Query) -> BackendSearchParameters { - let queryText = query.query - - return BackendSearchParameters(q: queryText, disjunctiveFacets: nil) - } - - // 4 - public override func map(query: Query, disjunctiveFacets: [String], refinements: [String : [String]]) -> BackendSearchParameters { - let queryText = query.query - - return BackendSearchParameters(q: queryText, disjunctiveFacets: disjunctiveFacets) - } - - // 5 - public override func map(results: BackendSearchResults) -> SearchResults { - let nbHits = results.total - let hits = results.hits - - // 6 - let categoryFacet = ["chairs": 10, "tables": 15] - let facets = ["category": categoryFacet] - let extraContent = ["facets": facets] - - return SearchResults(nbHits: nbHits, hits: hits, extraContent: extraContent) - } - - // 7 - public override func search(_ query: BackendSearchParameters, searchResultsHandler: @escaping SearchResultsHandler) { - - let queryText = query.q ?? "" - - Alamofire.request("https://yourbackend.com/search?q=\(queryText)").responseJSON { responseJson in - - if let json = responseJson.result.value as? [String: Any] { - do { - - let hitsJson = json["hits"] as! [String: Any] - let total = hitsJson["total"] as! Int - let hits = hitsJson["hits"] as! [[String: Any]] - - let backendSearchResults = BackendSearchResults(total: total, hits: hits) - - // 8 - searchResultsHandler(backendSearchResults, nil) - - } catch let error { - searchResultsHandler(nil, error) - } - } - } - - } -} -``` - -1- Create your models that will hold the query parameters and results that you need in order to make your custom backend call - -2- Create your class that inherits from `SearchClient`. Use your 2 models created in 1 for the generics of that class. This is will ensure strong typing and good practices throughout this implementation. - -3- Implement the basic param mapper function that converts a query to your parameter model. Make sure to take all the fields you need from the query parameter. - -4- Implement the advanced param mapper function. It is the same as 3, but with 2 more parameters that you can use for your call: `disjunctiveFacets` and `refinements`. - -5- Implement the result mapper function that converts your result model back to an Algolia `SearchResults` that can be understood by InstantSearch. - -6- In case you want to specify the possible facets for a refinement list, make sure to specify the facets property appropriately. In the code snippet, we just give an example, but usually you'll want to get this data from your custom result model. - -7- Implement the search method, same idea as the basic implementation. The only difference is that now it provides your custom parameter model as its parameter. - -8- When you get new search results, you serialise them into your custom response model and then call the `searchResultsHandler` method. - - -## Last trick to get more out of the query -One little trick you can use to get more detailed information about the `query` being passed as a parameter is to upcast it to a `SearchParameters` by doing - -``` -let searchParameters = query as! SearchParameters -``` - -In that way, you can access to higher level properties like `disjunctiveFacets`, `facetRefinements`, `disjunctiveNumerics` and `numericRefinements`. This can be useful when transforming Algolia's Query. - diff --git a/docgen/src/data/communityHeader.json b/docgen/src/data/communityHeader.json deleted file mode 100755 index b6b85458..00000000 --- a/docgen/src/data/communityHeader.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "menu": { - "project": { - "view": "links", - "label": "InstantSearch iOS", - "logo": "", - "caret": "", - "url":"https://community.algolia.com/instantsearch-ios", - "dropdownItems": [] - }, - "community": { - "dropdownItems": [ - { - "name": "InstantSearch.js", - "url": "https://community.algolia.com/instantsearch.js/v2", - "logo": "http://res.cloudinary.com/hilnmyskv/image/upload/v1500619122/instantsearch-icon_black.svg", - "backgroundColor": "#fecf50" - }, - { - "name": "React InstantSearch", - "url": "https://community.algolia.com/instantsearch.js/react", - "logo": "https://community.algolia.com/img/logo-react-instantsearch.svg", - "backgroundColor": "linear-gradient(45deg, #3369e7, #00aeff), linear-gradient(#fafafa, #fafafa)" - }, - { - "name": "Android InstantSearch", - "url": "https://community.algolia.com/instantsearch-android/", - "logo": "http://res.cloudinary.com/hilnmyskv/image/upload/v1500619122/instantsearch-icon_white.svg", - "backgroundColor": "linear-gradient(112deg, #21c7d0, #2dde98)" - }, - { - "name": "Helper.js", - "url": "https://community.algolia.com/algoliasearch-helper-js/", - "logo": "https://community.algolia.com/img/logo-helper.svg", - "backgroundColor": "#FDBD57" - }, - { - "name": "Wordpress", - "url": "https://community.algolia.com/wordpress/", - "logo": "https://community.algolia.com/wordpress/img/icons/wp-icon.svg", - "backgroundColor": "linear-gradient(to bottom right, #4041B2, #516ED1)" - }] - } - }, - "sideMenu": [{ - "name": "Getting Started", - "url": "https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/", - "dropdownItems": null - }, - { - "name": "Guides", - "url": "", - "image": "", - "dropdownItems": [ - { - "name": "Getting Started", - "url": "https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/" - }, - { - "name": "Getting Started Part 2", - "url": "https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/" - }, - { - "name": "Getting Started Programatically", - "url": "https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/" - }, - { - "name": "Use a Custom Backend", - "url": "https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/ios/" - } - ] - }, - { - "name": "Documentation", - "url": "", - "image": "", - "dropdownItems": [ - { - "name": "Concepts", - "url": "https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/" - }, - { - "name": "Widgets", - "url": "https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/" - }, - { - "name": "API Reference", - "url": "https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/" - }, - { - "name": "Troubleshooting", - "url": "https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/" - } - ] - },{ - "name": "Examples", - "url": "https://www.algolia.com/doc/guides/building-search-ui/resources/demos/ios/" - },{ - "name": "Github", - "image": "", - "url": "https://github.com/algolia/instantsearch-ios" - }], - "mobileMenu": [{ - "name": "Getting Started", - "url": "https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/" - }, - { - "name": "Concepts", - "url": "concepts.html" - }, - { - "name": "Widgets", - "url": "widgets.html" - }, - { - "name": "API Reference", - "url": "reference/" - }, - { - "name": "Troubleshooting", - "url": "troubleshooting.html" - }, - { - "name": "Examples", - "url": "examples.html" - }], - "docSearch": { - "input_selector": "searchbox", - "input_selector_placeholder": "Search the docs", - "input_mobile_selector": "mobile-searchbox" - } -} diff --git a/docgen/src/examples.md b/docgen/src/examples.md deleted file mode 100644 index 84c59b70..00000000 --- a/docgen/src/examples.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Examples -layout: examples.pug -name: examples -category: main -withHeadings: true -navWeight: 0 -editable: true -githubSource: docgen/src/examples.md ---- - -We made a demo application to give you an idea of what you can build with InstantSearch iOS: - -## E-commerce application - - - -This example imitates a product search interface like well-known e-commerce applications. - -- Search in the **product's name**, **type**, and **category** -- Filter with RefinementList by **type** or **category** -- Filter with Numeric filters by **price** or **rating** -- Custom views using `AlgoliaWidget` for filtering by **price** and **rating**. - -View source code on GitHub - -
-
-
-
-
-
-
- - - -## Tourism application - - -Example of a bed and breakfast search interface. - -- Search a place by **your location** around you -- Filter with Numberic filters by **radius** -- Filter with RefinementList by **room_type** -- Filter with Numeric filters by **price** -- Custom views using `AlgoliaWidget` for filtering by **price** and **room_type** -- Custom widgets for linking the search results with the `MKMapView` - -View source code on GitHub - -
-
-
-
-
-
-
-
- - -## Query Suggestions - - -Example of a query suggestion search interface. - -- Query suggestions appear when clicking on the search bar -- When clicking a query suggestion, the search bar is filled with that suggestion and results are refreshed -- Showing how you can use the ViewModels for customization of your widgets - -To learn how you can setup Query Suggestion on your Algolia application, [read this guide](https://www.algolia.com/doc/guides/analytics/query-suggestions/). - -View source code on GitHub - -
-
-
-
-
- -## Movies Demo - - -Example of a multi-index search interface. - -- Multi-Index table showcasing results from different indices (movies and actors) -- A load more button taking you to an infinite scrolling list -- Keep the state of the search when moving to the load more screen -- Uses the new iOS 11 SearchBar in NavigationBar. - -View source code on GitHub diff --git a/docgen/src/fonts/algolia-website-iconfont.eot b/docgen/src/fonts/algolia-website-iconfont.eot deleted file mode 100755 index e66378c3..00000000 Binary files a/docgen/src/fonts/algolia-website-iconfont.eot and /dev/null differ diff --git a/docgen/src/fonts/algolia-website-iconfont.svg b/docgen/src/fonts/algolia-website-iconfont.svg deleted file mode 100755 index 16a0bc9c..00000000 --- a/docgen/src/fonts/algolia-website-iconfont.svg +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - -{ - "fontFamily": "algolia-website-iconfont", - "majorVersion": 1, - "minorVersion": 1, - "version": "Version 1.1", - "fontId": "algolia-website-iconfont", - "psName": "algolia-website-iconfont", - "subFamily": "Regular", - "fullName": "algolia-website-iconfont", - "description": "Font generated by IcoMoon." -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/src/fonts/algolia-website-iconfont.ttf b/docgen/src/fonts/algolia-website-iconfont.ttf deleted file mode 100755 index 05463d50..00000000 Binary files a/docgen/src/fonts/algolia-website-iconfont.ttf and /dev/null differ diff --git a/docgen/src/fonts/algolia-website-iconfont.woff b/docgen/src/fonts/algolia-website-iconfont.woff deleted file mode 100755 index ae5c297f..00000000 Binary files a/docgen/src/fonts/algolia-website-iconfont.woff and /dev/null differ diff --git a/docgen/src/getting-started-part2.md b/docgen/src/getting-started-part2.md deleted file mode 100755 index 4213eec5..00000000 --- a/docgen/src/getting-started-part2.md +++ /dev/null @@ -1,231 +0,0 @@ ---- -title: Getting Started Part 2 -layout: main.pug -name: getting-started-part2 -category: main -withHeadings: true -navWeight: 100 -editable: true -githubSource: docgen/src/getting-started-part2.md ---- - -*This guide is a followup to the [first part of the getting started guide](https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/). Make sure to go through it first before moving to this one.* - -*This guide will walk you through how to properly highlight results, as well as filter results which is essential for a complete search experience.* - -## Setup - -Your starting project can be one of the following: - -- The project you used in the [getting started part 1](https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/) -- Download [this project](https://github.com/algolia/instantsearch-swift-examples/tree/master/Getting%20Started%20Part%202%20Start) as a starting point and run `pod update`. - -## Highlight your results - - - -Your current application lets the user search and displays results, but doesn't explain _why_ these results match the user's query. - -You can improve it by using the Highlighting feature: InstantSearchCore offers a helper method just for that. -- At the top of your file, add `import InstantSearchCore`. -- In your `tableView(_:cellForRowAt:)` method, remove the statement `cell.textLabel?.text = hit["name"] as? String` as we don't need it anymore, and add the following before the `return cell` statement: - - -```swift -cell.textLabel?.highlightedTextColor = .blue -cell.textLabel?.highlightedBackgroundColor = .yellow -cell.textLabel?.highlightedText = SearchResults.highlightResult(hit: hit, path: "name")?.value -``` - -Restart your application and type something in the SearchBar: the results are displayed with your keywords highlighted in these views! - -In this part, you've learned: - -- Highlighting search results. - ----- - -## Filter your results: RefinementList - - - -A refinementList lets the user refine the search results by choosing filters from a list of filters. In this section, we will build a refinementList to filter results based on the categories selected. **We will only go through the storyboard approach here**. - -### Setup New Screen and Navigation - -- Go to File -> New -> File, then select Cocoa Touch Class, and name it RefinementViewController, then create it. -- Let's first setup the navigation controller. In your `Main.storyboard` file, click on your `ViewController`, then in your menu bar at the top, go to Editor -> Embed in -> Navigation Controller. -- In your Utilities bar on your right, drag and drop a `View Controller` from the Object library to your storyboard. Click on it and then in the Identity Inspector, change its custom class to `RefinementViewController`. -- In your Object library again, drag and drop a `Bar Button Item` to the Navigation bar of the first ViewController. Double click on it to change its name to "Filter". -- From this button, hold on Ctrl, and drag it to the `RefinementViewConroller` view, and select `Show` as the Action Segue. - -Now that we have our navigation setup, let's add our refinementList as a `tableView`. - -* Drag and drop a Table View from the Object Library onto the `RefinementViewController` view, and change its custom class to be `RefinementTableWidget`. Go ahead and create a prototype cell for this table and specify `refinementCell` as the identifier. Also, change the `Style` of the cell in the identity inspector to `Subtitle`. -* Then, create an `IBOutlet` of that tableView into `RefinementViewController.swift`, and call it `tableView`. -* Finally, add the `import InstantSearch` statement at the top of `RefinementViewController.swift`. - -
- -
-
- -### The RefinementList - -We can implement a [RefinementList][widgets-refinementlist] with the exact same idea as the Hits widgets: using a base class and then implementing some delegate methods. However, this time, we will implement it using the helper class in order to show you how things can be done differently. That will help you use InstantSearch in the case where your `ViewController` already inherits from a subclass of `UIViewController`, and not `UIViewController` itself. Also, since you cannot subclass a Swift class in Objective-C, then this method will be useful if you decide to write your app in Objective-C. - -- First things first, go to `Main.Storyboard` and then select the `tableView` in the screen containing your `refinementList`. This will be your `refinementList`. Note that we already changed the class of the table to be a `RefinementTableWidget`. -- Now, go to the Attributes Inspector pane and then at the top, specify the `attribute` to be equal to `category`. This will associate the `refinementList` with the attribute `category`. -- Next, go to the `RefinementViewController.swift` class, and then add protocol `, RefinementTableViewDataSource` next to `UIViewController`. -- Add the following property below the declared `tableView`: - -```swift -var refinementController: RefinementController! -``` - -This `refinementController` will take care of the `dataSource` and `delegate` methods of the `tableView`, and will provide other advanced `dataSource` and `delegate` methods that contain information about the refinement. To achieve that, add the following in your `viewDidLoad` method: - -```swift -refinementController = RefinementController(table: tableView) -tableView.dataSource = refinementController -tableView.delegate = refinementController -refinementController.tableDataSource = self -// refinementController.tableDelegate = self - -InstantSearch.shared.register(widget: tableView) -``` -Remember at the end to always register the widget to `InstantSearch` so that it receives interesting search events from it. Finally, we need to add our dataSource method to specify the look and feel of the refinementList cell. - -```swift -func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, containing facet: String, with count: Int, is refined: Bool) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "refinementCell", for: indexPath) - - cell.textLabel?.text = facet - cell.detailTextLabel?.text = String(count) - cell.accessoryType = refined ? .checkmark : .none - - return cell -} -``` -We're done! **Build and run your application: you now have an InstantSearch iOS app that can filter data!**. Click on Filter and select refinements then go back. You will see that the stats and hits widget automatically update with the new data. Sweet! You can also go to your `Main.storyboard` and play around with the custom parameters of each widgets which are available in the attributes inspector. - -In this part, you've learned: - -- How to add the `RefinementList` widget. -- How to configure the widget. -- How to specify the look and feel of your refinement cells. - -## Display data from multiple hits - -It is probable that your app is targeting multiple indices: for example, you want to search through bestbuy items, as well as IKEA items. Let's see how this is possible. - -First make sure that you're using the latest version of InstantSearch (after 2.1.0). - -### Code - -In order to specify to InstantSearch which index you want to target, go to your `AppDelegate` and then inside your `application(_:didFinishLaunchingWithOptions:)` method, replace what is in it with: - -```swift -let searcherIds = [SearcherId(index: "bestbuy_promo"), SearcherId(index: "ikea")] -InstantSearch.shared.configure(appID: ALGOLIA_APP_ID, apiKey: ALGOLIA_API_KEY, searcherIds: searcherIds) -``` - -In that way, InstantSearch is aware of the indices that it has to deal with. Next, go to your `ViewController` and replace all of it with the following: - -```swift -import UIKit -import InstantSearch -import InstantSearchCore - -class HitsViewController: MultiHitsTableViewController { - - @IBOutlet weak var tableView: MultiHitsTableWidget! - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - - hitsTableView = tableView - - InstantSearch.shared.registerAllWidgets(in: self.view) - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, containing hit: [String : Any]) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "hitCell", for: indexPath) - - if indexPath.section == 0 { // bestbuy - cell.textLabel?.highlightedTextColor = .blue - cell.textLabel?.highlightedBackgroundColor = .yellow - cell.textLabel?.highlightedText = SearchResults.highlightResult(hit: hit, path: "name")?.value - } else { // ikea - cell.textLabel?.highlightedTextColor = .white - cell.textLabel?.highlightedBackgroundColor = .black - cell.textLabel?.highlightedText = SearchResults.highlightResult(hit: hit, path: "name")?.value - } - - return cell - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - - let label = UILabel(frame: CGRect(x: 0, y: 0, width: 300, height: 30)) - - let view = UIView() - view.addSubview(label) - view.backgroundColor = UIColor.gray - return view - } -} -``` - -Things to note: - -First, from the previous single-index implementation, we changed some classes from `HitsXYZ` to `MultiHitsXYZ`. These are `MultiHitsTableViewController` and `MultiHitsTableWidget`, -Nothing changed in the `ViewDidLoad`, and in the `cellForRowAt` method, we now have to deal with 2 sections since we'll have one for bestbuy, and one for ikea. Finally, we specify a `viewForHeaderInSection` to put a separator between these 2 sections. - -### Storyboard - -Now, head into the `Main.Storyboard`, select your stat widget, and then in the property inspector, add `ikea` to the `index` field. This specifies that the stats label shows number of results for ikea products. Next, click on the tableView which show the hits, and then in the identity inspector, change the class to `MultiHitsTableWidget`. Then, head to the identity inspector and specify the following: - -- Indices: `bestbuy_promo,ikea` -- Hits Per Section: `5,10` - -Here, we are saying that we want to show the `bestbuy_promo` index in the first section and the `ikea` index in the second. -We also specify that we want 5 hits to appear for the `bestbuy_promo` index and 10 for the `ikea` index. - -Great, now **run your app**, search in the search bar and you should see results appearing from the indices! - -There's still one more thing: If you click on the filter button, **the app will crash**. Why is that? This is because we are in "multi-index" mode and the refinement list doesn't have a clue what index to target: is it bestbuy_promo or ikea? - -In order to specify this, go to your `main.storyboard` class, then click on the refinement list. In the attribute inspector, specify `bestbuy_promo` as the index. - -Now go ahead and **run your app again**. When going to the filters screen and selecting a filter, you'll notice in the main screen that the refinements are only being applied to the `bestbuy_promo` index. - -In this part, you've learned: - -- How to configure InstantSearch for Multi-indexing. -- How to use the `MultiHitsTableWidget` which can show different indices. -- How to specify an index for widgets such as the `StatsLabelWidget` and the `RefinementTableWidget`. - -### Notes - -- You might have seen `Variant` or `Variants` attributes in the identity inspector (variants example: `main,details`). These are used in case you want 2 widgets to use the same index but with different configurations. You also have to specify those variants when configuring InstantSearch using the `SearcherId(index:variant)` constructor instead of `SearcherId(index:)`. -- Concerning the `SearchBarWidet`: by not specifying any index there, InstantSearch will conclude that the Search bar should search in all index. If an index was specified, then the `SearchBarWidget` would only trigger a search in that particular widget. - -## Go further - -Your application now displays your data from multiple indices, lets your users enter a query, displays search results as-they-type and lets users filter by refinements: you just built a full instant-search interface! Congratulations 🎉 - -This is only an introduction to what you can do with InstantSearch iOS: -- Have a look at our [examples][examples] to see more complex examples of applications built with InstantSearch. -- You can also head to our [Widgets page][widgets] to see the other components that you could use. -- You can check how to customise further your widgets and view controllers with [ViewModels](viewmodels). - -[algolia_sign_up]: https://www.algolia.com/users/sign_up -[widgets]: widgets.html -[examples]: examples.html -[widgets-hits]: widgets.html#hits -[widgets-searchbox]: widgets.html#searchbar -[widgets-refinementlist]: widgets.html#refinementlist -[widgets-stats]: widgets.html#stats -[viewmodels]: concepts.html#viewmodels \ No newline at end of file diff --git a/docgen/src/getting-started-programmatic.md b/docgen/src/getting-started-programmatic.md deleted file mode 100755 index 294c55ba..00000000 --- a/docgen/src/getting-started-programmatic.md +++ /dev/null @@ -1,211 +0,0 @@ ---- -title: Getting Started Programmatically -layout: main.pug -name: getting-started-programmatic -category: main -withHeadings: true -navWeight: 100 -editable: true -githubSource: docgen/src/getting-started-programmatic.md ---- - -*This guide will walk you through the few steps needed to start a project with InstantSearch iOS. -We will start from an empty iOS project, and create from scratch a full search interface!* - -*Another thing to point out is that this getting started guide will show you how to build your search experience programmatically. However, if you prefer to write your UI using storyboards and xibs, you can follow the [getting started guide with storyboard](https://www.algolia.com/doc/guides/building-search-ui/getting-started/ios/).* - -## Before we start -To use InstantSearch iOS, you need an Algolia account. You can create one by clicking [here][algolia_sign_up], or use the following credentials: -- APP ID: `latency` -- Search API Key: `3d9875e51fbd20c7754e65422f7ce5e1` -- Index name: `bestbuy_promo` - -*These credentials will let you use a preloaded dataset of products appropriate for this guide.* - -## Create a new project -Let's get started! In Xcode, create a new Project: - -- On the Template screen, select **Single View Application** and click next -- Specify your Product name, select Swift as the language, and iPhone as the Device. Then create. - -
- -
-
- -We will use CocoaPods for adding the dependency to `InstantSearch`. - -- If you don't have CocoaPods installed on your machine, open your terminal and run `sudo gem install cocoapods`. -- Go to the root of your project then type `pod init`. A `Podfile` will be created for you. -- Open your `Podfile` and add `pod 'InstantSearch'` below your target. -- On your terminal, run `pod update`. -- Close you Xcode project and then at the root of your project, open `projectName.xcworkspace`. - -## Initialize InstantSearch - -To initialize InstantSearch, you need an Algolia account with a configured non-empty index. - -Go to your `AppDelegate.swift` file and then add `import InstantSearch` at the top. Then inside your `application(_:didFinishLaunchingWithOptions:)` method, add the following: - -```swift -InstantSearch.shared.configure(appID: "latency", apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db", index: "bestbuy_promo") -InstantSearch.shared.params.attributesToRetrieve = ["name", "salePrice"] -InstantSearch.shared.params.attributesToHighlight = ["name"] -``` - -This will initialize InstantSearch with the credentials proposed at the beginning. You can also chose to replace them with the credentials of your own app. - -To understand the above, we are using the singleton `InstantSearch.shared` to configure InstantSearch with our Algolia credentials. `InstantSearch.shared` will be used throughout our app to easily deal with InstantSearch. You could also have created your own instance of `InstantSearch` and passed it around your controllers, but we won't do that in this guide. - -Next, we added the attributes that we want to retrieve and highlight. As a side note, some search parameters can be defaulted in the Algolia dashboard by going to Indices -> Display tab. If you add the configuration there, then you do not need to specify the `attributesToRetrieve` and `attributesToHighlight` as shown above. - -## Search your data: SearchBar - - - -InstantSearch iOS is based on a system of [widgets][widgets] that communicate when a user interacts with your app. The first widget we'll add is a [SearchBar][widgets-searchbox] since any search experience requires one. InstantSearch will automatically recognize your SearchBar as a source of search queries. We will also add a `Stats` widget to show how the number of results change when you type a query in your SearchBar. - -- Go to your `ViewController.swift` file and then add `import InstantSearch` at the top. -- Below your class definition, declare your [SearchBar][widgets-searchbox] and [Stats][widgets-stats] widget: - -```swift -// Create your widgets -let searchBar = SearchBarWidget(frame: .zero) -let stats = StatsLabelWidget(frame: .zero) -``` - -- Then, inside your `viewDidLoad` method, add the following: - -```swift -initUI() - -// Add all widgets to InstantSearch -InstantSearch.shared.registerAllWidgets(in: self.view) -``` - -- Finally, we need to add the views to the `ViewController`'s view and specify the autolayout constraints so that the layout looks good on any device. You don't have to focus too much on understanding this part since it is not related to InstantSearch, and more related to iOS layout. Add this function to your file: - -```swift -func initUI() { - // Add the declared views to the main view - self.view.addSubview(searchBar) - self.view.addSubview(stats) - - // Add autolayout constraints - searchBar.translatesAutoresizingMaskIntoConstraints = false - stats.translatesAutoresizingMaskIntoConstraints = false - - let views = ["searchBar": searchBar, "stats": stats] - var constraints = [NSLayoutConstraint]() - constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-30-[searchBar]-10-[stats]", options: [], metrics: nil, views:views) - constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-25-[searchBar]-25-|", options: [], metrics: nil, views:views) - constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-25-[stats]-25-|", options: [], metrics: nil, views:views) - NSLayoutConstraint.activate(constraints) - - // Style the stats label - stats.textAlignment = .center - stats.font = UIFont.boldSystemFont(ofSize:18.0) -} -``` - -**Build and run your application: you now have the most basic search experience!** You should see that the results are changing on each key stroke. Fantastic! - -### Recap - -You just used your very first widgets from InstantSearch. In this part, you've learned: - -- How to create a SearchBar Widget. -- How to create a StatsLabel Widget. -- How to register widgets to InstantSearch. - -## Display your data: Hits - - - -The whole point of a search experience is to display the dataset that matches best the query entered by the user. That's what we will implement in this section with the [hits][widgets-hits] widget. - -- Let's go ahead and create our tableView. Next to your properties declared, add the following: - -```swift -let tableView = HitsTableWidget(frame: .zero) -``` - -- Then we will specify the layout constraint of the table to fill the most of the page. **Replace** your `initUI` method with the following (again, no need to worry about understanding this part): - -```swift -func initUI() { - // Add the declared views to the main view - self.view.addSubview(searchBar) - self.view.addSubview(stats) - self.view.addSubview(tableView) - - // Add autolayout constraints - searchBar.translatesAutoresizingMaskIntoConstraints = false - stats.translatesAutoresizingMaskIntoConstraints = false - tableView.translatesAutoresizingMaskIntoConstraints = false - - let views = ["searchBar": searchBar, "stats": stats, "tableView": tableView] - var constraints = [NSLayoutConstraint]() - constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-30-[searchBar]-10-[stats]-10-[tableView]-|", options: [], metrics: nil, views:views) - constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-25-[searchBar]-25-|", options: [], metrics: nil, views:views) - constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-25-[stats]-25-|", options: [], metrics: nil, views:views) - constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[tableView]-|", options: [], metrics: nil, views:views) - NSLayoutConstraint.activate(constraints) - - // Register tableView identifier - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "hitCell") - - // Style the stats label - stats.textAlignment = .center - stats.font = UIFont.boldSystemFont(ofSize:18.0) -} -``` - -Now that we have our `Table View` setup, we still need to specify what fields from the Algolia response we want to show, as well as the layout of our cells. InstantSearch provides both base classes and helper classes in order to achieve this. Here, we will look at the easiest and most flexible way: using the base class. - -- In your `ViewController` class, replace `UIViewController` with `HitsTableViewController`. This class will help you setup a lot of boilerplate code for you. -- Next, in your `viewDidLoad` method, before registering your widgets to InstantSearch, add the following: - -```swift -hitsTableView = tableView -``` - -This will associate the `hitsTableView` in the base class to the tableView that you just created. Behind the scenes, your `ViewController` class will become the `delegate` and `dataSource` of the tableView, the same way the UIKit base class `UITableViewController` does that for you. - -Next, we can specify our cells with a method provided by the base class, which contains the hit for the specific row. - -```swift -override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, containing hit: [String : Any]) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "hitCell", for: indexPath) - - cell.textLabel?.text = hit["name"] as? String - - return cell -} -``` - -Here we use the json hit, extract the `name` of the product, and assign it to the `text` property of the cell's `textLabel`. - -**Build and run your application: you now have an InstantSearch iOS app displaying your data!** You can also enjoy the infinite scrolling of the table as well if you set it to true! - -In this part, you've learned: - -- How to build your interface with Widgets by adding the `Hits` widget. -- How to configure widgets. -- How to specify the look and feel of your hit cells. - -## Go further - -Your application now displays your data, lets your users enter a query and displays search results as-they-type. That is pretty nice already! However, we can go further and improve on that. - -- In the [getting started part 2](getting-started-part2.html), you will learn about properly highlighting results, as well as filtering results which is essential for a complete search experience. Note that part 2 will use the storyboard for simplicity. -- You can also have a look at our [examples][examples] to see more complex examples of applications built with InstantSearch. -- Finally, You can head to our [widgets page][widgets] to see other components that you could use. - -[algolia_sign_up]: https://www.algolia.com/users/sign_up -[widgets]: widgets.html -[examples]: examples.html -[widgets-hits]: widgets.html#hits -[widgets-searchbox]: widgets.html#searchbar -[widgets-refinementlist]: widgets.html#refinementlist -[widgets-stats]: widgets.html#stats \ No newline at end of file diff --git a/docgen/src/getting-started.md b/docgen/src/getting-started.md deleted file mode 100755 index a02b0d08..00000000 --- a/docgen/src/getting-started.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: Getting Started -layout: main.pug -name: getting-started -category: main -withHeadings: true -navWeight: 100 -editable: true -githubSource: docgen/src/getting-started.md ---- - -*This guide will walk you through the few steps needed to start a project with InstantSearch iOS. -We will start from an empty iOS project, and then create from scratch a full search interface!* - -*Another thing to point out is that this getting started guide will show you how to build your search experience using storyboards (or xibs). However, if you prefer to write your UI programatically, you can follow the [programmatic getting started guide](getting-started-programmatic.html).* - -## Before we start -To use InstantSearch iOS, you need an Algolia account. You can create one by clicking [here][algolia_sign_up], or use the following credentials: -- APP ID: `latency` -- Search API Key: `1f6fd3a6fb973cb08419fe7d288fa4db` -- Index name: `bestbuy_promo` - -*These credentials will let you use a preloaded dataset of products appropriate for this guide.* - -## Create a new project -Let's get started! In Xcode, create a new Project: - -- On the Template screen, select **Single View Application** and click next -- Specify your Product name, select Swift as the language, and iPhone as the Device. Then create. - -
- -
-
- -We will use CocoaPods for adding the dependency to `InstantSearch`. - -- If you don't have CocoaPods installed on your machine, open your terminal and run `sudo gem install cocoapods`. -- Go to the root of your project then type `pod init`. A `Podfile` will be created for you. -- Open your `Podfile` and add `pod 'InstantSearch'` below your target. -- On your terminal, run `pod update`. -- Close you Xcode project and then at the root of your project, open `projectName.xcworkspace`. - -## Initialize InstantSearch - -To initialize InstantSearch, you need an Algolia account with a configured non-empty index. - -Go to your `AppDelegate.swift` file and then add `import InstantSearch` at the top. Then inside your `application(_:didFinishLaunchingWithOptions:)` method, add the following: - -```swift -InstantSearch.shared.configure(appID: "latency", apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db", index: "bestbuy_promo") -InstantSearch.shared.params.attributesToRetrieve = ["name", "salePrice"] -InstantSearch.shared.params.attributesToHighlight = ["name"] -``` - -This will initialize InstantSearch with the credentials proposed at the beginning. You can also chose to replace them with the credentials of your own app. - -To understand the above, we are using the singleton `InstantSearch.shared` to configure InstantSearch with our Algolia credentials. `InstantSearch.shared` will be used throughout our app to easily deal with InstantSearch. You could also have created your own instance of `InstantSearch` and passed it around your controllers, but we won't do that in this guide. - -Next, we added the attributes that we want to retrieve and highlight. As a side note, some search parameters can be defaulted in the Algolia dashboard by going to Indices -> Display tab. If you add the configuration there, then you do not need to specify the `attributesToRetrieve` and `attributesToHighlight` as shown above. - -## Search your data: SearchBar - - - -InstantSearch iOS is based on a system of [widgets][widgets] that constitute your search interface. The first widget we'll add is a [SearchBar][widgets-searchbox] since any search experience requires one. InstantSearch will automatically recognize your SearchBar as a source of search queries. We will also add a `Stats` widget to show how the number of results change when you type a query in your SearchBar. - -- Start by going to your `ViewController.swift` file and then add `import InstantSearch` at the top. -- Then inside your `viewDidLoad` method, add the following: - -```swift -// Register all widgets in view to InstantSearch -InstantSearch.shared.registerAllWidgets(in: self.view) -``` - -Here, we're telling InstantSearch to inspect all the subviews in the `ViewController`'s view. So what we need to do now is add the widgets to our view! - -- Open your `Main.storyboard` file, and on the Utility Tab on your right, go to the Object Library at the bottom. -- Drag and drop a `Search Bar` to your view. Click on it and then on the identity inspector, add the custom class `SearchBarWidget`. -- Now repeat the process but this time add a `Label` to the view, and then let the custom class be a `StatsLabelWidget`. -- Finally, make the width of the label bigger so that the text can clearly appear. - -
- -
-
- -**Build and run your application: you now have the most basic search experience!** You should see that the results are changing on each key stroke. Fantastic! - -### Recap - -You just used your very first widgets from InstantSearch. In this part, you've learned: - -- How to create a SearchBar Widget. -- How to create a StatsLabel Widget. -- How to register widgets to InstantSearch. - -## Display your data: Hits - - - -The whole point of a search experience is to display the dataset that matches best the query entered by the user. That's what we will implement in this section with the [hits][widgets-hits] widget. - -- In your `Main.Storyboard`, drag and drop a `Table View` from the Object Library and resize it to make it bigger. -- Select the `Table View` and change its custom class to `HitsTableWidget`. - - - -> *If you go to the attributes inspector, you will see that at the top, there are 4 configuration parameters that you can change, like `Hits Pet Page` and `Infinite Scrolling`. Feel free to change them to your needs, or keep the default values.* - -

- -

- -- Click on your `Table View`, and in the attributes inspector, add a prototype cell under the `Table View` section by replacing `0` with `1`. -- You should now be able to see a `Table View Cell` (under your `Table View`) in the Document Outline on the left of the storyboard file. Select that, and in the attributes inspector, specify `hitCell` as the identifier. -- Finally, we need to have a reference to the `HitsTableView` in your `ViewController`. For that, go ahead and create an `IBOutlet` and call it `tableView`. - -
- -
-
- -Now that we have our `Table View` setup, we still need to specify what fields from the Algolia response we want to show, as well as the layout of our cells. InstantSearch provides both base classes and helper classes in order to achieve this. Here, we will look at the easiest and most flexible way: using the base class `HitsTableViewController`. - -- In your `ViewController` class, replace `UIViewController` with `HitsTableViewController`. This class will help you setup a lot of boilerplate code for you. -- Next, in your `viewDidLoad` method, before registering your widgets to InstantSearch, add the following: - -```swift -hitsTableView = tableView -``` - -This will associate the `hitsTableView` in the base class to the `tableView` that we just created. Behind the scenes, your `ViewController` class will become the `delegate` and `dataSource` of the tableView, the same way the UIKit base class `UITableViewController` does that for you. - -Next, we can specify our cells with a method provided by the base class, which contains the hit for the specific row. - -```swift -override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, containing hit: [String : Any]) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "hitCell", for: indexPath) - - cell.textLabel?.text = hit["name"] as? String - - return cell -} -``` - -Here we use the json hit, extract the `name` of the product, and assign it to the `text` property of the cell's `textLabel`. - -**Build and run your application: you now have an InstantSearch iOS app displaying your data!** You can also enjoy the infinite scrolling of the table as well if you set it to true! - -In this part, you've learned: - -- How to build your interface with widgets by adding the `Hits` widget. -- How to configure widgets. -- How to specify the look and feel of your hit cells. - -## Go Further - -Your application now displays your data, lets your users enter a query and displays search results as-they-type. That is pretty nice already! However, we can go further and improve on that. - -- In the [getting started part 2](getting-started-part2.html), you will learn about properly highlighting results, as well as filtering results which is essential for a complete search experience. Finally, you will see how you can target multiple indices. -- You can also have a look at our [examples][examples] to see more complex examples of applications built with InstantSearch. -- Finally, You can head to our [widgets page][widgets] to see other components that you could use. - -[algolia_sign_up]: https://www.algolia.com/users/sign_up -[widgets]: widgets.html -[examples]: examples.html -[widgets-hits]: widgets.html#hits -[widgets-searchbox]: widgets.html#searchbar -[widgets-refinementlist]: widgets.html#refinementlist -[widgets-stats]: widgets.html#stats \ No newline at end of file diff --git a/docgen/src/images/algolia-fast-bg.svg b/docgen/src/images/algolia-fast-bg.svg deleted file mode 100755 index 55ace050..00000000 --- a/docgen/src/images/algolia-fast-bg.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - Slice - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/src/images/android-screen.svg b/docgen/src/images/android-screen.svg deleted file mode 100755 index 4ae52845..00000000 --- a/docgen/src/images/android-screen.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - Group 2 - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/src/images/android-tile.svg b/docgen/src/images/android-tile.svg deleted file mode 100755 index bb475456..00000000 --- a/docgen/src/images/android-tile.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - Artboard Copy - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/src/images/android-tile_dark.svg b/docgen/src/images/android-tile_dark.svg deleted file mode 100755 index e5f9b187..00000000 --- a/docgen/src/images/android-tile_dark.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - android-tile - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/src/images/arrow-list-icon.svg b/docgen/src/images/arrow-list-icon.svg deleted file mode 100755 index 3bd759fa..00000000 --- a/docgen/src/images/arrow-list-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - Path - Created with Sketch. - - - - - \ No newline at end of file diff --git a/docgen/src/images/background-hero.svg b/docgen/src/images/background-hero.svg deleted file mode 100644 index 46ca56e7..00000000 --- a/docgen/src/images/background-hero.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docgen/src/images/e-commerce.svg b/docgen/src/images/e-commerce.svg deleted file mode 100755 index 9b21dabd..00000000 --- a/docgen/src/images/e-commerce.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - e-commerce - Created with Sketch. - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/src/images/facets-illus.svg b/docgen/src/images/facets-illus.svg deleted file mode 100755 index d7fbbb7c..00000000 --- a/docgen/src/images/facets-illus.svg +++ /dev/null @@ -1,197 +0,0 @@ - - - - facets-illus - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Deep linking & search in iOS 9 will change ev.. - - - The holy grail for any investor is to discover the next G... - - - Next-level searching in Slack - - - Day to day productivity is one thing. Having a long-ter... - - - How Google Is Taking Search Outside the Box - - - The I/O conference had virtual reality, photos and elec... - - - A painstakingly crafted search for Hearthsto.. - - - Hearthstone’s popularity is stunning. Is it the worthy... - - - A painstakingly crafted search for Hearthsto.. - - - Hearthstone’s popularity is stunning. Is it the worthy... - - - - - - - - - - - TYPE - - - Article - - - - - - - - - - - - - - - - - - - - - - - - Video - - - Categorie - - - 236 - - - 48 - - - 12 - - - - - TAGS - - - - - Startup - - - - - - Entrepreneurship - - - - - - Search - - - - - - Design - - - - - - Marketing - - - - - - - - - \ No newline at end of file diff --git a/docgen/src/images/github-icon.svg b/docgen/src/images/github-icon.svg deleted file mode 100755 index b9ade5d8..00000000 --- a/docgen/src/images/github-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docgen/src/images/light-speed.svg b/docgen/src/images/light-speed.svg deleted file mode 100644 index 2b6323fc..00000000 --- a/docgen/src/images/light-speed.svg +++ /dev/null @@ -1 +0,0 @@ -Rectangle-path \ No newline at end of file diff --git a/docgen/src/images/lines-code.png b/docgen/src/images/lines-code.png deleted file mode 100755 index 55ecac4a..00000000 Binary files a/docgen/src/images/lines-code.png and /dev/null differ diff --git a/docgen/src/images/logo_isjs.svg b/docgen/src/images/logo_isjs.svg deleted file mode 100644 index 99fc46eb..00000000 --- a/docgen/src/images/logo_isjs.svg +++ /dev/null @@ -1 +0,0 @@ -logo \ No newline at end of file diff --git a/docgen/src/images/media.svg b/docgen/src/images/media.svg deleted file mode 100755 index 3fd42e68..00000000 --- a/docgen/src/images/media.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - media - Created with Sketch. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docgen/src/images/more-icon.svg b/docgen/src/images/more-icon.svg deleted file mode 100755 index 244a4b18..00000000 --- a/docgen/src/images/more-icon.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - more-icon - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/docgen/src/index.md b/docgen/src/index.md deleted file mode 100755 index e0733612..00000000 --- a/docgen/src/index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: InstantSearch iOS -layout: index.pug ---- - diff --git a/docgen/src/stylesheets/components/_buttons.sass b/docgen/src/stylesheets/components/_buttons.sass deleted file mode 100644 index 90d2cd0d..00000000 --- a/docgen/src/stylesheets/components/_buttons.sass +++ /dev/null @@ -1,178 +0,0 @@ -.btn - padding: 0 20px - display: inline-block - border-radius: 50px - font-size: 16px - font-weight: 400 - height: 40px - line-height: 40px - border: none - box-sizing: border-box - position: relative - transition: background .2s, box-shadow .2s - &:hover, &:focus, &:active, &.active - outline: 0 !important - box-shadow: none - text-decoration: none - @media (max-width: $screen-sm) - font-size: 14px - - .icon:before - position: relative - top: 2px - - -.btn-static-default - border: 2px solid $tertiary-color - color: $text-color - background-color: transparent - line-height: 36px - &:hover, &:focus - color: $text-color - background-color: rgba($tertiary-color,.3) - - -.btn-static-primary - border: none - color: #fff - background-image: linear-gradient(284deg, $java, $shamrock) - &:hover, &:focus - color: white - background-image: linear-gradient(284deg, $java, $shamrock) - &:active, &.active - background-image: linear-gradient(284deg, darken($java, 4%), darken($shamrock, 4%)) - - &.btn-shadow - box-shadow: 0 2px 6px 0 rgba($shamrock, 0.4) - &:hover, &:focus - box-shadow: 0 4px 12px rgba($shamrock, 0.4) - - -.btn-static-secondary - border: none - color: #fff - background-image: linear-gradient(80deg, $algolia-blue, $royal-blue) - &:hover, &:focus - color: white - background-image: linear-gradient(80deg, $algolia-blue, $royal-blue) - &:active, &.active - background-image: linear-gradient(80deg, darken($algolia-blue, 4%), darken($royal-blue, 4%)) - &.btn-shadow - box-shadow: 0 2px 6px 0 rgba($royal-blue, 0.4) - &:hover, &:focus - box-shadow: 0 4px 12px rgba($royal-blue, 0.4) - - - -.btn-static-tertiary - border: none - color: #fff - background-image: linear-gradient(112deg, $radical-red, $bittersweet) - &:hover, &:focus - color: white - background-image: linear-gradient(112deg, $radical-red, $bittersweet) - &:active, &.active - background-image: linear-gradient(112deg, darken($radical-red, 4%), darken($bittersweet, 4%)) - &.btn-shadow - box-shadow: 0 2px 6px 0 rgba($bittersweet, 0.4) - &:hover, &:focus - box-shadow: 0 4px 12px rgba($bittersweet, 0.4) - -.btn-static-theme - border: none - color: #fff - background-image: linear-gradient(to right, #211e39, #6a7599) - &:hover, &:focus - color: #fff - background-color: darken($theme-color, 10%) - &:active, &.active - background-image: darken($theme-color, 10%) - &.btn-shadow - box-shadow: 0 2px 6px 0 rgba($koromiko, 0.4) - &:hover, &:focus - box-shadow: 0 4px 12px rgba($koromiko, 0.4) - - svg - width: 16px - height: 16px - vertical-align: middle - margin-left: 0.5em - - use - fill: #fff - - - -.btn-static-inverse - color: white - &:hover, &:focus, &:active, &.active - color: white - - - -.btn-static-dark - color: $white - background-color: $deep-cove - background-image: linear-gradient(283deg, $deep-cove, $bunting) - border-color: $deep-cove - &:hover, &:focus - color: $white - background-color: $bunting - border-color: $bunting - &.btn-shadow - box-shadow: 0 2px 6px 0 rgba($bunting, 0.4) - &:hover, &:focus - box-shadow: 0 4px 12px rgba($bunting, 0.4) - -.btn-static-white - color: $bunting - background-color: $white - border-color: $white - &:hover, &:focus - color: $bunting - - -.btn-static-enterprise - color: $white - background-color: $deep-cove - border: solid 2px $shamrock - line-height: 36px - - &:hover, &:focus - color: white - background-color: $shamrock - border-color: $shamrock - - -// Circles -.btn-circle - width: 42px - height: 42px - padding: 0 - line-height: 42px - - - -// Shadows -.btn-static-shadow-dark - box-shadow: 0 2px 6px 0 rgba($deep-cove, 0.5) - &:hover, &:focus - box-shadow: 0 4px 12px rgba($deep-cove, 0.5) - - -.btn-static-default, -.btn-static-primary, -.btn-static-secondary, -.btn-static-tertiary, -.btn-static-inverse, -.btn-static-dark, -.btn-static-enterprise - &:active, &.active, - &.btn-shadow:active, .btn-shadow.active, - &.btn-static-shadow-dark:active, .btn-static-shadow-dark.active - box-shadow: inset 0 0 4px 2px rgba(#050f2c, 0.3) - - -// Sizes -.btn-lg - min-width: 210px diff --git a/docgen/src/stylesheets/components/_cards.sass b/docgen/src/stylesheets/components/_cards.sass deleted file mode 100644 index e69de29b..00000000 diff --git a/docgen/src/stylesheets/components/_code-highlighting.scss b/docgen/src/stylesheets/components/_code-highlighting.scss deleted file mode 100644 index e3ffdd71..00000000 --- a/docgen/src/stylesheets/components/_code-highlighting.scss +++ /dev/null @@ -1,44 +0,0 @@ -/* - MDN-LIKE Theme - Mozilla - Ported to CodeMirror by Peter Kroon - Report bugs/issues here: https://github.com/codemirror/CodeMirror/issues - GitHub: @peterkroon - - The mdn-like theme is inspired on the displayed code examples at: https://developer.mozilla.org/en-US/docs/Web/CSS/animation - -*/ -.cm-s-mdn-like.CodeMirror { color: #999; background-color: #fff; } -.cm-s-mdn-like div.CodeMirror-selected { background: #cfc; } -.cm-s-mdn-like .CodeMirror-line::selection, .cm-s-mdn-like .CodeMirror-line > span::selection, .cm-s-mdn-like .CodeMirror-line > span > span::selection { background: #cfc; } -.cm-s-mdn-like .CodeMirror-line::-moz-selection, .cm-s-mdn-like .CodeMirror-line > span::-moz-selection, .cm-s-mdn-like .CodeMirror-line > span > span::-moz-selection { background: #cfc; } - -.cm-s-mdn-like .CodeMirror-gutters { background: #f8f8f8; border-left: 6px solid rgba(0,83,159,0.65); color: #333; } -.cm-s-mdn-like .CodeMirror-linenumber { color: #aaa; padding-left: 8px; } -.cm-s-mdn-like .CodeMirror-cursor { border-left: 2px solid #222; } - -.cm-s-mdn-like .cm-keyword { color: #6262FF; } -.cm-s-mdn-like .cm-atom { color: #F90; } -.cm-s-mdn-like .cm-number { color: #ca7841; } -.cm-s-mdn-like .cm-def { color: #8DA6CE; } -.cm-s-mdn-like span.cm-variable-2, .cm-s-mdn-like span.cm-tag { color: #690; } -.cm-s-mdn-like span.cm-variable-3, .cm-s-mdn-like span.cm-def, .cm-s-mdn-like span.cm-type { color: #07a; } - -.cm-s-mdn-like .cm-variable { color: #07a; } -.cm-s-mdn-like .cm-property { color: #905; } -.cm-s-mdn-like .cm-qualifier { color: #690; } - -.cm-s-mdn-like .cm-operator { color: #cda869; } -.cm-s-mdn-like .cm-comment { color:#777; font-weight:normal; } -.cm-s-mdn-like .cm-string { color:#07a; font-style:italic; } -.cm-s-mdn-like .cm-string-2 { color:#bd6b18; } /*?*/ -.cm-s-mdn-like .cm-meta { color: #000; } /*?*/ -.cm-s-mdn-like .cm-builtin { color: #9B7536; } /*?*/ -.cm-s-mdn-like .cm-tag { color: #997643; } -.cm-s-mdn-like .cm-attribute { color: #d6bb6d; } /*?*/ -.cm-s-mdn-like .cm-header { color: #FF6400; } -.cm-s-mdn-like .cm-hr { color: #AEAEAE; } -.cm-s-mdn-like .cm-link { color:#ad9361; font-style:italic; text-decoration:none; } -.cm-s-mdn-like .cm-error { border-bottom: 1px solid red; } - -div.cm-s-mdn-like .CodeMirror-activeline-background { background: #efefff; } -div.cm-s-mdn-like span.CodeMirror-matchingbracket { outline:1px solid grey; color: inherit; } diff --git a/docgen/src/stylesheets/components/_code-snippets.scss b/docgen/src/stylesheets/components/_code-snippets.scss deleted file mode 100644 index c54f8fe6..00000000 --- a/docgen/src/stylesheets/components/_code-snippets.scss +++ /dev/null @@ -1,88 +0,0 @@ -@import "modules/base"; - -$light-theme: ( - ('keyword', '#00aaff'), - ('operator', '#f95faa'), - ('property', '#75de00'), - ('variable', '#75de00'), - ('string', '#eeba00'), - ('comment', '#bbbbbb') -); - -$size: 13px; -$line-height: round(($size / 2)*2.69); -$gutter-width: 3.5em; -$padding: 13px; -$line-number-size: round(($size / 2)*1.53); - -pre { - - &.al-snippet { - box-sizing: border-box; - line-height: initial; - word-break: initial; - word-wrap: initial; - border-radius: 5px; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1); - display: inline-block; - margin: initial !important; - padding: $padding*2 $padding*2 $padding*2 $padding*4.5; - position: relative; - line-height: $line-height; - font-size: $size; - white-space: pre-line; - - &:before { - content: '1 2 3 4 5 6 7 8'; - display: block; - padding: $padding*2 0 !important; - position: absolute; - width: $gutter-width; - background: #FDFDFD; - border-right: 1px solid rgba(black, 0.1); - top: 0; - left: 0; - z-index: 9; - height: 100%; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - word-spacing: $gutter-width; - white-space: pre-line; - line-height: $line-height*1.08; - font-size: $line-number-size*1.11; - text-align: center; - color: #bbbbbb; - } - - code { - margin: 0; - padding: 0; - font-weight: 500; - white-space: inherit; - font-family: 'Roboto Mono', roboto, sans-serif; - font-size: $size; - } - - // Theme - &[data-snippet-theme="light"] { - border: solid 1px #d7d9de !important; - background-color: $white !important; - - // Main text color - color: #666666; - - // Now, let' give it colors - // Namespace : sn- - @each $name, $color in $light-theme { - .sn-#{$name} { - color: #{$color} - } - } - } - } -} - - -* { - background: red !important -} \ No newline at end of file diff --git a/docgen/src/stylesheets/components/_colored-tiles.scss b/docgen/src/stylesheets/components/_colored-tiles.scss deleted file mode 100644 index 9149248b..00000000 --- a/docgen/src/stylesheets/components/_colored-tiles.scss +++ /dev/null @@ -1,62 +0,0 @@ -.cm--item-block, -.colored-tile { - max-width: 120px; - height: 120px; - border-radius: 16px; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1); - position: relative; - - &.small { - width: 64px; - height: 64px; - border-radius: 8px; - } - - - img { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - margin: auto; - } - - &.block-green, - &.tile-green{ - background-image: linear-gradient(322deg, #29c9b2, #b7da46); - } - - &.block-blue, - &.tile-blue { - background-image: linear-gradient(135deg, #53ddec, #463d9d); - } - - &.block-orange, - &.tile-orange { - background-image: linear-gradient(140deg, #ffc003, #f02b2b); - } - - &.block-red, - &.tile-red{ - background-image: linear-gradient(315deg, #ec4918, #e25d8d); - } - - &.block-yellow, - &.tile-yellow { - background-image: linear-gradient(134deg, #fad961, #f76b1c); - } - - &.block-purple, - &.tile-purple { - background-image: linear-gradient(138deg, #af84e3, #5071c7); - } - - &.block-seaweed, - &.tile-seaweed { - background-image: linear-gradient(135deg, #13c4a5, #10a4b8); - } - &.tile-lime { - background-image: linear-gradient(45deg, #5f8e3d 0%, #94be46 100%); - } -} diff --git a/docgen/src/stylesheets/components/_columns.scss b/docgen/src/stylesheets/components/_columns.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/docgen/src/stylesheets/components/_documentation.sass b/docgen/src/stylesheets/components/_documentation.sass deleted file mode 100644 index 5a33bc16..00000000 --- a/docgen/src/stylesheets/components/_documentation.sass +++ /dev/null @@ -1,443 +0,0 @@ -$sidebar-width: 260px -$offset-height: 60px - -.documentation-section, -.examples-section - padding-bottom: 300px - - .container - article - width: 100% - position: relative - margin-top: 60px - - p, - ul, - ol - font-size: 16px - line-height: 22px - - ul - margin: 0 - position: relative - - li - padding: 0 - - ul - padding: 0 1em - - h2,h3,h4 - color: #201c38 - - h2 - font-size: 28px - padding-right: 1em - margin-top: 32px - padding-bottom: 8px - - &:first-of-type - margin-top: 0 - - +small-mq - font-size: 22px - - h3 - margin-top: 32px - font-size: 24px - color: #201c38 - position: relative - z-index: 10 - - h4 - font-size: 18px - font-weight: normal - margin-top: 2em - - .api tr td:first-child - vertical-align: top - - p, ul - font-size: 90% - margin: .5em 0 - line-height: 1.5em - - ul - padding-left: 1.5em - - em - text-decoration: underline - font-style: normal - - .documentation-container - float: left - position: relative - left: #{$sidebar-width} - width: calc(100% - #{$sidebar-width}) - min-height: 1000px - padding-left: 60px - padding-top: 60px - - p, li - color: rgba(32, 28, 56, 0.8) - line-height: 2 - font-size: 16px - font-family: 'Open Sans', Helvetica Neue, helvetica, sans-serif - clear: both - - ul, - ol - font-size: 16px - line-height: 22px - - li - clear: both - margin-bottom: 10px - - ul - font-weight: normal - font-size: 14px - position: relative - - li - line-height: 26px - - &:before - content: '-' - float: left - margin-right: 6px - color: $logan - - .content - & p:first-child - margin-top: 0 - - .heading - background: #fff - border: 1px solid #d8d8d8 - border-radius: 2px 2px 0 0 - margin: -8px 0 0 - padding: 0.75em 1em - font-family: $paragraphs-font-family - border-radius: 6px 6px 0 0 - font-size: 13px - color: #8994c6 - font-weight: 400 - - &+ pre - border-top: none - border-radius: 0 0 6px 6px - - button - float: right - background: #f1f4fd - border: none - padding: 2px 8px - font-size: 90% - border-radius: 2px - - .icon - width: 12px - - svg - width: 12px - height: 12px - - use - fill: #8794cb - - pre - margin: 0 0 .5em 0 - line-height: 23px - white-space: pre - overflow-x: auto - word-break: inherit - word-wrap: inherit - border: 1px solid #d8d8d8 - border-radius: 0 0 2px 2px - padding: .5em 0 - position: relative - z-index: 1 - background: #fff - border-radius: 6px - - // avoid scroll bar being unusable because

used and headers have :content that will be over - - code - display: block - width: calc(100% - 2em) - margin: auto - - - p + div.heading - margin-top: 1em - - + pre - margin-bottom: 2em - - h2, h3, .api-entry, .css-class, .type - &:before - content: "" - display: block - height: $offset-height - margin: (-$offset-height) 0 0 - - .anchor - margin-left: .2em - display: inline - visibility: hidden - - h2, h3 - .anchor - text-decoration: none - - .anchor:after - content: '#' - color: $accent-color - - &:hover - .anchor - visibility: visible - - &:target - .anchor - visibility: visible - - .struct-def - padding-left: 0 - margin-top: 24px - - .default-value - font-size: 12px - line-height: 16px - code - background: transparent - - .type - list-style-type: none - - p - margin-top: 0 - - .editThisPage - position: absolute - font-size: 12px - right: 0 - top: 12px - padding: 3px 6px - border: 1px solid darken($accent-color, 20%) - color: darken($accent-color, 20%) - border-radius: 3px - text-transform: uppercase - opacity: .7 - - &:hover - background: $accent-color - color: white - opacity: 1 - text-decoration: none - - - +small-mq - padding-top: 32px - - .container - .documentation-container - left: 0 - width: 100% - padding: 32px 1em - - article - float: left - margin-top: 40px - - .documentation .hero-section - .fl-left - max-width: 300px !important - width: 300px !important - - - .typed-link - color: inherit - text-decoration: underline - -.examples-section > .container - padding-top: 60px - -.storybook-section - min-height: 120px - text-align: center - line-height: 120px - -.img-object - padding: 6px - border: 1px solid rgba(black, 0.1) - margin: 16px 0 - margin-right: 26px - max-width: 100% - max-height: 500px - margin: auto - - &[align="right"] - float: right - margin-right: 0 - margin-left: 26px - - & + p:not(.cb) - clear: none !important - - & ~ p:not(.cb) - clear: none !important - - &[align="left"] - float: left - margin: 18px 0 16px - margin-right: 26px - - & + p - clear: none !important - - & ~ p - clear: none !important - -.documentation-container - table - border-collapse: collapse - border-spacing: 0 - border: 1px solid #d8d8d8 - color: darken($logan, 15%) - - td, th - padding: 0 - - table, th, td - - table - width: 100% - display: table - - table.bordered > thead > tr, table.bordered > tbody > tr - border-bottom: 1px solid #d0d0d0 - - table.striped > tbody > tr:nth-child(odd) - background-color: #f2f2f2 - - table.striped > tbody > tr > td - border-radius: 0 - padding: 0 8px !important - - table.highlight > tbody > tr - transition: background-color 0.25s ease - - table.highlight > tbody > tr:hover - background-color: #f2f2f2 - - table.centered thead tr th, table.centered tbody tr td - text-align: center - - thead, - tbody > tr:not(:last-child) - border-bottom: 1px solid #d0d0d0 - - thead th - color: darken($logan, 35%) - - td, th - padding: 15px 5px - display: table-cell - text-align: left - vertical-align: middle - border-radius: 2px - - td, - th - padding: 15px 16px - border-left: 1px solid #d0d0d0 - - @media only screen and (max-width: 992px) - table.responsive-table - width: 100% - border-collapse: collapse - border-spacing: 0 - display: block - position: relative - - table.responsive-table td:empty:before - content: " " - - table.responsive-table th, table.responsive-table td - margin: 0 - vertical-align: top - - table.responsive-table th - text-align: left - - table.responsive-table thead - display: block - float: left - - table.responsive-table thead tr - display: block - padding: 0 10px 0 0 - - table.responsive-table thead tr th::before - content: " " - - table.responsive-table tbody - display: block - width: auto - position: relative - overflow-x: auto - white-space: nowrap - - table.responsive-table tbody tr - display: inline-block - vertical-align: top - - table.responsive-table th - display: block - text-align: right - - table.responsive-table td - display: block - min-height: 1.25em - text-align: left - - table.responsive-table tr - padding: 0 10px - - table.responsive-table thead - border: 0 - border-right: 1px solid #d0d0d0 - - table.responsive-table.bordered th - border-bottom: 0 - border-left: 0 - - table.responsive-table.bordered td - border-left: 0 - border-right: 0 - border-bottom: 0 - - table.responsive-table.bordered tr - border: 0 - - table.responsive-table.bordered tbody tr - border-right: 1px solid #d0d0d0 - - - .live-example - border: 1px solid #d8d8d8 - padding: 20px - - h3 - margin-top: 0 - margin-bottom: 10px - &:before - height: 0 - margin: 0 - - .live-example-helpers - border-top: 1px solid #d8d8d8 - padding-top: 10px - margin-top: 20px diff --git a/docgen/src/stylesheets/components/_dropdown.scss b/docgen/src/stylesheets/components/_dropdown.scss deleted file mode 100644 index c5d2ab3a..00000000 --- a/docgen/src/stylesheets/components/_dropdown.scss +++ /dev/null @@ -1,339 +0,0 @@ -// Detect lightness -@function detectLightness($color) { - @if (lightness($color) > 60) { - @return mix($color, #000, 90%); - } @else { - @return mix($color, $white, 90%); - } - } - -// Spacing -@function spacing($type) { - @if $type == 'compact' { - @return 12px; - } - @if $type == 'spacious' { - @return 22px; - } - @else { - @return 16px; - } -} - -// Description -@function desc($display) { - @if $display == false { - @return none; - } - @else { - @return block; - } -} - -// Layout type -@mixin layout-type($type) { - @if $type == 'small' { - max-width: 500px; - min-width: 400px; - } - @if $type == 'normal' { - max-width: 600px; - min-width: 500px; - } - @if $type == 'large' { - max-width: 800px; - min-width: 600px; - } - @if $type == 'full' { - width: 100%; - } -} - -// Alignement type -@mixin alignment-type($type) { - @if $type == 'left' { - left: 0 !important; - right: inherit !important; - } - @if $type == 'right' { - right: 0 !important; - left: inherit !important; - } - @if $type == 'center' { - left: 0 !important; - right: 0 !important; - margin: auto !important; - } -} - -// Mixin - Shadow type -@mixin shadow-type($type) { - @if $type == 'light' { - box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2), 0 2px 3px 0 rgba(0, 0, 0, 0.1) ; - } - @if $type == 'heavy' { - box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); - } -} - -@mixin dropdown( - $main-color: #458EE1, - $layout-type: normal, - $layout-alignment: right, - $layout-width: 800, - $background-color: $white, - $border-radius: 4, - $border-width: 1, - $border-color: #d9d9d9, - $box-shadow: light, - $branding-position: bottom, - $font-size: normal, - $header-color: #33363D , - $title-color: #02060C, - $subtitle-color: #A4A7AE, - $text-color: #63676D, - $highlight-color: #3881FF, - $spacing: normal, - $include-desc: true, - $background-category-header: $white -){ - - $padding: spacing($spacing); - - $header-size: 1em; - $title-size: .9em; - $text-size: .85em; - $subtitle-size: .9em; - - @if $font-size == 'small' { - $header-size: .95em; - $title-size: .8em; - $text-size: .75em; - $subtitle-size: .8em; - } - - @else if $font-size == 'large' { - $header-size: 1.1em; - $title-size: 1em; - $text-size: .9em; - $subtitle-size: 1em; - } - - .algolia-autocomplete { - display: block; - width: 100%; - height: 100%; - } - - // powered by - .algolia-docsearch-footer { - width: 100px; - height: 20px; - z-index: 2000; - margin-top: $padding/2; - float: right; - font-size: 0; - line-height: 0; - &--logo { - background-image: url('data:image/svg+xml;utf8,'); - background-repeat: no-repeat; - background-position: center; - background-size: 100%; - overflow: hidden; - text-indent: -9000px; - padding: 0!important; - width: 100%; - height: 100%; - display: block; - } - } - - - // Dropdown wrapper - .aa-dropdown-menu { - position: relative; - top: -6px; - border-radius: $border-radius+px; - margin: 6px 0 0; - padding: 0; - text-align: left; - height: auto; - position: relative; - background: transparent; - border: none; - @include layout-type($layout-type); - @include alignment-type($layout-alignment); - @include shadow-type($box-shadow); - - // Arrow - &:before { - position: absolute; - content: ''; - width: 14px; - height: 14px; - background: $background-color; - z-index: 0; - top: -7px; - border-top: $border-width+px solid $border-color; - border-right: $border-width+px solid $border-color; - transform: rotate(-45deg); - border-radius: 2px; - z-index: 999; - @if $layout-alignment == 'center' { - display: none; - } - @else { - display: block; - #{$layout-alignment}: 48px; - } - } - - .aa-suggestions { - position: relative; - z-index: 1000; - } - - [class^="aa-dataset-"] { - position: relative; - border: solid $border-width+px $border-color; - background: $background-color; - border-radius: $border-radius+px; - overflow: auto; - padding: 0 $padding $padding/2; - } - - // Inner-grid setup - * { - box-sizing: border-box; - } - } - - // Each suggestion item is wrapped - .algolia-docsearch-suggestion { - position: relative; - padding: 0; - background: $background-color; - color: $title-color; - overflow: hidden; - - &--highlight { - color: darken($main-color,10%); - background-color: rgba($main-color,.08); - .algolia-docsearch-suggestion--category-header &{ - background: inherit; - } - } - - &--content { - display: block; - float: right; - width: 70%; - position: relative; - padding: $padding/3 0 $padding/3 $padding/1.5; - cursor: pointer; - - .aa-cursor &{ - background: rgba(black, .03); - } - &:before { - content: ''; - position: absolute; - display: block; - top: 0; - height: 100%; - width: 1px; - background: #ddd; - left: -1px; - } - } - - &--category-header { - position: relative; - border-bottom: 1px solid #ddd; - display: none; - padding: $padding/4 0; - font-size: $header-size; - color: $header-color; - - .algolia-docsearch-suggestion__main & { - display: block; - margin-top: $padding; - } - } - - &--wrapper { - width: 100%; - float: left; - padding: $padding/2 0 0 0; - } - - &--subcategory-column { - float: left; - width: 30%; - display: none; - padding-left: 0; - text-align: right; - position: relative; - padding: $padding/3 $padding/1.5; - color: $subtitle-color; - font-size: $subtitle-size; - word-wrap: break-word; - - &:before { - content: ''; - position: absolute; - display: block; - top: 0; - height: 100%; - width: 1px; - background: #ddd; - right: 0; - } - - .algolia-docsearch-suggestion__secondary & { - display: block !important; - } - } - - &--subcategory-inline { - display: none; - } - - &--title { - margin-bottom: $padding/4; - color: $title-color; - font-size: $title-size; - font-weight: bold; - } - - &--text { - display: desc($include-desc); - line-height: 1.2em; - font-size: $text-size; - color: $text-color; - } - - &--no-results{ - width: 100%; - padding: $padding/2 0; - text-align: center; - font-size: 1.2em; - - &::before{ - display: none; - } - } - - code { - padding: 1px 5px; - font-size: 90%; - border: none; - color: #222222; - background-color: #EBEBEB; - border-radius: 3px; - .algolia-docsearch-suggestion--highlight { - background: none; - } - } - } -} diff --git a/docgen/src/stylesheets/components/_examples.scss b/docgen/src/stylesheets/components/_examples.scss deleted file mode 100644 index 435efedd..00000000 --- a/docgen/src/stylesheets/components/_examples.scss +++ /dev/null @@ -1,145 +0,0 @@ -.examples-container { - padding: 40px 0 ; - - .flex-it-2 { - @include small-mq { - flex: 0 1 100% !important - } - } - - .example-tile { - margin-bottom: 4em; - background: #fefefe; - border: 1px solid #ebebeb; - position: relative; - text-align: center; - overflow: hidden; - - - a { - color: #fff; - line-height: 5; - padding: 0 1em; - - - @include small-mq { - figcaption { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: 100%; - display: block; - height: 50px; - line-height: 1; - font-size: 12px; - top: 30px; - } - } - } - - - figure { - img { - width: 100%; - } - } - - figcaption { - width: 100%; - height: 100%; - text-indent: 0; - color: inherit; - left: 0; - background: darken($accent-color, 0.95); - text-transform: uppercase; - font-size: 1.6em; - font-weight: bold; - color: #fff; - transform: scale(0); - z-index: 2; - line-height: 18; - transition: transform 0.2s ease; - position: absolute; - visibility: visible; - - } - - &:hover { - - figcaption { - transform: scale(1); - top: 0; - } - } - } -} - - - -.form-control { - width: 100%; - box-sizing: border-box; - padding-left: 16px; - line-height: 40px; - height: 40px; - border: 1px solid #cccccc; - border-radius: 3px; - outline: none; -} - -.form-group { - margin-bottom: 1em; - clear: both; - - label { - display: block; - font-weight: bold; - font-size: .8em; - } -} - -#map-example-container, -#map-example-container-paris { - // trigger zindex value otherwise - // map goes hover other elements (like header) - position: relative; - z-index: 0; -} - - -body.example { - .cm-navigation { - z-index: 99999; - top: 0; - border-bottom: 1px solid darken(#1d2f40, 10%); - } - - aside { - float: left; - top: 130px; - } - .content-wrapper { - top: 60px; - } - - #results-topbar { - min-height: 60px; - } - - .btn{ - color: black; - } - -} - -#examples { - ul { - margin-top: 20px; - } - ul > li { - clear: none; - } - h2 { - clear: both; - } -} \ No newline at end of file diff --git a/docgen/src/stylesheets/components/_footer.sass b/docgen/src/stylesheets/components/_footer.sass deleted file mode 100644 index 618df5b7..00000000 --- a/docgen/src/stylesheets/components/_footer.sass +++ /dev/null @@ -1,176 +0,0 @@ - -$footer-shade-1: #2E2A51 -$footer-shade-2: #1F1C3F -$footer-shade-3: #12102E - -// Footer CTA -.footer-new-cta - background: url('../images/algolia-fast-bg.svg')no-repeat center center / cover - margin-bottom: -90px - padding-bottom: 60px - height: 750px - - .homepage & - height: 940px - - @media (min-width: $screen-md) - margin-top: -200px - - @media (max-width: $screen-md) - height: 860px - text-align: center - - @media (max-width: $screen-sm) - height: 820px - - .stellar-container - width: 100% !important - - @media (max-width: $screen-xs) - height: 960px - - @include diagonal(-6deg, $white, 180px, before) - top: -80px - z-index: 1 - - @media (max-width: $screen-sm) - height: 80px - top: -40px - - .button-holder - background: url('../images/light-speed.svg')no-repeat center top / contain - span.inline - float: right - - @media (max-width: $screen-lg) - top: 4% - - i.icon-search - vertical-align: middle - - - @media (max-width: ($screen-lg) ) - background: url('../images/light-speed.svg')no-repeat right top / 92% - - @media (max-width: ($screen-md) ) - background: transparent - span.inline - width: 100% - text-align: center - - .btn - margin: auto - -#footer - position: relative - - - @include diagonal(-4deg, $footer-shade-2, 130px, after) - bottom: 64px - left: 0 - - .credits - margin-top: 80px - text-align: center - position: relative - background-color: $footer-shade-3 - color: $portage - z-index: 99 - line-height: 40px - @include diagonal(-2deg, $footer-shade-3, 80px, before) - top: -40px - left: 0 - - @media (max-width: $screen-md) - margin-top: 40px - - h4 - line-height: 40px - border-bottom: solid 1px rgba($portage,.5) - - .footer-nav - li - line-height: 1.7em - a - color: $portage - font-size: .9em - &:hover - color: $algolia-blue - text-decoration: none - - .network-links - margin: 0 - padding: 0 - li - display: inline-block - padding: 0 - margin: 0 - text-align: center - margin: .5em .2em .5em 0 - .btn - color: $portage - width: 38px - line-height: 40px - max-height: 38px - font-size: 24px - padding: 0 - border: solid 1px rgba($portage,.4) - &:hover - color: white - - .newsletter-signup - position: relative - - .mc-signupmessage - width: 100% - height: 100% - background-color: $white - position: absolute - z-index: 1 - display: none - pointer-events: none - opacity: 0 - transition: 400ms ease-in-out - - svg - width: 15px - margin-left: 4px - vertical-align: middle - - .form-control - border-radius: 20px - line-height: 40px - padding: 0 16px - height: 40px - border: none - width: 230px - - @media (max-width: $screen-md) - width: 180px !important - - &--success - .mc-signupmessage - animation: fadeIn 200ms ease-in-out forwards - display: block - -@keyframes fadeIn - from - opacity: 0 - to - opacity: 1 - -.search-icon, -.arrow-icon - margin-left: 1em - width: 22px - height: 42px - vertical-align: middle - - @media (max-width: $screen-sm) - display: none - - -.arrow-icon - width: 16px - use - fill: $bunting diff --git a/docgen/src/stylesheets/components/_hero.sass b/docgen/src/stylesheets/components/_hero.sass deleted file mode 100644 index 5cc7cb1b..00000000 --- a/docgen/src/stylesheets/components/_hero.sass +++ /dev/null @@ -1,55 +0,0 @@ -.hero - padding-top: 56px - text-align: center - position: relative - - h1 - font-size: 56px !important - h2 - font-size: 20px !important - font-weight: 300 !important - display: inline-block - max-width: 80% - line-height: 1.5 - - &:not(.hero-doc) - @include diagonal(-6deg, $white, 300px, before) - bottom: -190px - z-index: 1 - box-shadow: rgba(112, 128, 175, 0.05) 0 -16px 24px - - @media (max-width: $screen-sm) - height: 100px - bottom: -80px - - @media (max-width: $screen-sm) - figure - img - width: 80% - max-width: 320px - - h1 - font-size: 36px !important - font-weight: bold - h2 - font-size: 18px !important - - - .row - background: url(../images/background-hero.svg)no-repeat center left / 80% - - @media (max-width: $screen-sm) - background: url(../images/background-hero.svg)no-repeat center left / cover - - @media (min-width: 1600px) - height: 600px - background: url(../images/background-hero.svg)no-repeat center left / 80% - - -// Hero documentation -.hero.hero-doc - background: - image: url('../images/logo_isjs.svg'), linear-gradient(to right, #211e39, #6a7599) - repeat: no-repeat - size: 330px, 100% - position: top 90% right 5em, center left diff --git a/docgen/src/stylesheets/components/_light-switcher.scss b/docgen/src/stylesheets/components/_light-switcher.scss deleted file mode 100644 index d02ba4ac..00000000 --- a/docgen/src/stylesheets/components/_light-switcher.scss +++ /dev/null @@ -1,26 +0,0 @@ -.light-switcher { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - padding: 8px 16px; - padding-left: 32px; - border: 1px solid $portage; - font-size: 16px; - color: #7f8b9d; - line-height: 24px; - display: inline-block; - min-width: 236px; - - &--light { - border-radius: 0; - border: none; - border-bottom: solid 2px $shamrock; - color: white; - } - - &.location-switch { - background: url(image_path("pricing/icon-pin.svg"))no-repeat center left 8px, - url(image_path("pricing/icon-up-down.svg"))no-repeat center right 8px; - background-size: 19px, 8px; - } -} diff --git a/docgen/src/stylesheets/components/_medias.sass b/docgen/src/stylesheets/components/_medias.sass deleted file mode 100644 index fbbe78c6..00000000 --- a/docgen/src/stylesheets/components/_medias.sass +++ /dev/null @@ -1,7 +0,0 @@ -figure - figcaption - visibility: hidden - - -.section-illustration - max-width: 100% \ No newline at end of file diff --git a/docgen/src/stylesheets/components/_modal-hint.scss b/docgen/src/stylesheets/components/_modal-hint.scss deleted file mode 100644 index 98e7f59c..00000000 --- a/docgen/src/stylesheets/components/_modal-hint.scss +++ /dev/null @@ -1,109 +0,0 @@ -@import "modules/base"; - -.modal-hint { - position: fixed; - width: 380px; - height: auto; - top: 16px; - right: 16px; - background: $white; - padding: 2em 2em 1.5em; - text-align: center; - border-radius: 6px; - box-shadow: 0 16px 32px 0 rgba($bunting, 0.2); - border: 1px solid rgba(0,0,0,0.1); - will-change: opacity, transform; - opacity: 0; - z-index: -1; - visibility: hidden; - - - h3 { - margin-top: 0; - } - - .col-md-6 { - text-align: center; - - a { - padding: 8px 0; - display: inline-block; - margin-right: 8px; - color: #7485AA; - } - } - - .img-thumbnail { - max-width: 100%; - position: relative; - display: inline-block; - } - - .icon { - display: inline-block; - max-width: 16px; - max-height: 16px; - fill: #667492; - position: relative; - top: 4px; - } - - .btn { - width: 100%; - } - - - #closeHintAssets { - position: absolute; - top: 0; - right: 0; - - width: 32px; - height: 32px; - display: block; - - svg { - width: 28px; - height: 28px; - padding: 8px; - - path { - fill: #667492 - } - } - - &:hover { - svg path { - fill: #FF2060 - } - } - } -} - - -/// ANIMATION -.animated { - -webkit-animation-duration: 0.75s; - animation-duration: 0.75s; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; -} - -@keyframes bounceInRight { - 0%, 60%, 75%, 90%, 100% { - transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); - } - - 0% { - opacity: 0; - transform: translate3d(3000px, 0, 0); - } - - 100% { - transform: none; - } -} - -.bounceInRight { - animation-name: bounceInRight; -} \ No newline at end of file diff --git a/docgen/src/stylesheets/components/_navigation.sass b/docgen/src/stylesheets/components/_navigation.sass deleted file mode 100644 index 124cfa40..00000000 --- a/docgen/src/stylesheets/components/_navigation.sass +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Algolia components - community header - * Light theme - */ - - -// Navigation -.algc-navigation - background: $white - box-shadow: 0 10px 40px 0 rgba($bunting,0.07), 0 2px 9px 0 rgba($bunting,0.06) - - -.algc-navigation__brands a, -.algc-navigation__menu .algc-menu__list a - color: $bunting - font-weight: 400 - - &:hover - color: $deep-cove - text-decoration: none - - -.algc-menu__search .algc-search__input button svg path - fill: $bunting !important - - -.algc-menu__search--holder.open input[type="search"] - background: rgba($bunting, 0.1) - color: $bunting - - &::-webkit-input-placeholder - color: $bunting - &:-moz-placeholder - color: $bunting - &::-moz-placeholder - color: $bunting - &:-ms-input-placeholder - color: $bunting - -.algc-navigation__brands svg.algc-arrowseparator path - fill: $logan - - -// Dropdown -.algc-dropdownroot - .algc-dropdownroot__dropdownarrow - background: #F7F7FF - - - &.activeDropdown .algc-dropdownroot__dropdownbg - box-shadow: 0 50px 100px rgba(50, 50, 93, 0.1), 0 15px 35px rgba(50, 50, 93, 0.15), 0 5px 15px rgba(0, 0, 0, 0.1), 0 -4px 16px rgba(50, 50, 93,0.1) - - -.algc-dropdownroot__widelist li h4 - white-space: nowrap - overflow: hidden - text-overflow: ellipsis - width: 70% - display: block -// Mobile -.algc-openmobile - background: #332e58 - border-radius: 6px - -.algc-mobilemenu - background: #f8faff - &:after - display: none - - a - color: $bunting - .algc-mobilemenu__item:after - background: linear-gradient(76deg, rgba(#211e39, 0.01) 0%, rgba(#211e39, 0) 18%, #211e39 46%, rgba(#6a7599, 0.7) 93%, rgba(#6a7599, 0) 100%) - - - -@media only screen and (max-width: $screen-sm) - .algc-navigation__brands .algc-navigation__li.algc-navigation__li--community - width: 45px !important - overflow: hidden !important diff --git a/docgen/src/stylesheets/components/_quotes.scss b/docgen/src/stylesheets/components/_quotes.scss deleted file mode 100644 index d9efb211..00000000 --- a/docgen/src/stylesheets/components/_quotes.scss +++ /dev/null @@ -1,26 +0,0 @@ -@import "modules/base"; - -/** - * Quotes holder - * - */ -.quotes-holder { - .quote { - display: block; - font-size: 26px; - font-style: italic; - font-weight: lighter; - line-height: 1.3em; - - p { - color: $deep-cove; - } - } - footer { - margin: 16px 0; - - p { - color: $cloudy-blue-3; - } - } -} diff --git a/docgen/src/stylesheets/components/_sidebar.sass b/docgen/src/stylesheets/components/_sidebar.sass deleted file mode 100644 index 7c9e954e..00000000 --- a/docgen/src/stylesheets/components/_sidebar.sass +++ /dev/null @@ -1,136 +0,0 @@ -$sidebarTopOffset: 60px; - -.sidebar - width: $sidebar-width - overflow: scroll - height: calc(100vh - 56px - #{$sidebarTopOffset}) - margin-top: $sidebarTopOffset - padding-top: 30px - padding-bottom: 30px - - &.fixed - position: fixed - top: $sidebarTopOffset - - ul - list-style: none - padding: 0 !important - margin: 0 !important - - li - display: inline-block - height: 40px - margin: 0 - padding: 0 - width: 100% - - ul - padding-left: 1em !important - - a - height: 40px - display: inline-block - width: 100% - line-height: 40px - text-decoration: none - font-weight: 300 - color: rgba(32, 28, 56, 0.8) - font-family: OpenSans, helvetica, sans-serif - transition: transform .2s ease - - &:before - content: '' - display: block - width: 10px - height: 10px - background: $accent-color - border-radius: 100% - position: absolute - top: 50% - left: -20px - opacity: 0 - transform: translateY(-50%) - transition: opacity .2s ease 0.15s - - &.navItem-active - color: #201c38 - font-weight: 600 - transform: translateX(20px) - transition: transform .2s ease - - &:before - opacity: 1 - - @include small-mq - width: calc(100% + 16px) - height: calc(100%) - max-width: 320px - position: fixed - overflow: auto - left: 0 - top: -10px !important - border: none - padding-top: 0 - background: #fefefe - z-index: 150 - display: block - padding: 1em - box-shadow: -20px 0 100px rgba(0,0,0,0.25) - padding-top: 1em - - transform: translateX(-100%) - will-change: transform - transition: transform 0.3s ease - - .sidebar-header - max-width: calc(100% - 1em) - - - &.Showed - transform: translateX(0) - - - .sidebar-container - width: 100% - max-width: 100% - - - - -.sidebar-opener - position: fixed - left: 2em - bottom: 2em - height: 52px - width: 52px - background: #39b1de - background-image: url(../assets/img/open-doc-menu_icon.svg), linear-gradient(284deg, darken(#211e39, 15%), darken(#6A7599, 15%)) - background-repeat: no-repeat - background-position: 65% center, center center - background-size: 60%, 100% - z-index: 11 - border-radius: 50% - box-shadow: 0 6px 8px 0 rgba(0,0,0,0.14),0 1px 5px 0 rgba(0,0,0,0.12),0 3px 1px -2px rgba(0,0,0,0.2) - transition: transform 0.2s ease, left 0.2s ease - transform-origin: center center - cursor: pointer - display: none - z-index: 99999 - - @include small-mq - display: block - - - &:active - transform: scale(0.9) - - - &.Showed - transform: rotate(-180deg) - transform-origin: center center - background-image: url(../assets/img/open-doc-menu_icon.svg), linear-gradient(284deg, darken(#211e39, 15%), darken(#6A7599, 15%)) - left: 340px - - - - diff --git a/docgen/src/stylesheets/components/_switcher.scss b/docgen/src/stylesheets/components/_switcher.scss deleted file mode 100644 index 7b9b5e4c..00000000 --- a/docgen/src/stylesheets/components/_switcher.scss +++ /dev/null @@ -1,69 +0,0 @@ -.switcher { - cursor: pointer; - display: inline-block; - color: $white; - line-height: 24px; - - input { - display: none; - } - - label { - width: 48px; - height: 24px; - background: #2b97c5; - display: inline-block; - border-radius: 12px; - cursor: pointer; - margin: 0 12px; - font-weight: 400; - - &:after { - width: 16px; - height: 16px; - display: block; - content: ''; - background: $white; - border-radius: 14px; - position: relative; - top: 4px; - left: 4px; - transition: left 0.15s ease; - } - } - - input:checked ~ label[for="switch-plan"]:after { - left: 28px; - } - - .label-1, - .label-2 { - position: absolute; - } - - .label-1 { - float: left; - left: -55px; - } - .label-2 { - float: right; - right: -40px; - } - - label[for="switch-plan"]:not(.checked) { - .label-2 { - opacity: 0.5; - } - } - label[for="switch-plan"].checked { - &:after { - left: 28px; - } - .label-1 { - opacity: 0.5; - } - .label-2 { - opacity: 1; - } - } -} diff --git a/docgen/src/stylesheets/modules/_base.sass b/docgen/src/stylesheets/modules/_base.sass deleted file mode 100644 index 4bb29625..00000000 --- a/docgen/src/stylesheets/modules/_base.sass +++ /dev/null @@ -1,174 +0,0 @@ - -//fonts -$light: 300 -$regular: 400 -$medium: 500 -$bold: 600 - -//colors -$white: white -$black: black - -//V2 -$purple-heart: rgb(142,67,231) -$mulberry: rgb(184,69,146) -$radical-red: rgb(255,79,129) -$bittersweet: rgb(255,108,95) -$koromiko: rgb(255,193,104) -$shamrock: rgb(45,222,152) -$java: rgb(28,199,208) -$algolia-blue: rgb(0,174,255) -$royal-blue: rgb(51,105,231) - -$bunting: rgb(62,57,107) -$titan-white: rgb(248,250,255) -$logan: rgb(157,157,189) -$deep-cove: rgb(5,15,44) - -$port-gore: rgb(58, 69, 112) -$east-bay: rgb(73, 85, 136) -$portage: rgb(137, 148, 198) -$blue-bell: rgb(166, 176, 216) - -$ghost: rgb(196, 200, 216) -$athens-gray: rgb(238, 240, 247) - -$highlight-color: #f6624e -$theme-color: #6a7599 -$accent-color: $theme-color -//OLD -$cabaret: #E7486B -$rouge: #9C4274 -$sea-green: #13C4A5 -$blue-green: #10A4B8 -$violet: #8A63B3 -$chambray: #3B5295 -$sunset-orange: #F6624E -$saffron-mango: #FDBD57 -$red-pink: #fb366e -$plum: #65133a -$light-navy: #184a80 - -$primary-color: $bunting -$secondary-color: $titan-white -$tertiary-color: $portage - -$cloudy-blue: #c4c8d7; -$cloudy-blue-2: #a6b0d8; -$cloudy-blue-3: #8995C7; -$summertime: mix($radical-red, $bittersweet, 50%); - - -//header -$header-height: 60px - -//BootStrap -$gray-darker: lighten(#000, 13.5%) /* #222 */ -$gray-dark: lighten(#000, 20%) /* #333 */ -$gray: lighten(#000, 33.5%) /* #555 */ -$gray-light: lighten(#000, 46.7%) /* #777 */ -$gray-lighter: lighten(#000, 93.5%) /* #eee */ - -$brand-primary: $algolia-blue -$brand-secondary: #FA3870 -$brand-success: $shamrock - -$brand-info: $algolia-blue -$brand-warning: $koromiko -$brand-danger: $radical-red - -$font-size-base: 15px -$font-size-h1: 36px -$font-size-h2: 30px -$font-size-h3: 18px -$font-size-h4: 12px -$font-size-h5: 1em -$font-size-h6: 1em -$line-height-base: 1.6 - -$body-bg: $white -$text-color: $bunting - -$text-muted: $portage - -$border-radius-base: 3px -$border-radius-large: 3px -$border-radius-small: 3px - -$link-color: $brand-primary -$link-hover-color: darken($link-color, 15%) - -//fonts -$font-family-base: 'Montserrat', Helvetica, Arial, sans-serif -$headings-font-family: 'Montserrat', Helvetica, Arial, sans-serif -$font-stack: 'Montserrat', Helvetica, Arial, sans-serif -$paragraphs-font-family: 'Open Sans', helvetica, sans-serif -$headings-small-color: darken($tertiary-color,10%) - -//Buttons -$btn-font-weight: normal -//default -$btn-default-color: #777 -$btn-default-bg: $white -$btn-default-border: #ccc -//primary -$btn-primary-color: $white -$btn-primary-bg: $brand-primary -$btn-primary-border: darken($btn-primary-bg, 5%) -//success -$btn-success-color: $white -$btn-success-bg: $brand-success -$btn-success-border: darken($btn-success-bg, 5%) -//info -$btn-info-color: $white -$btn-info-bg: $brand-info -$btn-info-border: darken($btn-info-bg, 5%) -//warning -$btn-warning-color: $white -$btn-warning-bg: $brand-warning -$btn-warning-border: darken($btn-warning-bg, 5%) -//danger -$btn-danger-color: $white -$btn-danger-bg: $brand-danger -$btn-danger-border: darken($btn-danger-bg, 5%) -//link -$btn-link-disabled-color: $gray-light -$input-bg-disabled: $gray-lighter - -//Panels -$panel-bg: $white - -//** Border color for elements within panels -$panel-inner-border: lighten($tertiary-color,20%) -$panel-footer-bg: lighten($secondary-color, 10%) - -$panel-default-text: $gray-dark -$panel-default-border: $white -$panel-default-heading-bg: $white - -//forms -$input-group-addon-bg: lighten($secondary-color,3%) -$input-border: lighten($gray,30%) - -//tables -$table-border-color: $secondary-color -$table-bg-accent: lighten($secondary-color,5%) - -//tooltip -$tooltip-bg: $primary-color -$tooltip-opacity: 1 - -//popover -$popover-max-width: 300px - -//Cards -$card-border-transition: all 240ms ease-in-out - - -//code -$pre-bg: $bunting -$pre-color: #C2CCCC - - -// Searchboxes -$searchbox-landing: (input-width: 300px, input-height: 50px, border-width: 2px, border-radius: 25px, input-border-color: #cccccc, input-focus-border-color: #ff2e83, input-background: white, input-focus-background: white, font-size: 14px, placeholder-color: #bbbbbb, icon: sbx-icon-search-18, icon-size: 30px, icon-position: left, icon-color: #ff2e83, icon-background: white, icon-background-opacity: 0, icon-clear: sbx-icon-clear-5, icon-clear-size: 18px) diff --git a/docgen/src/stylesheets/modules/_mixins.sass b/docgen/src/stylesheets/modules/_mixins.sass deleted file mode 100644 index d4794150..00000000 --- a/docgen/src/stylesheets/modules/_mixins.sass +++ /dev/null @@ -1,243 +0,0 @@ - -//illus retina -@mixin at2x($path, $ext: "png", $w: auto, $h: auto) - background-image: url(image_path($path + "." + $ext)) - background-repeat: no-repeat - $at2x_path: $path + "@2x" + "." + $ext - @media all and (-webkit-min-device-pixel-ratio: 1.5), all and (-o-min-device-pixel-ratio: 3 / 2), all and (min--moz-device-pixel-ratio: 1.5), all and (min-device-pixel-ratio: 1.5) - background-image: url(image_path($at2x_path)) - background-size: $w $h - -@mixin text-truncate - overflow: hidden - text-overflow: ellipsis - white-space: nowrap - -//vertical alignment -@mixin vertical-align($position: relative) - transform: translateY(-50%) - position: $position - top: 50% - -@mixin horizontal-align($position) - position: $position; - left: 50%; - transform: translateX(-50%); - -@mixin center($position) - position: $position; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - -// Responsive -// Responsive Breakpoints -@mixin big-min-mq - @media (min-width: $screen-lg) - @content - -@mixin big-mq - @media (max-width: $screen-lg) - @content - -@mixin medium-mq - @media (max-width: $screen-md) - @content - -@mixin small-mq - @media (max-width: $screen-sm) - @content - -@mixin mobile-mq - @media (max-width: $screen-xs) - @content - -// Placeholder -@mixin placeholder - &::-webkit-input-placeholder - @content - &:-moz-placeholder - @content - &::-moz-placeholder - @content - &:-ms-input-placeholder - @content - -//BEM helpers -@mixin block($block) - #{$block} - @content - -@mixin element($element) - &__#{$element} - @content - -@mixin modifier($modifier) - &--#{$modifier} - @content - - -@mixin diagonal($rotation, $background, $height, $pos: after) - &:#{$pos} - content: '' - display: block - position: absolute - width: 100% - height: $height - background: #{$background} - transform: skewY($rotation) - z-index: 0 - - @media (min-width: $screen-lg) - transform: skewY($rotation/2) - - @content - -@mixin clearfix() - &:before, - &:after - content: " " - display: table - - &:after - clear: both - - -@function even-px($value) - @if type-of($value) == "number" - @if unitless($value) - $value: $value * 1px - @else if unit($value) == "em" - $value: $value / 1em * 16px - @else if unit($value) == "pts" - $value: $value * 1.3333 * 1px - @else if unit($value) == "%" - $value: $value * 16 / 100% * 1px - $value: round($value) - @if $value % 2 != 0 - $value: $value + 1 - @return $value - - -// Hero animation -=animateScreen($id, $start, $stop) - @keyframes popUp#{$id} - #{$start} - transform: translateY(10px) - opacity: 0 - #{$stop} - transform: translateY(0) - opacity: 1 - - animation: popUp#{$id} 2s ease 0.25s - -=searchbox($font-size: 90%, $input-width: 350px, $input-height: $font-size * 2.4, $border-width: 1px, $border-radius: $input-height / 2, $input-border-color: #cccccc, $input-focus-border-color: #1ec9ea, $input-background: #f8f8f8, $input-focus-background: white, $placeholder-color: #aaaaaa, $icon: "sbx-icon-search-1", $icon-size: $input-height / 1.6, $icon-position: left, $icon-color: #888888, $icon-background: $input-focus-border-color, $icon-background-opacity: 0.1, $icon-clear: "sbx-icon-clear-1", $icon-clear-size: $font-size / 1.1) - display: inline-block - position: relative - width: $input-width - height: even-px($input-height) - white-space: nowrap - box-sizing: border-box - font-size: $font-size - &__wrapper - width: 100% - height: 100% - &__input - display: inline-block - transition: box-shadow .4s ease, background .4s ease - border: 0 - border-radius: even-px($border-radius) - box-shadow: inset 0 0 0 $border-width $input-border-color - background: $input-background - padding: 0 - padding-right: if($icon-position == "right", even-px($input-height) + even-px($icon-clear-size) + 8px, even-px($input-height * 0.8)) + if($icon-background-opacity == 0, 0, even-px($font-size)) - padding-left: if($icon-position == "right", even-px($font-size / 2) + even-px($border-radius / 2), even-px($input-height) + if($icon-background-opacity == 0, 0, even-px($font-size * 1.2))) - width: 100% - height: 100% - vertical-align: middle - white-space: normal - font-size: inherit - appearance: none - &::-webkit-search-decoration, - &::-webkit-search-cancel-button, - &::-webkit-search-results-button, - &::-webkit-search-results-decoration - display: none - &:hover - box-shadow: inset 0 0 0 $border-width darken($input-border-color, 10%) - &:focus, - &:active - outline: 0 - box-shadow: inset 0 0 0 $border-width $input-focus-border-color - background: $input-focus-background - &::placeholder - color: $placeholder-color - &__submit - position: absolute - top: 0 - @if $icon-position == "right" - right: 0 - left: inherit - @else - right: inherit - left: 0 - margin: 0 - border: 0 - border-radius: if($icon-position == "right", 0 $border-radius $border-radius 0, $border-radius 0 0 $border-radius) - background-color: rgba($icon-background, $icon-background-opacity) - padding: 0 - width: even-px($input-height) + if($icon-background-opacity == 0, 0, even-px($font-size / 2)) - height: 100% - vertical-align: middle - text-align: center - font-size: inherit - user-select: none - // Helper for vertical alignement of the icon - &::before - display: inline-block - margin-right: -4px - height: 100% - vertical-align: middle - content: '' - &:hover, - &:active - cursor: pointer - &:focus - outline: 0 - svg - width: even-px($icon-size) - height: even-px($icon-size) - vertical-align: middle - fill: $icon-color - &__reset - display: none - position: absolute - top: (even-px($input-height) - even-px($icon-clear-size)) / 2 - 4px - right: if($icon-position == "right", even-px($input-height) + if($icon-background-opacity == 0, 0, even-px($font-size)), (even-px($input-height) - even-px($icon-clear-size)) / 2 - 4px) - margin: 0 - border: 0 - background: none - cursor: pointer - padding: 0 - font-size: inherit - user-select: none - fill: rgba(black, 0.5) - &:focus - outline: 0 - svg - display: block - margin: 4px - width: even-px($icon-clear-size) - height: even-px($icon-clear-size) - &__input:valid ~ &__reset - display: block - animation-name: sbx-reset-in - animation-duration: .15s - @at-root - @keyframes sbx-reset-in - 0% - transform: translate3d(-20%, 0, 0) - opacity: 0 - 100% - transform: none - opacity: 1 diff --git a/docgen/src/stylesheets/pages/_home.sass b/docgen/src/stylesheets/pages/_home.sass deleted file mode 100644 index 20a640b3..00000000 --- a/docgen/src/stylesheets/pages/_home.sass +++ /dev/null @@ -1,127 +0,0 @@ -.screen-mockup - top: -260px - - img - max-width: 40% - - @media (max-height: 730px) - top: -120px - - @media (max-width: $screen-sm) - top: -180px - img - max-width: 80% !important - width: inherit !important - -.section-best-practices - @media (max-width: $screen-sm) - .img-responsive - top: 100px - -.section-infinite-possibilities - @include diagonal(-4deg, /*$titan-white*/ $titan-white, 180px, after) - top: -80px - left: 0 - -.section-framework - @include diagonal(-4deg, /*$titan-white*/ #FFF, 180px, after) - top: -80px - left: 0 - -.live-section - @include diagonal(-4deg, /*$titan-white*/ $titan-white, 180px, after) - top: -80px - left: 0 - - a - text-decoration: none - - a:hover span - text-decoration: underline - - .container .col-md-12 - transform-style: preserve-3d - perspective: 1030px - - .row - @media (max-width: $screen-sm) - height: auto !important - - - - - .card - margin: auto - width: 100% - top: 0 - left: 0 - - @media (max-width: $screen-md) - transform: none !important - - - - -.discover-section - @include diagonal(-4deg, /*$titan-white*/ #FFF, 180px, after) - top: -80px - left: 0 - .card - line-height: 60px - - .card:hover - background: $titan-white !important - - @media (max-width: $screen-sm) - .logos-container - height: auto !important - - .card - margin-bottom: 1em - - .col-md-12 .col-md-4, - .mobile-projects - padding: 0 - - - -.content - .btn - @media (max-width: $screen-sm) - margin: auto - display: block - max-width: 280px - - @media (max-width: $screen-xs) - width: 100% - -.home - &.content - margin-bottom: 300px - - @media (max-width: $screen-sm) - .container, - .col-md-12, - .col-md-6, - .col-md-8.col-md-offset-2, - .col-md-10.col-md-offset-2 - padding-left: 0 !important - padding-right: 0 !important - max-width: 100% - .col-md-10.col-md-offset-2 .m-l-large - margin-left: 0 !important - .col-md-12.row - margin: 0 !important - - .section-illustration - display: block - max-width: 80% - margin: 2em auto - - h3, p - text-align: center - - .screen-mockup img - max-width: 600px - - diff --git a/docgen/src/stylesheets/partials/_bootstrap.sass b/docgen/src/stylesheets/partials/_bootstrap.sass deleted file mode 100644 index 4159a154..00000000 --- a/docgen/src/stylesheets/partials/_bootstrap.sass +++ /dev/null @@ -1,47 +0,0 @@ -@import "modules/_base" - -// Core variables and mixins -@import "../vendors/bootstrap/variables" -@import "../vendors/bootstrap/mixins" -// Reset and dependencies -@import "../vendors/bootstrap/normalize" -//@import "../vendors/bootstrap/print" -//@import "../vendors/bootstrap/glyphicons" -// Core CSS -@import "../vendors/bootstrap/scaffolding" -@import "../vendors/bootstrap/type" -@import "../vendors/bootstrap/code" -@import "../vendors/bootstrap/grid" -@import "../vendors/bootstrap/tables" -@import "../vendors/bootstrap/forms" -@import "../vendors/bootstrap/buttons" -// Components -// @import "../vendors/bootstrap/component-animations" -// @import "../vendors/bootstrap/dropdowns" -//@import "../vendors/bootstrap/button-groups" -//@import "../vendors/bootstrap/input-groups" -// @import "../vendors/bootstrap/navs" -// @import "../vendors/bootstrap/navbar" -//@import "../vendors/bootstrap/breadcrumbs" -//@import "../vendors/bootstrap/pagination" -//@import "../vendors/bootstrap/pager" -//@import "../vendors/bootstrap/labels" -//@import "../vendors/bootstrap/badges" -//@import "../vendors/bootstrap/jumbotron" -@import "../vendors/bootstrap/thumbnails" -//@import "../vendors/bootstrap/alerts" -//@import "../vendors/bootstrap/progress-bars" -// @import "../vendors/bootstrap/media" -// @import "../vendors/bootstrap/list-group" -//@import "../vendors/bootstrap/panels" -//@import "../vendors/bootstrap/responsive-embed" -//@import "../vendors/bootstrap/wells" -//@import "../vendors/bootstrap/close" -// Components w/ JavaScript -// @import "../vendors/bootstrap/modals" -//@import "../vendors/bootstrap/tooltip" -//@import "../vendors/bootstrap/popovers" -// @import "../vendors/bootstrap/carousel" -// Utility classes -@import "../vendors/bootstrap/utilities" -@import "../vendors/bootstrap/responsive-utilities" diff --git a/docgen/src/stylesheets/partials/_colors.sass b/docgen/src/stylesheets/partials/_colors.sass deleted file mode 100644 index 564e06f2..00000000 --- a/docgen/src/stylesheets/partials/_colors.sass +++ /dev/null @@ -1,123 +0,0 @@ -/* foreground colors */ -.color-white - color: $white !important -.color-titan - color: $titan-white !important -.color-primary - color: $brand-primary !important -.color-secondary - color: $brand-secondary !important -.color-purple-heart - color: $purple-heart !important -.color-mulberry - color: $mulberry !important -.color-radical-red - color: $radical-red !important -.color-bittersweet - color: $bittersweet !important -.color-koromiko - color: $koromiko !important -.color-shamrock - color: $shamrock !important -.color-java - color: $java !important -.color-algolia-blue - color: $algolia-blue !important -.color-royal-blue - color: $royal-blue !important -.color-bunting - color: $bunting !important -.color-deep-cove - color: $deep-cove !important - -.color-port-gore - color: $port-gore !important -.color-east-bay - color: $east-bay !important -.color-portage - color: $portage !important -.color-blue-bell - color: $blue-bell !important -.color-ghost - color: $ghost !important -.color-athens-gray - color: $athens-gray !important -.color-logan - color: $logan !important - -.color-current - color: currentColor !important - - -.fill-dark - background-color: $primary-color !important -.fill-white - background-color: $white !important -.fill-titan - background-color: $titan-white !important -.fill-purple-heart - background-color: $purple-heart !important -.fill-mulberry - background-color: $mulberry !important -.fill-radical-red - background-color: $radical-red !important -.fill-bittersweet - background-color: $bittersweet !important -.fill-koromiko - background-color: $koromiko !important -.fill-shamrock - background-color: $shamrock !important -.fill-java - background-color: $java !important -.fill-algolia-blue - background-color: $algolia-blue !important -.fill-royal-blue - background-color: $royal-blue !important -.fill-deep-cove - background-color: $deep-cove !important -.fill-bunting - background-color: $bunting !important -.fill-port-gore - background-color: $port-gore !important -.fill-east-bay - background-color: $east-bay !important -.fill-portage - background-color: $portage !important -.fill-blue-bell - background-color: $blue-bell !important -.fill-ghost - background-color: $ghost !important -.fill-athens-gray - background-color: $athens-gray !important -.fill-logan - background-color: $logan !important - - -.gradient-primary, -.gradient-green - background-image: linear-gradient(320deg, $shamrock, mix($shamrock,$java)) !important - -.gradient-secondary, -.gradient-blue - background-image: linear-gradient(256deg, $algolia-blue, $royal-blue) !important - -.gradient-tertiary, -.gradient-pink - background-image: linear-gradient(256deg, $bittersweet, $radical-red) !important - -.gradient-orange - background-image: linear-gradient(256deg, $koromiko, $bittersweet) - -.gradient-purple - background-image: linear-gradient(269deg, #8e43e6, #b84592) - -.gradient-light-blue - background-image: linear-gradient(269deg, $algolia-blue, $java) - - -.gradient-dark - background-image: linear-gradient(283deg, $deep-cove, $bunting) -// Not sure why we use that one? -.color-gradient-red - color: #E1271A - background-image: linear-gradient(to bottom, #E39290, #E1271A, #B91E14) diff --git a/docgen/src/stylesheets/partials/_fonts.sass b/docgen/src/stylesheets/partials/_fonts.sass deleted file mode 100644 index 3b8cd6f5..00000000 --- a/docgen/src/stylesheets/partials/_fonts.sass +++ /dev/null @@ -1,13 +0,0 @@ -@import url('https://fonts.googleapis.com/css?family=Montserrat:300,400|Open+Sans:400,600,700') - -h1, h2, h3, h4 - font-size: 32px - font-family: $headings-font-family - -p - font-size: 15px - line-height: 1.6 - font-family: $paragraphs-font-family - -strong - font-family: "Montserrat", helvetica, sans-serif \ No newline at end of file diff --git a/docgen/src/stylesheets/partials/_helpers.sass b/docgen/src/stylesheets/partials/_helpers.sass deleted file mode 100644 index d33041ae..00000000 --- a/docgen/src/stylesheets/partials/_helpers.sass +++ /dev/null @@ -1,599 +0,0 @@ -/** - *spacer - */ -@mixin spacer - width: 100% - font-size: 0 - margin: 0 - padding: 0 - border: 0 - display: block - - -.spacer8 - +spacer - height: 8px -.spacer16 - +spacer - height: 16px -.spacer24 - +spacer - height: 24px -.spacer32 - +spacer - height: 32px -.spacer40 - +spacer - height: 40px -.spacer48 - +spacer - height: 48px -.spacer56 - +spacer - height: 56px -.spacer64 - +spacer - height: 64px -.spacer80 - +spacer - height: 80px -.spacer120 - +spacer - height: 120px - - -/** - *width - */ -.w100 - width: 100px -.w150 - width: 150px -.w200 - width: 200px -.w100p - width: 100% -.m100 - max-width: 100px -.m200 - max-width: 200px -.m300 - max-width: 300px -.m400 - max-width: 400px -.m500 - max-width: 500px -.m600 - max-width: 600px -.m700 - max-width: 700px -.m800 - max-width: 800px -.m900 - max-width: 900px -.m1000 - max-width: 1000px -.m1200 - max-width: 1200px -.m100p - max-width: 100% - -/** - *height - */ -.h80 - height: 80px -.h100 - height: 100px -.h200 - height: 200px -.h300 - height: 300px -.h400 - height: 400px -.h500 - height: 500px -.h600 - height: 600px -.h700 - height: 700px -.h800 - height: 800px -.hfull - height: 100% - -/** - * positions - */ -.pos-rel - position: relative -.pos-stc - position: static -.pos-abt - position: absolute -.pos-fix - position: fixed -/** - * Indexing - */ -.z-1 - z-index: 1 -.z-2 - z-index: 2 -.z-3 - z-index: 3 -.z-4 - z-index: 4 -.z-5 - z-index: 5 -.z-10 - z-index: 10 -.z-20 - z-index: 20 -.z-100 - z-index: 100 - - -/** - *font size - */ -.text-heading - font-size: 56px - -.text-xxl - font-size: 40px - -.text-xl - font-size: 32px - -.text-lg - font-size: 24px - -.text-regular - font-size: 15px - -.text-sm - font-size: 12px - -.text-xsm - font-size: 10px - -.text-xs - font-size: 8px - -/** - *font weight - */ -.text-thin - font-weight: 300 -.text-normal - font-weight: normal -.text-demi - font-weight: 500 -.text-bold - font-weight: bold -.text-bolder - font-weight: bolder - -/** - *borders - */ -.b-t - border-top: 1px solid $secondary-color -.b-r - border-right: 1px solid $secondary-color -.b-b - border-bottom: 1px solid $secondary-color -.b-l - border-left: 1px solid $secondary-color -.b-n - border: none !important - - -/** - * Radius - */ -.radius6 - border-radius: 6px -.radius100p - border-radius: 100% - - -/** - *padding - */ -.padder - padding: 0 15px -.p-mini - padding: 5px -.p-small - padding: 10px -.p-large - padding: 20px -.p-xlarge - padding: 30px - -.p-t-mini - padding-top: 5px -.p-t-small - padding-top: 10px -.p-t-large - padding-top: 20px -.p-t-xlarge - padding-top: 30px - -.p-b-mini - padding-bottom: 5px -.p-b-small - padding-bottom: 10px -.p-b-large - padding-bottom: 20px -.p-b-xlarge - padding-bottom: 30px - -.p-l-small - padding-left: 10px -.p-l-large - padding-left: 20px -.p-l-xlarge - padding-left: 30px -.p-r-small - padding-right: 10px -.p-r-large - padding-right: 20px -.p-r-xlarge - padding-right: 30px -.p-l-xxlarge - padding-left: 60px -.p-r-xxlarge - padding-right: 60px - -.no-padding - padding: 0 !important -.no-p-l - padding-left: 0 -.no-p-r - padding-right: 0 -.no-p-t - padding-top: 0 !important -.no-p-b - padding-bottom: 0 - -/** - *margin - */ -.m-l-r-auto - margin-left: auto - margin-right: auto -.m-l - margin-left: 15px -.m-l-none - margin-left: 0 -.m-l-mini - margin-left: 5px -.m-l-small - margin-left: 10px -.m-l-large - margin-left: 20px -.m-l-n - margin-left: -15px -.m-l-n-mini - margin-left: -5px -.m-l-n-small - margin-left: -10px -.m-l-n-large - margin-left: -20px -.m-t - margin-top: 15px -.m-t-none - margin-top: 0 -.m-t-mini - margin-top: 5px -.m-t-small - margin-top: 10px -.m-t-large - margin-top: 20px -.m-t-n - margin-top: -15px -.m-t-n-xmini - margin-top: -1px -.m-t-n-mini - margin-top: -5px -.m-t-n-small - margin-top: -10px -.m-t-n-large - margin-top: -20px -.m-r - margin-right: 15px -.m-r-none - margin-right: 0 -.m-r-mini - margin-right: 5px -.m-r-small - margin-right: 10px -.m-r-large - margin-right: 20px -.m-r-n - margin-right: -15px -.m-r-n-mini - margin-right: -5px -.m-r-n-small - margin-right: -10px -.m-r-n-large - margin-right: -20px -.m-b - margin-bottom: 15px -.m-b-none - margin-bottom: 0 -.m-b-mini - margin-bottom: 5px -.m-b-small - margin-bottom: 10px -.m-b-large - margin-bottom: 20px -.m-b-xlarge - margin-bottom: 30px -.m-b-n - margin-bottom: -15px -.m-b-n-mini - margin-bottom: -5px -.m-b-n-small - margin-bottom: -10px -.m-b-n-large - margin-bottom: -20px - -.no-margin - margin: 0 !important - -/** - * text - */ -.text-center - text-align: center -.text-justify - text-align: justify -.text-left - text-align: left -.text-right - text-align: right -.vertical-align-middle - display: inline-block !important - vertical-align: middle !important -.vertical-align-bottom - display: inline-block !important - vertical-align: bottom !important -.text-ellipsis - white-space: nowrap - overflow: hidden - max-width: 100% - text-overflow: ellipsis -.text-break-word - word-wrap: break-word -.nowrap - word-wrap: nowrap - white-space: nowrap -.lower-case - text-transform: lowercase -.upper-case - text-transform: uppercase -.hidden-text - text-indent: -9999px - color: transparent - -/** - * Line height - */ -.line-h-regular - line-height: 1.5 - -.line-h-small - line-height: 1.2 - -/** - *line - */ -.line - width: 100% - height: 1px - margin: 10px 0 - font-size: 0 - overflow: hidden - border-width: 0 - background-color: $secondary-color -.line-dashed - border-style: dashed - background: transparent -.headline - border-bottom: 5px solid $black - margin-top: 0 - line-height: 45px - -.no-line - border-width: 0 -.no-border - border-color: transparent !important -.no-radius - border-radius: 0 -.block - display: block -.inline - display: inline-block -.pull-left - float: left -.pull-right - float: right -.pull-none - float: none -.pull-in - margin-right: -15px - margin-left: -15px -.line-v - border-left: 1px solid #dddddd - padding-left: 20px -.line-v-right - border-right: 1px solid #dddddd - padding-right: 20px - -/** - * Flexbox - */ -.flex-container - display: flex - -.flex-dir-col - flex-flow: column wrap - -.flex-dir-row - flex-flow: row wrap - -.flex-space-around - justify-content: space-around - -.flex-space-between - justify-content: space-between - -.flex-align-center - align-items: center - -.flex-it-4 - flex: 0 1 25% - // IE10 -_:-ms-input-placeholder, :root .flex-it-4 - -ms-flex-preferred-size: 21% !important - -.flex-it-3 - flex: 0 1 33% - // IE10 -_:-ms-input-placeholder, :root .flex-it-3 - -ms-flex-preferred-size: 29% !important - -.flex-it-2 - flex: 0 1 50% - // IE10 -_:-ms-input-placeholder, :root .flex-it-2 - -ms-flex-preferred-size: 48% !important - - @supports ( not ( mix-blend-mode: luminosity)) - flex: 0 1 49.95% - - -.flex-it-1 - flex: 0 1 100% - -/** - * others - */ -.unscroll - overflow: hidden !important - -.clickable - cursor: pointer - -.rotate-45 - transform: rotate(45deg) - -.content-box - box-sizing: content-box - -.faded - opacity: .3 - -.op0 - opacity: 0 - -.line-through - text-decoration: line-through - -.no-decoration - text-decoration: none - - -.v-center - position: absolute - top: 50% - transform: translateY(-50%) - -.h-center - position: absolute - left: 50% - transform: translateX(-50%) - -.vh-center - position: absolute - top: 50% - left: 50% - transform: translate(-50%,-50%) - - -/** - * Hidden utilities - */ -.hidden-sm - @media (max-width: $screen-sm) - display: none !important - -.visible-sm - @media (max-width: $screen-sm) - display: block !important - -.hidden - visibility: hidden - -/** - * CSS for and - */ -sub, sup - font-size: 50% - line-height: 0 - position: relative - vertical-align: baseline - -sup - top: -0.7em - -sub - bottom: -0.25em - -[ng\:cloak], [ng-cloak], .ng-cloak - display: none !important - - - -/** - * Lists - */ -.list-none - list-style: none - -/** - * Clearfix - */ -.cf:before, -.cf:after - content: " " - display: table - - -.cf:after - clear: both - -/** - * For IE 6/7 only - * Include this rule to trigger hasLayout and contain floats. - */ -.cf - *zoom: 1 - -.elevation0 - box-shadow: 0 5px 15px 0 rgba(112, 128, 175, 0.2) - -.elevation1 - box-shadow: 0 10px 40px 0 rgba($bunting,0.07), 0 2px 9px 0 rgba($bunting,0.06) - -.elevation2 - box-shadow: 0 16px 32px 0 rgba($bunting, 0.2) - -// Placeholders -%elevation1 - box-shadow: 0 10px 40px 0 rgba($bunting,0.07), 0 2px 9px 0 rgba($bunting,0.06) - diff --git a/docgen/src/stylesheets/style.sass b/docgen/src/stylesheets/style.sass deleted file mode 100644 index ae1fd6d0..00000000 --- a/docgen/src/stylesheets/style.sass +++ /dev/null @@ -1,32 +0,0 @@ -@charset 'utf-8' - -@import 'node_modules/algolia-frontend-components/dist/_communityHeader' - - -@import 'modules/base' -@import 'modules/mixins' - -@import 'partials/bootstrap' -@import 'partials/colors' -@import 'partials/helpers' -@import 'partials/fonts' - -@import 'components/navigation' -@import 'components/hero' -@import 'components/buttons' -@import 'components/cards' -@import 'components/footer' -@import 'components/medias' -@import 'components/documentation' -@import 'components/sidebar' -@import 'components/examples' -@import 'components/code-highlighting' - - -@import 'pages/home' - - -* - box-sizing: border-box - -webkit-font-smoothing: antialiased - -moz-osx-font-smoothing: grayscale diff --git a/docgen/src/stylesheets/vendors/bootstrap/_alerts.scss b/docgen/src/stylesheets/vendors/bootstrap/_alerts.scss deleted file mode 100755 index e45de830..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_alerts.scss +++ /dev/null @@ -1,68 +0,0 @@ -// -// Alerts -// -------------------------------------------------- - - -// Base styles -// ------------------------- - -.alert { - padding: $alert-padding; - margin-bottom: $line-height-computed; - border: 1px solid transparent; - border-radius: $alert-border-radius; - - // Headings for larger alerts - h4 { - margin-top: 0; - // Specified for the h4 to prevent conflicts of changing $headings-color - color: inherit; - } - // Provide class for links that match alerts - .alert-link { - font-weight: $alert-link-font-weight; - } - - // Improve alignment and spacing of inner content - > p, - > ul { - margin-bottom: 0; - } - > p + p { - margin-top: 5px; - } -} - -// Dismissible alerts -// -// Expand the right padding and account for the close button's positioning. - -.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. -.alert-dismissible { - padding-right: ($alert-padding + 20); - - // Adjust close link position - .close { - position: relative; - top: -2px; - right: -21px; - color: inherit; - } -} - -// Alternate styles -// -// Generate contextual modifier classes for colorizing the alert. - -.alert-success { - @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text); -} -.alert-info { - @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text); -} -.alert-warning { - @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text); -} -.alert-danger { - @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); -} diff --git a/docgen/src/stylesheets/vendors/bootstrap/_badges.scss b/docgen/src/stylesheets/vendors/bootstrap/_badges.scss deleted file mode 100755 index c913f144..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_badges.scss +++ /dev/null @@ -1,63 +0,0 @@ -// -// Badges -// -------------------------------------------------- - - -// Base class -.badge { - display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: $font-size-small; - font-weight: $badge-font-weight; - color: $badge-color; - line-height: $badge-line-height; - vertical-align: baseline; - white-space: nowrap; - text-align: center; - background-color: $badge-bg; - border-radius: $badge-border-radius; - - // Empty badges collapse automatically (not available in IE8) - &:empty { - display: none; - } - - // Quick fix for badges in buttons - .btn & { - position: relative; - top: -1px; - } - .btn-xs & { - top: 0; - padding: 1px 5px; - } - - // [converter] extracted a& to a.badge - - // Account for badges in navs - .list-group-item.active > &, - .nav-pills > .active > a > & { - color: $badge-active-color; - background-color: $badge-active-bg; - } - .list-group-item > & { - float: right; - } - .list-group-item > & + & { - margin-right: 5px; - } - .nav-pills > li > a > & { - margin-left: 3px; - } -} - -// Hover state, but only for links -a.badge { - &:hover, - &:focus { - color: $badge-link-hover-color; - text-decoration: none; - cursor: pointer; - } -} diff --git a/docgen/src/stylesheets/vendors/bootstrap/_breadcrumbs.scss b/docgen/src/stylesheets/vendors/bootstrap/_breadcrumbs.scss deleted file mode 100755 index 3641e333..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_breadcrumbs.scss +++ /dev/null @@ -1,26 +0,0 @@ -// -// Breadcrumbs -// -------------------------------------------------- - - -.breadcrumb { - padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal; - margin-bottom: $line-height-computed; - list-style: none; - background-color: $breadcrumb-bg; - border-radius: $border-radius-base; - - > li { - display: inline-block; - - + li:before { - content: "#{$breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space - padding: 0 5px; - color: $breadcrumb-color; - } - } - - > .active { - color: $breadcrumb-active-color; - } -} diff --git a/docgen/src/stylesheets/vendors/bootstrap/_button-groups.scss b/docgen/src/stylesheets/vendors/bootstrap/_button-groups.scss deleted file mode 100755 index e761daaf..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_button-groups.scss +++ /dev/null @@ -1,243 +0,0 @@ -// -// Button groups -// -------------------------------------------------- - -// Make the div behave like a button -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-block; - vertical-align: middle; // match .btn alignment given font-size hack above - > .btn { - position: relative; - float: left; - // Bring the "active" button to the front - &:hover, - &:focus, - &:active, - &.active { - z-index: 2; - } - } -} - -// Prevent double borders when buttons are next to each other -.btn-group { - .btn + .btn, - .btn + .btn-group, - .btn-group + .btn, - .btn-group + .btn-group { - margin-left: -1px; - } -} - -// Optional: Group multiple button groups together for a toolbar -.btn-toolbar { - margin-left: -5px; // Offset the first child's margin - @include clearfix; - - .btn-group, - .input-group { - float: left; - } - > .btn, - > .btn-group, - > .input-group { - margin-left: 5px; - } -} - -.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { - border-radius: 0; -} - -// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match -.btn-group > .btn:first-child { - margin-left: 0; - &:not(:last-child):not(.dropdown-toggle) { - @include border-right-radius(0); - } -} -// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it -.btn-group > .btn:last-child:not(:first-child), -.btn-group > .dropdown-toggle:not(:first-child) { - @include border-left-radius(0); -} - -// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) -.btn-group > .btn-group { - float: left; -} -.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group > .btn-group:first-child { - > .btn:last-child, - > .dropdown-toggle { - @include border-right-radius(0); - } -} -.btn-group > .btn-group:last-child > .btn:first-child { - @include border-left-radius(0); -} - -// On active and open, don't show outline -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} - - -// Sizing -// -// Remix the default button sizing classes into new ones for easier manipulation. - -.btn-group-xs > .btn { @extend .btn-xs; } -.btn-group-sm > .btn { @extend .btn-sm; } -.btn-group-lg > .btn { @extend .btn-lg; } - - -// Split button dropdowns -// ---------------------- - -// Give the line between buttons some depth -.btn-group > .btn + .dropdown-toggle { - padding-left: 8px; - padding-right: 8px; -} -.btn-group > .btn-lg + .dropdown-toggle { - padding-left: 12px; - padding-right: 12px; -} - -// The clickable button for toggling the menu -// Remove the gradient and set the same inset shadow as the :active state -.btn-group.open .dropdown-toggle { - @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); - - // Show no shadow for `.btn-link` since it has no other button styles. - &.btn-link { - @include box-shadow(none); - } -} - - -// Reposition the caret -.btn .caret { - margin-left: 0; -} -// Carets in other button sizes -.btn-lg .caret { - border-width: $caret-width-large $caret-width-large 0; - border-bottom-width: 0; -} -// Upside down carets for .dropup -.dropup .btn-lg .caret { - border-width: 0 $caret-width-large $caret-width-large; -} - - -// Vertical button groups -// ---------------------- - -.btn-group-vertical { - > .btn, - > .btn-group, - > .btn-group > .btn { - display: block; - float: none; - width: 100%; - max-width: 100%; - } - - // Clear floats so dropdown menus can be properly placed - > .btn-group { - @include clearfix; - > .btn { - float: none; - } - } - - > .btn + .btn, - > .btn + .btn-group, - > .btn-group + .btn, - > .btn-group + .btn-group { - margin-top: -1px; - margin-left: 0; - } -} - -.btn-group-vertical > .btn { - &:not(:first-child):not(:last-child) { - border-radius: 0; - } - &:first-child:not(:last-child) { - border-top-right-radius: $border-radius-base; - @include border-bottom-radius(0); - } - &:last-child:not(:first-child) { - border-bottom-left-radius: $border-radius-base; - @include border-top-radius(0); - } -} -.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group-vertical > .btn-group:first-child:not(:last-child) { - > .btn:last-child, - > .dropdown-toggle { - @include border-bottom-radius(0); - } -} -.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { - @include border-top-radius(0); -} - - -// Justified button groups -// ---------------------- - -.btn-group-justified { - display: table; - width: 100%; - table-layout: fixed; - border-collapse: separate; - > .btn, - > .btn-group { - float: none; - display: table-cell; - width: 1%; - } - > .btn-group .btn { - width: 100%; - } - - > .btn-group .dropdown-menu { - left: auto; - } -} - - -// Checkbox and radio options -// -// In order to support the browser's form validation feedback, powered by the -// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use -// `display: none;` or `visibility: hidden;` as that also hides the popover. -// Simply visually hiding the inputs via `opacity` would leave them clickable in -// certain cases which is prevented by using `clip` and `pointer-events`. -// This way, we ensure a DOM element is visible to position the popover from. -// -// See https://github.com/twbs/bootstrap/pull/12794 and -// https://github.com/twbs/bootstrap/pull/14559 for more information. - -[data-toggle="buttons"] { - > .btn, - > .btn-group > .btn { - input[type="radio"], - input[type="checkbox"] { - position: absolute; - clip: rect(0,0,0,0); - pointer-events: none; - } - } -} diff --git a/docgen/src/stylesheets/vendors/bootstrap/_buttons.scss b/docgen/src/stylesheets/vendors/bootstrap/_buttons.scss deleted file mode 100755 index 37bf259e..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_buttons.scss +++ /dev/null @@ -1,160 +0,0 @@ -// -// Buttons -// -------------------------------------------------- - - -// Base styles -// -------------------------------------------------- - -.btn { - display: inline-block; - margin-bottom: 0; // For input.btn - font-weight: $btn-font-weight; - text-align: center; - vertical-align: middle; - touch-action: manipulation; - cursor: pointer; - background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 - border: 1px solid transparent; - white-space: nowrap; - @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $border-radius-base); - @include user-select(none); - - &, - &:active, - &.active { - &:focus, - &.focus { - @include tab-focus; - } - } - - &:hover, - &:focus, - &.focus { - color: $btn-default-color; - text-decoration: none; - } - - &:active, - &.active { - outline: 0; - background-image: none; - @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); - } - - &.disabled, - &[disabled], - fieldset[disabled] & { - cursor: $cursor-disabled; - pointer-events: none; // Future-proof disabling of clicks - @include opacity(.65); - @include box-shadow(none); - } -} - - -// Alternate buttons -// -------------------------------------------------- - -.btn-default { - @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border); -} -.btn-primary { - @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border); -} -// Success appears as green -.btn-success { - @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border); -} -// Info appears as blue-green -.btn-info { - @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border); -} -// Warning appears as orange -.btn-warning { - @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border); -} -// Danger and error appear as red -.btn-danger { - @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border); -} - - -// Link buttons -// ------------------------- - -// Make a button look and behave like a link -.btn-link { - color: $link-color; - font-weight: normal; - border-radius: 0; - - &, - &:active, - &.active, - &[disabled], - fieldset[disabled] & { - background-color: transparent; - @include box-shadow(none); - } - &, - &:hover, - &:focus, - &:active { - border-color: transparent; - } - &:hover, - &:focus { - color: $link-hover-color; - text-decoration: underline; - background-color: transparent; - } - &[disabled], - fieldset[disabled] & { - &:hover, - &:focus { - color: $btn-link-disabled-color; - text-decoration: none; - } - } -} - - -// Button Sizes -// -------------------------------------------------- - -.btn-lg { - // line-height: ensure even-numbered height of button next to large input - @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large); -} -.btn-sm { - // line-height: ensure proper height of button next to small input - @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small); -} -.btn-xs { - @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $border-radius-small); -} - - -// Block button -// -------------------------------------------------- - -.btn-block { - display: block; - width: 100%; -} - -// Vertically space out multiple block buttons -.btn-block + .btn-block { - margin-top: 5px; -} - -// Specificity overrides -input[type="submit"], -input[type="reset"], -input[type="button"] { - &.btn-block { - width: 100%; - } -} diff --git a/docgen/src/stylesheets/vendors/bootstrap/_carousel.scss b/docgen/src/stylesheets/vendors/bootstrap/_carousel.scss deleted file mode 100755 index 49db83fc..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_carousel.scss +++ /dev/null @@ -1,267 +0,0 @@ -// -// Carousel -// -------------------------------------------------- - - -// Wrapper for the slide container and indicators -.carousel { - position: relative; -} - -.carousel-inner { - position: relative; - overflow: hidden; - width: 100%; - - > .item { - display: none; - position: relative; - @include transition(.6s ease-in-out left); - - // Account for jankitude on images - > img, - > a > img { - @include img-responsive; - line-height: 1; - } - - // WebKit CSS3 transforms for supported devices - @media all and (transform-3d), (-webkit-transform-3d) { - transition: transform .6s ease-in-out; - backface-visibility: hidden; - perspective: 1000; - - &.next, - &.active.right { - transform: translate3d(100%, 0, 0); - left: 0; - } - &.prev, - &.active.left { - transform: translate3d(-100%, 0, 0); - left: 0; - } - &.next.left, - &.prev.right, - &.active { - transform: translate3d(0, 0, 0); - left: 0; - } - } - } - - > .active, - > .next, - > .prev { - display: block; - } - - > .active { - left: 0; - } - - > .next, - > .prev { - position: absolute; - top: 0; - width: 100%; - } - - > .next { - left: 100%; - } - > .prev { - left: -100%; - } - > .next.left, - > .prev.right { - left: 0; - } - - > .active.left { - left: -100%; - } - > .active.right { - left: 100%; - } - -} - -// Left/right controls for nav -// --------------------------- - -.carousel-control { - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: $carousel-control-width; - @include opacity($carousel-control-opacity); - font-size: $carousel-control-font-size; - color: $carousel-control-color; - text-align: center; - text-shadow: $carousel-text-shadow; - // We can't have this transition here because WebKit cancels the carousel - // animation if you trip this while in the middle of another animation. - - // Set gradients for backgrounds - &.left { - @include gradient-horizontal($start-color: rgba(0,0,0,.5), $end-color: rgba(0,0,0,.0001)); - } - &.right { - left: auto; - right: 0; - @include gradient-horizontal($start-color: rgba(0,0,0,.0001), $end-color: rgba(0,0,0,.5)); - } - - // Hover/focus state - &:hover, - &:focus { - outline: 0; - color: $carousel-control-color; - text-decoration: none; - @include opacity(.9); - } - - // Toggles - .icon-prev, - .icon-next, - .glyphicon-chevron-left, - .glyphicon-chevron-right { - position: absolute; - top: 50%; - z-index: 5; - display: inline-block; - } - .icon-prev, - .glyphicon-chevron-left { - left: 50%; - margin-left: -10px; - } - .icon-next, - .glyphicon-chevron-right { - right: 50%; - margin-right: -10px; - } - .icon-prev, - .icon-next { - width: 20px; - height: 20px; - margin-top: -10px; - font-family: serif; - } - - - .icon-prev { - &:before { - content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) - } - } - .icon-next { - &:before { - content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) - } - } -} - -// Optional indicator pips -// -// Add an unordered list with the following class and add a list item for each -// slide your carousel holds. - -.carousel-indicators { - position: absolute; - bottom: 10px; - left: 50%; - z-index: 15; - width: 60%; - margin-left: -30%; - padding-left: 0; - list-style: none; - text-align: center; - - li { - display: inline-block; - width: 10px; - height: 10px; - margin: 1px; - text-indent: -999px; - border: 1px solid $carousel-indicator-border-color; - border-radius: 10px; - cursor: pointer; - - // IE8-9 hack for event handling - // - // Internet Explorer 8-9 does not support clicks on elements without a set - // `background-color`. We cannot use `filter` since that's not viewed as a - // background color by the browser. Thus, a hack is needed. - // - // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we - // set alpha transparency for the best results possible. - background-color: #000 \9; // IE8 - background-color: rgba(0,0,0,0); // IE9 - } - .active { - margin: 0; - width: 12px; - height: 12px; - background-color: $carousel-indicator-active-bg; - } -} - -// Optional captions -// ----------------------------- -// Hidden by default for smaller viewports -.carousel-caption { - position: absolute; - left: 15%; - right: 15%; - bottom: 20px; - z-index: 10; - padding-top: 20px; - padding-bottom: 20px; - color: $carousel-caption-color; - text-align: center; - text-shadow: $carousel-text-shadow; - & .btn { - text-shadow: none; // No shadow for button elements in carousel-caption - } -} - - -// Scale up controls for tablets and up -@media screen and (min-width: $screen-sm-min) { - - // Scale up the controls a smidge - .carousel-control { - .glyphicon-chevron-left, - .glyphicon-chevron-right, - .icon-prev, - .icon-next { - width: 30px; - height: 30px; - margin-top: -15px; - font-size: 30px; - } - .glyphicon-chevron-left, - .icon-prev { - margin-left: -15px; - } - .glyphicon-chevron-right, - .icon-next { - margin-right: -15px; - } - } - - // Show and left align the captions - .carousel-caption { - left: 20%; - right: 20%; - padding-bottom: 30px; - } - - // Move up the indicators - .carousel-indicators { - bottom: 20px; - } -} diff --git a/docgen/src/stylesheets/vendors/bootstrap/_close.scss b/docgen/src/stylesheets/vendors/bootstrap/_close.scss deleted file mode 100755 index 62ce30fa..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_close.scss +++ /dev/null @@ -1,35 +0,0 @@ -// -// Close icons -// -------------------------------------------------- - - -.close { - float: right; - font-size: ($font-size-base * 1.5); - font-weight: $close-font-weight; - line-height: 1; - color: $close-color; - text-shadow: $close-text-shadow; - @include opacity(.2); - - &:hover, - &:focus { - color: $close-color; - text-decoration: none; - cursor: pointer; - @include opacity(.5); - } - - // [converter] extracted button& to button.close -} - -// Additional properties for button version -// iOS requires the button element instead of an anchor tag. -// If you want the anchor version, it requires `href="#"`. -button.close { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} diff --git a/docgen/src/stylesheets/vendors/bootstrap/_code.scss b/docgen/src/stylesheets/vendors/bootstrap/_code.scss deleted file mode 100755 index caa5f063..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_code.scss +++ /dev/null @@ -1,69 +0,0 @@ -// -// Code (inline and block) -// -------------------------------------------------- - - -// Inline and block code styles -code, -kbd, -pre, -samp { - font-family: $font-family-monospace; -} - -// Inline code -code { - padding: 2px 4px; - font-size: 90%; - color: $code-color; - background-color: $code-bg; - border-radius: $border-radius-base; -} - -// User input typically entered via keyboard -kbd { - padding: 2px 4px; - font-size: 90%; - color: $kbd-color; - background-color: $kbd-bg; - border-radius: $border-radius-small; - box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); - - kbd { - padding: 0; - font-size: 100%; - font-weight: bold; - box-shadow: none; - } -} - -// Blocks of code -pre { - display: block; - padding: (($line-height-computed - 1) / 2); - margin: 0 0 ($line-height-computed / 2); - font-size: ($font-size-base - 1); // 14px to 13px - line-height: $line-height-base; - word-break: break-all; - word-wrap: break-word; - color: $pre-color; - background-color: $pre-bg; - border: 1px solid $pre-border-color; - border-radius: $border-radius-base; - - // Account for some code outputs that place code tags in pre tags - code { - padding: 0; - font-size: inherit; - color: inherit; - white-space: pre-wrap; - background-color: transparent; - border-radius: 0; - } -} - -// Enable scrollable blocks of code -.pre-scrollable { - max-height: $pre-scrollable-max-height; - overflow-y: scroll; -} diff --git a/docgen/src/stylesheets/vendors/bootstrap/_component-animations.scss b/docgen/src/stylesheets/vendors/bootstrap/_component-animations.scss deleted file mode 100755 index 1f76b8c0..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_component-animations.scss +++ /dev/null @@ -1,38 +0,0 @@ -// -// Component animations -// -------------------------------------------------- - -// Heads up! -// -// We don't use the `.opacity()` mixin here since it causes a bug with text -// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. - -.fade { - opacity: 0; - @include transition(opacity .15s linear); - &.in { - opacity: 1; - } -} - -.collapse { - display: none; - visibility: hidden; - - &.in { display: block; visibility: visible; } - // [converter] extracted tr&.in to tr.collapse.in - // [converter] extracted tbody&.in to tbody.collapse.in -} - -tr.collapse.in { display: table-row; } - -tbody.collapse.in { display: table-row-group; } - -.collapsing { - position: relative; - height: 0; - overflow: hidden; - @include transition-property(height, visibility); - @include transition-duration(.35s); - @include transition-timing-function(ease); -} diff --git a/docgen/src/stylesheets/vendors/bootstrap/_dropdowns.scss b/docgen/src/stylesheets/vendors/bootstrap/_dropdowns.scss deleted file mode 100755 index c7256e10..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_dropdowns.scss +++ /dev/null @@ -1,213 +0,0 @@ -// -// Dropdown menus -// -------------------------------------------------- - - -// Dropdown arrow/caret -.caret { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: $caret-width-base solid; - border-right: $caret-width-base solid transparent; - border-left: $caret-width-base solid transparent; -} - -// The dropdown wrapper (div) -.dropdown { - position: relative; -} - -// Prevent the focus on the dropdown toggle when closing dropdowns -.dropdown-toggle:focus { - outline: 0; -} - -// The dropdown menu (ul) -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: $zindex-dropdown; - display: none; // none by default, but block on "open" of the menu - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; // override default ul - list-style: none; - font-size: $font-size-base; - text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) - background-color: $dropdown-bg; - border: 1px solid $dropdown-fallback-border; // IE8 fallback - border: 1px solid $dropdown-border; - border-radius: $border-radius-base; - @include box-shadow(0 6px 12px rgba(0,0,0,.175)); - background-clip: padding-box; - - // Aligns the dropdown menu to right - // - // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` - &.pull-right { - right: 0; - left: auto; - } - - // Dividers (basically an hr) within the dropdown - .divider { - @include nav-divider($dropdown-divider-bg); - } - - // Links within the dropdown menu - > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: $line-height-base; - color: $dropdown-link-color; - white-space: nowrap; // prevent links from randomly breaking onto new lines - } -} - -// Hover/Focus state -.dropdown-menu > li > a { - &:hover, - &:focus { - text-decoration: none; - color: $dropdown-link-hover-color; - background-color: $dropdown-link-hover-bg; - } -} - -// Active state -.dropdown-menu > .active > a { - &, - &:hover, - &:focus { - color: $dropdown-link-active-color; - text-decoration: none; - outline: 0; - background-color: $dropdown-link-active-bg; - } -} - -// Disabled state -// -// Gray out text and ensure the hover/focus state remains gray - -.dropdown-menu > .disabled > a { - &, - &:hover, - &:focus { - color: $dropdown-link-disabled-color; - } - - // Nuke hover/focus effects - &:hover, - &:focus { - text-decoration: none; - background-color: transparent; - background-image: none; // Remove CSS gradient - @include reset-filter; - cursor: $cursor-disabled; - } -} - -// Open state for the dropdown -.open { - // Show the menu - > .dropdown-menu { - display: block; - } - - // Remove the outline when :focus is triggered - > a { - outline: 0; - } -} - -// Menu positioning -// -// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown -// menu with the parent. -.dropdown-menu-right { - left: auto; // Reset the default from `.dropdown-menu` - right: 0; -} -// With v3, we enabled auto-flipping if you have a dropdown within a right -// aligned nav component. To enable the undoing of that, we provide an override -// to restore the default dropdown menu alignment. -// -// This is only for left-aligning a dropdown menu within a `.navbar-right` or -// `.pull-right` nav component. -.dropdown-menu-left { - left: 0; - right: auto; -} - -// Dropdown section headers -.dropdown-header { - display: block; - padding: 3px 20px; - font-size: $font-size-small; - line-height: $line-height-base; - color: $dropdown-header-color; - white-space: nowrap; // as with > li > a -} - -// Backdrop to catch body clicks on mobile, etc. -.dropdown-backdrop { - position: fixed; - left: 0; - right: 0; - bottom: 0; - top: 0; - z-index: ($zindex-dropdown - 10); -} - -// Right aligned dropdowns -.pull-right > .dropdown-menu { - right: 0; - left: auto; -} - -// Allow for dropdowns to go bottom up (aka, dropup-menu) -// -// Just add .dropup after the standard .dropdown class and you're set, bro. -// TODO: abstract this so that the navbar fixed styles are not placed here? - -.dropup, -.navbar-fixed-bottom .dropdown { - // Reverse the caret - .caret { - border-top: 0; - border-bottom: $caret-width-base solid; - content: ""; - } - // Different positioning for bottom up menu - .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 1px; - } -} - - -// Component alignment -// -// Reiterate per navbar.less and the modified component alignment there. - -@media (min-width: $grid-float-breakpoint) { - .navbar-right { - .dropdown-menu { - right: 0; left: auto; - } - // Necessary for overrides of the default right aligned menu. - // Will remove come v4 in all likelihood. - .dropdown-menu-left { - left: 0; right: auto; - } - } -} diff --git a/docgen/src/stylesheets/vendors/bootstrap/_forms.scss b/docgen/src/stylesheets/vendors/bootstrap/_forms.scss deleted file mode 100755 index 6449ac8a..00000000 --- a/docgen/src/stylesheets/vendors/bootstrap/_forms.scss +++ /dev/null @@ -1,550 +0,0 @@ -// -// Forms -// -------------------------------------------------- - - -// Normalize non-controls -// -// Restyle and baseline non-control form elements. - -fieldset { - padding: 0; - margin: 0; - border: 0; - // Chrome and Firefox set a `min-width: min-content;` on fieldsets, - // so we reset that to ensure it behaves more like a standard block element. - // See https://github.com/twbs/bootstrap/issues/12359. - min-width: 0; -} - -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: $line-height-computed; - font-size: ($font-size-base * 1.5); - line-height: inherit; - color: $legend-color; - border: 0; - border-bottom: 1px solid $legend-border-color; -} - -label { - display: inline-block; - max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141) - margin-bottom: 5px; - font-weight: bold; -} - - -// Normalize form controls -// -// While most of our form styles require extra classes, some basic normalization -// is required to ensure optimum display with or without those classes to better -// address browser inconsistencies. - -// Override content-box in Normalize (* isn't specific enough) -input[type="search"] { - @include box-sizing(border-box); -} - -// Position radios and checkboxes better -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; // IE8-9 - line-height: normal; -} - -// Set the height of file controls to match text inputs -input[type="file"] { - display: block; -} - -// Make range inputs behave like textual form controls -input[type="range"] { - display: block; - width: 100%; -} - -// Make multiple select elements height not fixed -select[multiple], -select[size] { - height: auto; -} - -// Focus for file, radio, and checkbox -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - @include tab-focus; -} - -// Adjust output element -output { - display: block; - padding-top: ($padding-base-vertical + 1); - font-size: $font-size-base; - line-height: $line-height-base; - color: $input-color; -} - - -// Common form controls -// -// Shared size and type resets for form controls. Apply `.form-control` to any -// of the following form controls: -// -// select -// textarea -// input[type="text"] -// input[type="password"] -// input[type="datetime"] -// input[type="datetime-local"] -// input[type="date"] -// input[type="month"] -// input[type="time"] -// input[type="week"] -// input[type="number"] -// input[type="email"] -// input[type="url"] -// input[type="search"] -// input[type="tel"] -// input[type="color"] - -.form-control { - display: block; - width: 100%; - height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) - padding: $padding-base-vertical $padding-base-horizontal; - font-size: $font-size-base; - line-height: $line-height-base; - color: $input-color; - background-color: $input-bg; - background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 - border: 1px solid $input-border; - border-radius: $input-border-radius; - @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); - @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); - - // Customize the `:focus` state to imitate native WebKit styles. - @include form-control-focus; - - // Placeholder - @include placeholder; - - // Disabled and read-only inputs - // - // HTML5 says that controls under a fieldset > legend:first-child won't be - // disabled if the fieldset is disabled. Due to implementation difficulty, we - // don't honor that edge case; we style them as disabled anyway. - &[disabled], - &[readonly], - fieldset[disabled] & { - cursor: $cursor-disabled; - background-color: $input-bg-disabled; - opacity: 1; // iOS fix for unreadable disabled content - } - - // [converter] extracted textarea& to textarea.form-control -} - -// Reset height for `textarea`s -textarea.form-control { - height: auto; -} - - -// Search inputs in iOS -// -// This overrides the extra rounded corners on search inputs in iOS so that our -// `.form-control` class can properly style them. Note that this cannot simply -// be added to `.form-control` as it's not specific enough. For details, see -// https://github.com/twbs/bootstrap/issues/11586. - -input[type="search"] { - -webkit-appearance: none; -} - - -// Special styles for iOS temporal inputs -// -// In Mobile Safari, setting `display: block` on temporal inputs causes the -// text within the input to become vertically misaligned. As a workaround, we -// set a pixel line-height that matches the given height of the input, but only -// for Safari. - -@media screen and (-webkit-min-device-pixel-ratio: 0) { - input[type="date"], - input[type="time"], - input[type="datetime-local"], - input[type="month"] { - line-height: $input-height-base; - } - input[type="date"].input-sm, - input[type="time"].input-sm, - input[type="datetime-local"].input-sm, - input[type="month"].input-sm { - line-height: $input-height-small; - } - input[type="date"].input-lg, - input[type="time"].input-lg, - input[type="datetime-local"].input-lg, - input[type="month"].input-lg { - line-height: $input-height-large; - } -} - - -// Form groups -// -// Designed to help with the organization and spacing of vertical forms. For -// horizontal forms, use the predefined grid classes. - -.form-group { - margin-bottom: 15px; -} - - -// Checkboxes and radios -// -// Indent the labels to position radios/checkboxes as hanging controls. - -.radio, -.checkbox { - position: relative; - display: block; - margin-top: 10px; - margin-bottom: 10px; - - label { - min-height: $line-height-computed; // Ensure the input doesn't jump when there is no text - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - cursor: pointer; - } -} -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - position: absolute; - margin-left: -20px; - margin-top: 4px \9; -} - -.radio + .radio, -.checkbox + .checkbox { - margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing -} - -// Radios and checkboxes on same line -.radio-inline, -.checkbox-inline { - display: inline-block; - padding-left: 20px; - margin-bottom: 0; - vertical-align: middle; - font-weight: normal; - cursor: pointer; -} -.radio-inline + .radio-inline, -.checkbox-inline + .checkbox-inline { - margin-top: 0; - margin-left: 10px; // space out consecutive inline controls -} - -// Apply same disabled cursor tweak as for inputs -// Some special care is needed because