├── .codeclimate.yml ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── History.md ├── LICENSE ├── README.md ├── Rakefile ├── jekyll-maps.gemspec ├── lib ├── jekyll-maps.rb └── jekyll-maps │ ├── google_map_api.js │ ├── google_map_api.rb │ ├── google_map_tag.rb │ ├── location_finder.rb │ ├── options_parser.rb │ └── version.rb ├── script ├── bootstrap └── cibuild └── spec ├── fixtures ├── _data │ ├── france │ │ └── places.yml │ ├── maps_styles │ │ └── fixture_style.json │ ├── no_url │ │ └── places.yml │ ├── places.yaml │ ├── spain │ │ └── places.yml │ └── usa │ │ ├── cities.yml │ │ └── observatories.yml ├── _layouts │ └── default.html ├── _my_collection │ └── japan.md ├── _posts │ ├── 2016-07-01-london.md │ ├── 2016-07-02-berlin.md │ ├── 2016-07-03-no-location.md │ └── 2017-06-19-multi-locations.md ├── page.md └── page_without_layout.md ├── google_map_tag_spec.rb ├── location_finder_spec.rb ├── options_parser_spec.rb └── spec_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | fixme: 10 | enabled: true 11 | rubocop: 12 | enabled: true 13 | eslint: 14 | enabled: true 15 | ratings: 16 | paths: 17 | - "**.inc" 18 | - "**.js" 19 | - "**.jsx" 20 | - "**.module" 21 | - "**.php" 22 | - "**.py" 23 | - "**.rb" 24 | exclude_paths: 25 | - script/ 26 | - spec/ 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.gem 3 | Gemfile.lock 4 | tmp 5 | spec/dest 6 | coverage 7 | _site 8 | node_modules 9 | .DS_store 10 | .#* 11 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AllCops: 3 | TargetRubyVersion: 2.0 4 | Include: 5 | - lib/**/*.rb 6 | - spec/**/*.rb 7 | Lint/EndAlignment: 8 | Severity: error 9 | Lint/UnreachableCode: 10 | Severity: error 11 | Lint/UselessAccessModifier: 12 | Enabled: false 13 | Metrics/AbcSize: 14 | Max: 20 15 | Metrics/BlockLength: 16 | Exclude: 17 | - spec/**/*.rb 18 | Metrics/ClassLength: 19 | Max: 300 20 | Exclude: 21 | - !ruby/regexp /spec\/.*.rb$/ 22 | Metrics/CyclomaticComplexity: 23 | Max: 8 24 | Metrics/LineLength: 25 | Exclude: 26 | - Rakefile 27 | - Gemfile 28 | - jekyll-maps.gemspec 29 | Max: 90 30 | Severity: warning 31 | Metrics/MethodLength: 32 | Max: 20 33 | CountComments: false 34 | Severity: error 35 | Metrics/ModuleLength: 36 | Max: 240 37 | Metrics/ParameterLists: 38 | Max: 4 39 | Metrics/PerceivedComplexity: 40 | Max: 8 41 | Style/Alias: 42 | Enabled: false 43 | Style/AlignArray: 44 | Enabled: false 45 | Style/AlignHash: 46 | EnforcedHashRocketStyle: table 47 | Style/AlignParameters: 48 | EnforcedStyle: with_fixed_indentation 49 | Enabled: false 50 | Style/AndOr: 51 | Severity: error 52 | Style/Attr: 53 | Enabled: false 54 | Style/BracesAroundHashParameters: 55 | Enabled: false 56 | Style/ClassAndModuleChildren: 57 | Enabled: false 58 | Style/Documentation: 59 | Enabled: false 60 | Style/DoubleNegation: 61 | Enabled: false 62 | Style/EmptyLinesAroundAccessModifier: 63 | Enabled: false 64 | Style/EmptyLinesAroundModuleBody: 65 | Enabled: false 66 | Style/ExtraSpacing: 67 | AllowForAlignment: true 68 | Style/FileName: 69 | Enabled: false 70 | Style/FirstParameterIndentation: 71 | EnforcedStyle: consistent 72 | Style/GuardClause: 73 | Enabled: false 74 | Style/HashSyntax: 75 | EnforcedStyle: hash_rockets 76 | Severity: error 77 | Style/IfUnlessModifier: 78 | Enabled: false 79 | Style/IndentArray: 80 | EnforcedStyle: consistent 81 | Style/IndentHash: 82 | EnforcedStyle: consistent 83 | Style/IndentationWidth: 84 | Severity: error 85 | Style/ModuleFunction: 86 | Enabled: false 87 | Style/MultilineMethodCallIndentation: 88 | EnforcedStyle: indented 89 | Style/MultilineOperationIndentation: 90 | EnforcedStyle: indented 91 | Style/MultilineTernaryOperator: 92 | Severity: error 93 | Style/PercentLiteralDelimiters: 94 | PreferredDelimiters: 95 | "%q": "{}" 96 | "%Q": "{}" 97 | "%r": "!!" 98 | "%s": "()" 99 | "%w": "()" 100 | "%W": "()" 101 | "%x": "()" 102 | Style/RedundantReturn: 103 | Enabled: false 104 | Style/RedundantSelf: 105 | Enabled: false 106 | Style/RegexpLiteral: 107 | EnforcedStyle: percent_r 108 | Style/RescueModifier: 109 | Enabled: false 110 | Style/SignalException: 111 | EnforcedStyle: only_raise 112 | Style/SingleLineMethods: 113 | Enabled: false 114 | Style/SpaceAroundOperators: 115 | Enabled: false 116 | Style/SpaceInsideBrackets: 117 | Enabled: false 118 | Style/StringLiterals: 119 | EnforcedStyle: double_quotes 120 | Style/StringLiteralsInInterpolation: 121 | EnforcedStyle: double_quotes 122 | Style/UnneededCapitalW: 123 | Enabled: false 124 | Style/IndentHeredoc: 125 | Enabled: false 126 | Style/SymbolArray: 127 | Enabled: false 128 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.4.1 4 | - 2.3.0 5 | - 2.2 6 | - 2.1 7 | script: script/cibuild 8 | after_success: 9 | - bundle exec codeclimate-test-reporter 10 | cache: bundler 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "codeclimate-test-reporter", :group => :test, :require => nil 6 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ## 2.4.0 / 2020-04-19 2 | 3 | * Lazy load Google Maps script (#36) 4 | 5 | ## 2.3.2 / 2020-01-04 6 | 7 | * Add support for Jekyll 4.x (#41) 8 | 9 | ## 2.3.1 / 2019-03-02 10 | 11 | * Fix filter checking logic (#38) 12 | * Allow custom HTML in marker popups (#37) 13 | 14 | ## 2.3.0 / 2018-03-17 15 | 16 | * customize popup link text (#34) 17 | * do not use page url for multiple on page locations (#33) 18 | 19 | ## 2.2.0 / 2018-03-07 20 | 21 | * implement custom marker icon (#30) 22 | * fix external URL in marker 23 | 24 | ## 2.1.1 / 2018-01-25 25 | 26 | * fixed JS lib injection in header (fix #31) 27 | 28 | ## 2.1.0 / 2018-01-10 29 | 30 | * fixed base_url in marker url (fix #28) 31 | 32 | ## 2.0.4 / 2017-07-19 33 | 34 | * allow multiple locations per document (fix #6) 35 | * allow inline locations with map attributes, e.g. `{% google_map laititude='42.23323' longitude='3.213232' %}` (fix #23) 36 | 37 | ## 2.0.3 / 2017-05-17 38 | 39 | * load locations from specific data file (fix #26) 40 | 41 | ## 2.0.2 / 2017-04-24 42 | 43 | * allow multi-word filters (fix #25) 44 | 45 | ## 2.0.1 / 2017-04-11 46 | 47 | * add option to hide markers (show_marker="false") 48 | * do not show link in marker popup when no URL is set 49 | 50 | ## 2.0.0 / 2016-11-06 51 | 52 | * change default behaviour to load location from current page 53 | * removed on-page flag 54 | * change attributes syntax to HTML-style 55 | 56 | ## 1.1.6 / 2016-09-07 57 | 58 | * fix #15 - broken page if there is
tag used 59 | * allow setting custom zoom level with `zoom:10` attribute 60 | 61 | ## 1.1.5 / 2016-09-03 62 | 63 | * allow to disable marker popup on the map 64 | 65 | ## 1.1.4 / 2016-07-31 66 | 67 | * open info window when marker is clicked 68 | 69 | ## 1.1.3 / 2016-07-28 70 | 71 | * on-page map flag 72 | 73 | ## 1.1.2 / 2016-07-22 74 | 75 | * configurable marker cluster 76 | 77 | ## 1.1.1 / 2016-07-20 78 | 79 | * configure GoogleMaps API key from \_config.yml 80 | 81 | ## 1.1.0 / 2016-07-19 82 | 83 | * add multiple maps to single page 84 | * load external JavaScript asynchronously 85 | * configure map element's id, CSS class and dimensions 86 | 87 | ## 1.0.2 / 2016-07-18 88 | 89 | * fix script loading 90 | * look for location coordinates in all collections 91 | 92 | ## 1.0.1 / 2016-07-17 93 | 94 | * implement Google Maps tag 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Anatoliy Yastreb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jekyll Maps 2 | 3 | [![Gem Version](https://badge.fury.io/rb/jekyll-maps.svg)](https://badge.fury.io/rb/jekyll-maps) 4 | [![Build Status](https://travis-ci.org/ayastreb/jekyll-maps.svg?branch=master)](https://travis-ci.org/ayastreb/jekyll-maps) 5 | [![Code Climate](https://codeclimate.com/github/ayastreb/jekyll-maps/badges/gpa.svg)](https://codeclimate.com/github/ayastreb/jekyll-maps) 6 | [![Test Coverage](https://codeclimate.com/github/ayastreb/jekyll-maps/badges/coverage.svg)](https://codeclimate.com/github/ayastreb/jekyll-maps/coverage) 7 | [![Dependency Status](https://gemnasium.com/badges/github.com/ayastreb/jekyll-maps.svg)](https://gemnasium.com/github.com/ayastreb/jekyll-maps) 8 | 9 | Jekyll Maps is a plugin that allows you to easily create different maps on your Jekyll site pages. 10 | It allows you to select which points to display on the map with different filters. 11 | 12 | GoogleMaps Marker Clusterer can be used if you have many points within close proximity. 13 | 14 | ## Installation 15 | 16 | 1. Add the following to your site's `Gemfile`: 17 | 18 | 19 | ```ruby 20 | gem 'jekyll-maps' 21 | ``` 22 | 23 | 2. Add the following to your site's `_config.yml`: 24 | 25 | 26 | ```yml 27 | plugins: 28 | - jekyll-maps 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Configure Google API Key 34 | 35 | To be able to use Google Maps you need to obtain 36 | [API Key](https://developers.google.com/maps/documentation/javascript/get-api-key). 37 | 38 | Once you have your API Key you need to add it to Jekyll's `_config.yml`: 39 | 40 | ```yml 41 | maps: 42 | google: 43 | api_key: 44 | ``` 45 | 46 | ### Data Source 47 | 48 | First, add location information to your posts YAML front-matter: 49 | 50 | ```yml 51 | location: 52 | latitude: 51.5285582 53 | longitude: -0.2416807 54 | ``` 55 | 56 | You can specify multiple locations per post: 57 | 58 | ```yml 59 | location: 60 | - latitude: 51.5285582 61 | longitude: -0.2416807 62 | - latitude: 52.5285582 63 | longitude: -2.2416807 64 | - title: custom marker title 65 | image: custom marker image 66 | url: custom marker url 67 | latitude: 51.5285582 68 | longitude: -0.2416807 69 | ``` 70 | 71 | Alternatively, you can add location info to your custom collection's documents or even in data 72 | files: 73 | 74 | ```yml 75 | - title: Paris 76 | url: http://google.fr 77 | location: 78 | latitude: 48.8587741 79 | longitude: 2.2074741 80 | 81 | - title: Madrid 82 | url: http://google.es 83 | location: 84 | latitude: 40.4378698 85 | longitude: -3.8196204 86 | ``` 87 | 88 | By default this plugin will display location from the page it's placed on: 89 | 90 | ``` 91 | {% google_map %} 92 | ``` 93 | 94 | But you can use src attribute to load locations from other places, like posts, collections or data 95 | files! 96 | 97 | For example, this map will show locations from all posts from 2016: 98 | 99 | ``` 100 | {% google_map src="_posts/2016" %} 101 | ``` 102 | 103 | This map will show locations from a collection called 'my_collection': 104 | 105 | ``` 106 | {% google_map src="_collections/my_collection" %} 107 | ``` 108 | 109 | This map will show locations from all data files located in 'my_points' sub-folder: 110 | 111 | ``` 112 | {% google_map src="_data/my_points" %} 113 | ``` 114 | 115 | You can configure map's dimensions and assign custom CSS class to the element: 116 | 117 | ``` 118 | {% google_map width="100%" height="400" class="my-map" %} 119 | ``` 120 | 121 | You can also just set marker coordinates directly in tag attributes: 122 | 123 | ``` 124 | {% google_map latitude="48.8587741" longitude="2.2074741" marker_title="My Location" marker_img="/img.jpg" marker_url="/my-location.html" %} 125 | ``` 126 | 127 | This will create a map with single marker in given location. `marker_title`, `marker_img` and 128 | `marker_url` attributes are optional and current page's data will be used by default. 129 | 130 | ### Filters 131 | 132 | You can also filter which locations to display on the map!
For instance, following tag will 133 | only display locations from documents which have `lang: en` in their front-matter data. 134 | 135 | ``` 136 | {% google_map src="_posts" lang="en" %} 137 | ``` 138 | 139 | ### Marker Cluster 140 | 141 | By default [Marker Clusterer](https://github.com/googlemaps/js-marker-clusterer) is enabled. If you 142 | have many markers on the map, it will group them and show icon with the count of markers in each 143 | cluster - 144 | [see example](https://googlemaps.github.io/js-marker-clusterer/examples/advanced_example.html). 145 | 146 | If you don't want to use marker cluster, you can disable it globally in `_config.yml`: 147 | 148 | ```yml 149 | maps: 150 | google: 151 | marker_cluster: 152 | enabled: false 153 | ``` 154 | 155 | Or you can disable it per single map tag: 156 | 157 | ``` 158 | {% google_map no_cluster %} 159 | ``` 160 | 161 | If you have any questions or proposals - open up an 162 | [issue](https://github.com/ayastreb/jekyll-maps/issues/new)! 163 | 164 | ## Examples 165 | 166 | Want to see it in action? Check out [Demo Page](https://ayastreb.me/jekyll-maps/#examples)! 167 | 168 | ## Contributing 169 | 170 | 1. Fork it (https://github.com/ayastreb/jekyll-maps/fork) 171 | 2. Create your feature branch (`git checkout -b my-new-feature`) 172 | 3. Commit your changes (`git commit -am 'Add some feature'`) 173 | 4. Push to the branch (`git push origin my-new-feature`) 174 | 5. Create a new Pull Request 175 | 176 | ## License 177 | 178 | [MIT](https://github.com/ayastreb/jekyll-maps/blob/master/LICENSE). Feel free to use, copy or 179 | distribute it. 180 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /jekyll-maps.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib", __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require "jekyll-maps/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "jekyll-maps" 7 | spec.summary = "Jekyll Google Maps integration" 8 | spec.description = "Google Maps support in Jekyll blog to easily embed maps with posts' locations" 9 | spec.version = Jekyll::Maps::VERSION 10 | spec.authors = ["Anatoliy Yastreb"] 11 | spec.email = ["anatoliy.yastreb@gmail.com"] 12 | 13 | spec.homepage = "https://ayastreb.me/jekyll-maps/" 14 | spec.licenses = ["MIT"] 15 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r!^(test|spec|features)/!) } 16 | spec.require_paths = ["lib"] 17 | 18 | spec.add_dependency "jekyll", ">= 3.0", "< 5.0" 19 | 20 | spec.add_development_dependency "rake", "~> 11.0" 21 | spec.add_development_dependency "rspec", "~> 3.5" 22 | spec.add_development_dependency "rubocop", "0.49.1" 23 | end 24 | -------------------------------------------------------------------------------- /lib/jekyll-maps.rb: -------------------------------------------------------------------------------- 1 | require "securerandom" 2 | require "ostruct" 3 | require "jekyll-maps/google_map_api" 4 | require "jekyll-maps/google_map_tag" 5 | require "jekyll-maps/location_finder" 6 | require "jekyll-maps/options_parser" 7 | require "jekyll-maps/version" 8 | 9 | module Jekyll 10 | module Maps 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/jekyll-maps/google_map_api.js: -------------------------------------------------------------------------------- 1 | /* global google */ 2 | /* global MarkerClusterer */ 3 | // eslint-disable-next-line no-unused-vars 4 | var jekyllMaps = (function() { 5 | 'use strict' 6 | var clusterSettings = {} 7 | var clusterReady = false 8 | var mapReady = false 9 | var options = {} 10 | var data = [] 11 | var maps = [] 12 | 13 | return { 14 | initializeMap: initializeMap, 15 | initializeCluster: initializeCluster, 16 | register: register 17 | } 18 | 19 | /** 20 | * Setup Google Maps options and call renderer. 21 | */ 22 | function initializeMap() { 23 | options = { 24 | mapTypeId: google.maps.MapTypeId.ROADMAP, 25 | center: new google.maps.LatLng(0, 0), 26 | styles: [] 27 | } 28 | mapReady = true 29 | render() 30 | } 31 | 32 | /** 33 | * Register map data to be rendered once Google Maps API is loaded. 34 | * 35 | * @param string id 36 | * @param Array locations 37 | * @param Object settings 38 | */ 39 | function register(id, locations, options) { 40 | data.push({ id: id, locations: locations, options: options }) 41 | render() 42 | } 43 | 44 | /** 45 | * Render maps data if Google Maps API is loaded. 46 | */ 47 | function render() { 48 | if (!mapReady) return 49 | 50 | while (data.length > 0) { 51 | var item = data.pop() 52 | var bounds = new google.maps.LatLngBounds() 53 | var mapOptions = Object.assign({}, options, item.options) 54 | var map = new google.maps.Map( 55 | document.getElementById(item.id), 56 | mapOptions 57 | ) 58 | var infoWindow = new google.maps.InfoWindow() 59 | var markers = item.locations.map(createMarker) 60 | 61 | map.fitBounds(bounds) 62 | google.maps.event.addListenerOnce(map, 'idle', function() { 63 | if (this.customZoom) this.setZoom(this.customZoom) 64 | }) 65 | if (mapOptions.useCluster) { 66 | maps.push({ map: map, markers: markers }) 67 | processCluster() 68 | } 69 | } 70 | 71 | function createMarker(location) { 72 | var position = new google.maps.LatLng( 73 | location.latitude, 74 | location.longitude 75 | ) 76 | bounds.extend(position) 77 | if (!mapOptions.showMarker) return false 78 | 79 | var marker = new google.maps.Marker({ 80 | position: position, 81 | title: location.title, 82 | image: location.image, 83 | popup_html: location.popup_html, 84 | icon: location.icon || mapOptions.markerIcon, 85 | url: markerUrl(mapOptions.baseUrl, location.url), 86 | url_text: location.url_text, 87 | map: map 88 | }) 89 | if (mapOptions.showMarkerPopup) marker.addListener('click', markerPopup) 90 | 91 | return marker 92 | } 93 | 94 | function markerUrl(baseUrl, url) { 95 | if (/^(https?|\/\/)/.test(url)) return url 96 | 97 | return url.length > 0 ? baseUrl + url : '' 98 | } 99 | 100 | function markerPopup() { 101 | var content = '
' + this.title + '
' 102 | if (this.popup_html.length > 0) { 103 | content += this.popup_html 104 | } 105 | else { 106 | var imageTag = 107 | this.image.length > 0 && 108 | '' + this.title + '' 109 | if (this.url.length > 0) { 110 | var linkContent = imageTag || this.url_text || 'View' 111 | content += '' + linkContent + '' 112 | } else if (imageTag) { 113 | content += imageTag 114 | } 115 | } 116 | content += '
' 117 | infoWindow.setContent(content) 118 | infoWindow.open(map, this) 119 | } 120 | } 121 | 122 | function initializeCluster(settings) { 123 | clusterReady = true 124 | clusterSettings = settings || {} 125 | processCluster() 126 | } 127 | 128 | function processCluster() { 129 | if (!clusterReady) return 130 | 131 | while (maps.length > 0) { 132 | var obj = maps.pop() 133 | // eslint-disable-next-line no-new 134 | new MarkerClusterer(obj.map, obj.markers, { 135 | gridSize: clusterSettings.grid_size || 25, 136 | imagePath: 137 | 'https://cdn.rawgit.com/googlemaps/js-marker-clusterer/gh-pages/images/m' 138 | }) 139 | } 140 | } 141 | })() 142 | /* Object.assign polyfill */ 143 | if (typeof Object.assign !== 'function') { 144 | Object.assign = function(target) { 145 | 'use strict' 146 | if (target == null) { 147 | throw new TypeError('Cannot convert undefined or null to object') 148 | } 149 | 150 | target = Object(target) 151 | for (var index = 1; index < arguments.length; index++) { 152 | var source = arguments[index] 153 | if (source != null) { 154 | for (var key in source) { 155 | if (Object.prototype.hasOwnProperty.call(source, key)) { 156 | target[key] = source[key] 157 | } 158 | } 159 | } 160 | } 161 | return target 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /lib/jekyll-maps/google_map_api.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Maps 3 | class GoogleMapApi 4 | HEAD_END_TAG = %r!! 5 | BODY_END_TAG = %r!! 6 | 7 | class << self 8 | def prepend_api_code(doc) 9 | @config = doc.site.config 10 | if doc.output =~ HEAD_END_TAG 11 | # Insert API code before header's end if this document has one. 12 | doc.output.gsub!(HEAD_END_TAG, %(#{api_code}#{Regexp.last_match})) 13 | else 14 | doc.output.prepend(api_code) 15 | end 16 | end 17 | 18 | def prepend_google_api_code(doc) 19 | @config = doc.site.config 20 | if doc.output =~ BODY_END_TAG 21 | # Insert API code before body's end if this document has one. 22 | doc.output.gsub!(BODY_END_TAG, %(#{google_api_code}#{Regexp.last_match})) 23 | else 24 | doc.output.prepend(api_code) 25 | end 26 | end 27 | 28 | private 29 | def api_code 30 | < 32 | #{js_lib_contents} 33 | 34 | HTML 35 | end 36 | 37 | private 38 | def google_api_code 39 | < 52 | 53 | 54 | // Load maps only when DOM is loaded 55 | document.addEventListener("DOMContentLoaded", function() { 56 | if (window.google && window.google.maps && jekyllMaps) { 57 | // Maps script already loaded -> Execute callback method 58 | jekyllMaps.initializeMap(); 59 | } else if (!('IntersectionObserver' in window) || 60 | !('IntersectionObserverEntry' in window) || 61 | !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) { 62 | // Intersection Observer -> Backup solution : load maps now 63 | lazyLoadGoogleMap(); 64 | } else { 65 | // Google Maps not loaded & Intersection Observer working -> Enable it 66 | enableMapsObserver(); 67 | } 68 | }); 69 | 70 | function enableMapsObserver() { 71 | // Enable Observer on all Maps 72 | var maps = document.getElementsByClassName('jekyll-map'); 73 | 74 | const observer = new IntersectionObserver(function(entries, observer) { 75 | // Test if one of the maps is in the viewport 76 | var isIntersecting = typeof entries[0].isIntersecting === 'boolean' ? entries[0].isIntersecting : entries[0].intersectionRatio > 0; 77 | if (isIntersecting) { 78 | lazyLoadGoogleMap(); 79 | observer.disconnect(); 80 | } 81 | }); 82 | 83 | for(var i = 0; i < maps.length; i++) { 84 | observer.observe(maps[i]); 85 | } 86 | } 87 | 88 | function lazyLoadGoogleMap() { 89 | // If google maps api script not already loaded 90 | if(!window.google || !window.google.maps) { 91 | var fjs = document.getElementsByTagName('script')[0]; 92 | var js = document.createElement('script'); 93 | js.id = 'gmap-api'; 94 | js.setAttribute('async', ''); 95 | js.setAttribute('defer', ''); 96 | js.src = "//maps.google.com/maps/api/js?key=#{api_key}&callback=#{Jekyll::Maps::GoogleMapTag::JS_LIB_NAME}.initializeMap"; 97 | fjs.parentNode.insertBefore(js, fjs); 98 | } 99 | } 100 | 101 | HTML 102 | end 103 | 104 | private 105 | def load_marker_cluster 106 | settings = @config.fetch("maps", {}) 107 | .fetch("google", {}) 108 | .fetch("marker_cluster", {}) 109 | return unless settings.fetch("enabled", true) 110 | < 113 | HTML 114 | end 115 | 116 | private 117 | def js_lib_contents 118 | @js_lib_contents ||= begin 119 | File.read(js_lib_path) 120 | end 121 | end 122 | 123 | private 124 | def js_lib_path 125 | @js_lib_path ||= begin 126 | File.expand_path("./google_map_api.js", File.dirname(__FILE__)) 127 | end 128 | end 129 | end 130 | end 131 | end 132 | end 133 | 134 | Jekyll::Hooks.register [:pages, :documents], :post_render do |doc| 135 | if doc.output =~ %r!#{Jekyll::Maps::GoogleMapTag::JS_LIB_NAME}! 136 | Jekyll::Maps::GoogleMapApi.prepend_api_code(doc) 137 | Jekyll::Maps::GoogleMapApi.prepend_google_api_code(doc) 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/jekyll-maps/google_map_tag.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Maps 3 | class GoogleMapTag < Liquid::Tag 4 | JS_LIB_NAME = "jekyllMaps".freeze 5 | DEFAULT_MAP_WIDTH = 600 6 | DEFAULT_MAP_HEIGHT = 400 7 | 8 | def initialize(_, args, _) 9 | @args = OptionsParser.parse(args) 10 | @finder = LocationFinder.new(@args) 11 | super 12 | end 13 | 14 | def render(context) 15 | locations = @finder.find(context.registers[:site], context.registers[:page]) 16 | @args[:attributes][:id] ||= SecureRandom.uuid 17 | 18 | < 20 | 27 | HTML 28 | end 29 | 30 | private 31 | def render_attributes 32 | attributes = [] 33 | attributes << "id='#{@args[:attributes][:id]}'" 34 | attributes << render_dimensions 35 | attributes << render_class 36 | attributes.join(" ") 37 | end 38 | 39 | private 40 | def render_dimensions 41 | width = @args[:attributes][:width] || DEFAULT_MAP_WIDTH 42 | height = @args[:attributes][:height] || DEFAULT_MAP_HEIGHT 43 | width_unit = width.to_s.include?("%") ? "" : "px" 44 | height_unit = height.to_s.include?("%") ? "" : "px" 45 | %(style='width:#{width}#{width_unit};height:#{height}#{height_unit};') 46 | end 47 | 48 | private 49 | def render_class 50 | css = @args[:attributes][:class] 51 | css = css.join(" ") if css.is_a?(Array) 52 | %(class='#{css} jekyll-map') 53 | end 54 | 55 | private 56 | def render_styles(site) 57 | style_name = @args[:attributes][:styles] || "default" 58 | maps_styles = site.data["maps_styles"] || {} 59 | maps_styles[style_name] || "[]" 60 | end 61 | 62 | private 63 | def map_options(site) 64 | opts = { 65 | :baseUrl => site.baseurl || "/", 66 | :useCluster => !@args[:flags][:no_cluster], 67 | :showMarker => @args[:attributes][:show_marker] != "false", 68 | :showMarkerPopup => @args[:attributes][:show_popup] != "false", 69 | :markerIcon => @args[:attributes][:marker_icon], 70 | :styles => render_styles(site) 71 | } 72 | if @args[:attributes][:zoom] 73 | opts[:customZoom] = @args[:attributes][:zoom].to_i 74 | end 75 | opts 76 | end 77 | end 78 | end 79 | end 80 | 81 | Liquid::Template.register_tag("google_map", Jekyll::Maps::GoogleMapTag) 82 | -------------------------------------------------------------------------------- /lib/jekyll-maps/location_finder.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Maps 3 | class LocationFinder 4 | def initialize(options) 5 | @documents = [] 6 | @options = options 7 | end 8 | 9 | def find(site, page) 10 | if @options[:attributes][:latitude] && @options[:attributes][:longitude] 11 | return [location_from_options(page)] 12 | elsif @options[:filters].empty? 13 | @documents << page if with_location?(page) 14 | else 15 | site.collections.each_value { |collection| filter(collection.docs) } 16 | site_data(site).each_value { |items| traverse(items) } 17 | end 18 | 19 | documents_to_locations 20 | end 21 | 22 | private 23 | def location_from_options(page) 24 | { 25 | :latitude => @options[:attributes][:latitude], 26 | :longitude => @options[:attributes][:longitude], 27 | :title => @options[:attributes][:marker_title] || page["title"], 28 | :icon => @options[:attributes][:marker_icon] || page["marker_icon"], 29 | :url => @options[:attributes][:marker_url] || fetch_url(page), 30 | :image => @options[:attributes][:marker_img] || page["image"] || "", 31 | :popup_html => @options[:attributes][:marker_popup_html] || "" 32 | } 33 | end 34 | 35 | private 36 | def site_data(site) 37 | return {} unless data_source? 38 | 39 | path = @options[:filters]["src"].scan(%r!_data\/([^\/]+)!).join(".") 40 | return site.data if path.empty? 41 | 42 | data = OpenStruct.new(site.data) 43 | if @options[:filters]["src"] =~ %r!\.ya?ml! 44 | { :path => data[path.gsub(%r!\.ya?ml!, "")] } 45 | else 46 | data[path] 47 | end 48 | end 49 | 50 | private 51 | def data_source? 52 | filters = @options[:filters] 53 | filters.key?("src") && filters["src"].start_with?("_data") 54 | end 55 | 56 | private 57 | def traverse(items) 58 | return filter(items) if items.is_a?(Array) 59 | 60 | items.each_value { |children| traverse(children) } if items.is_a?(Hash) 61 | end 62 | 63 | private 64 | def filter(docs) 65 | docs.each do |doc| 66 | @documents << doc if with_location?(doc) && match_filters?(doc) 67 | end 68 | end 69 | 70 | private 71 | def with_location?(doc) 72 | !doc["location"].nil? && !doc["location"].empty? 73 | end 74 | 75 | private 76 | def match_filters?(doc) 77 | @options[:filters].each do |filter, value| 78 | if filter == "src" 79 | if doc.respond_to?(:relative_path) 80 | return false unless doc.relative_path.start_with?(value) 81 | end 82 | elsif doc[filter].nil? || doc[filter] != value 83 | return false 84 | end 85 | end 86 | return true 87 | end 88 | 89 | private 90 | def documents_to_locations 91 | locations = [] 92 | @documents.each do |document| 93 | if document["location"].is_a?(Array) 94 | document["location"].each do |location| 95 | point = convert(document, location) 96 | point[:url] = "" if point[:url] == fetch_url(document) 97 | locations.push(point) 98 | end 99 | else 100 | locations.push(convert(document, document["location"])) 101 | end 102 | end 103 | locations 104 | end 105 | 106 | private 107 | def convert(document, location) 108 | { 109 | :latitude => location["latitude"], 110 | :longitude => location["longitude"], 111 | :title => location["title"] || document["title"], 112 | :icon => location["marker_icon"] || document["marker_icon"], 113 | :url => location["url"] || fetch_url(document), 114 | :url_text => location["url_text"], 115 | :image => location["image"] || document["image"] || "", 116 | :popup_html => location["marker_popup_html"] \ 117 | || document["marker_popup_html"] || "" 118 | } 119 | end 120 | 121 | private 122 | def fetch_url(document) 123 | return document["url"] if document.is_a?(Hash) && document.key?("url") 124 | return document.url if document.respond_to? :url 125 | "" 126 | end 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/jekyll-maps/options_parser.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Maps 3 | class OptionsParser 4 | OPTIONS_SYNTAX = %r!([^\s]+)\s*=\s*['"]+([^'"]+)['"]+! 5 | ALLOWED_FLAGS = %w( 6 | no_cluster 7 | ).freeze 8 | ALLOWED_ATTRIBUTES = %w( 9 | id 10 | width 11 | height 12 | class 13 | show_marker 14 | show_popup 15 | zoom 16 | latitude 17 | longitude 18 | marker_title 19 | marker_icon 20 | marker_img 21 | marker_url 22 | marker_popup_html 23 | styles 24 | ).freeze 25 | 26 | class << self 27 | def parse(raw_options) 28 | options = { 29 | :attributes => {}, 30 | :filters => {}, 31 | :flags => {} 32 | } 33 | raw_options.scan(OPTIONS_SYNTAX).each do |key, value| 34 | value = value.split(",") if value.include?(",") 35 | if ALLOWED_ATTRIBUTES.include?(key) 36 | options[:attributes][key.to_sym] = value 37 | else 38 | options[:filters][key] = value 39 | end 40 | end 41 | ALLOWED_FLAGS.each do |key| 42 | options[:flags][key.to_sym] = true if raw_options.include?(key) 43 | end 44 | options 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/jekyll-maps/version.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | module Maps 3 | VERSION = "2.4.0".freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | bundle install 4 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | bundle exec rspec --color $@ 4 | bundle exec rubocop -S -D 5 | -------------------------------------------------------------------------------- /spec/fixtures/_data/france/places.yml: -------------------------------------------------------------------------------- 1 | - title: Paris 2 | url: http://google.fr 3 | location: 4 | latitude: 48.8587741 5 | longitude: 2.2074741 6 | 7 | - title: Not a place 8 | url: http://yahoo.com 9 | -------------------------------------------------------------------------------- /spec/fixtures/_data/maps_styles/fixture_style.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "elementType": "geometry", 4 | "stylers": [ 5 | { 6 | "color": "#1d2c4d" 7 | } 8 | ] 9 | } 10 | ] -------------------------------------------------------------------------------- /spec/fixtures/_data/no_url/places.yml: -------------------------------------------------------------------------------- 1 | - title: No link 2 | location: 3 | latitude: 48.8587741 4 | longitude: 2.2074741 5 | -------------------------------------------------------------------------------- /spec/fixtures/_data/places.yaml: -------------------------------------------------------------------------------- 1 | - title: "Tokyo" 2 | url: "http://google.jp" 3 | location: 4 | latitude: 35.652832 5 | longitude: 139.839478 6 | 7 | - title: "New York" 8 | url: "http://google.com" 9 | location: 10 | latitude: 40.730610 11 | longitude: -73.935242 12 | -------------------------------------------------------------------------------- /spec/fixtures/_data/spain/places.yml: -------------------------------------------------------------------------------- 1 | - title: Madrid 2 | url: http://google.es 3 | location: 4 | latitude: 40.4378698 5 | longitude: -3.8196204 6 | 7 | - title: Not a place 8 | url: http://yahoo.com 9 | -------------------------------------------------------------------------------- /spec/fixtures/_data/usa/cities.yml: -------------------------------------------------------------------------------- 1 | - title: Boston 2 | state: MA 3 | location: 4 | latitude: 42.358935 5 | longitude: -71.056772 6 | 7 | - title: New York 8 | state: NY 9 | location: 10 | latitude: 40.710399 11 | longitude: -74.001612 12 | 13 | - title: Philadelphia 14 | state: PA 15 | location: 16 | latitude: 39.949775 17 | longitude: -75.164116 18 | 19 | - title: Pittsburgh 20 | state: PA 21 | location: 22 | latitude: 40.440482 23 | longitude: -79.996974 24 | -------------------------------------------------------------------------------- /spec/fixtures/_data/usa/observatories.yml: -------------------------------------------------------------------------------- 1 | - title: Apache Point Observatory 2 | state: NM 3 | location: 4 | latitude: 32.780341 5 | longitude: -105.819661 6 | 7 | - title: Naylor Observatory 8 | state: PA 9 | location: 10 | latitude: 40.149433 11 | longitude: -76.895617 12 | 13 | - title: Adams Observatory 14 | state: IA 15 | location: 16 | latitude: 42.092937 17 | longitude: -93.569096 18 | -------------------------------------------------------------------------------- /spec/fixtures/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jekyll Maps Plugin Test 6 | 7 | 8 |
9 | 15 |
16 | {{ content }} 17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/fixtures/_my_collection/japan.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tokyo 3 | image: data:image/jpeg;base64,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 4 | location: 5 | latitude: 35.6732619 6 | longitude: 139.5703038 7 | --- 8 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2016-07-01-london.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: London 3 | country: gb 4 | location: 5 | latitude: 51.5285582 6 | longitude: -0.2416807 7 | --- 8 | 9 | London is a capital of Great Britain. 10 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2016-07-02-berlin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Berlin 3 | country: de 4 | location: 5 | latitude: 52.5072111 6 | longitude: 13.1449604 7 | --- 8 | 9 | Berlin is in Germany. 10 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2016-07-03-no-location.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: post without location 3 | --- 4 | 5 | Nothing to see here. 6 | -------------------------------------------------------------------------------- /spec/fixtures/_posts/2017-06-19-multi-locations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Barcelona 3 | country: es 4 | image: /main-img.jpg 5 | location: 6 | - latitude: 41.3948976 7 | longitude: 2.0787279 8 | - title: sagrada familia 9 | latitude: 41.4032671 10 | longitude: 2.1739832 11 | - title: location with url 12 | latitude: 41.3864518 13 | longitude: 2.1890757 14 | image: /next-img.jpg 15 | url: /next-post 16 | url_text: "Next Post" 17 | --- 18 | 19 | Barcelona is in Spain. 20 | -------------------------------------------------------------------------------- /spec/fixtures/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test Page 3 | layout: default 4 | --- 5 | 6 | ** Jekyll Maps Plugin ** 7 | 8 | {% google_map src="_posts" width="600" height="400" %} 9 | -------------------------------------------------------------------------------- /spec/fixtures/page_without_layout.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test Page 3 | --- 4 | 5 | ** Jekyll Maps Plugin ** 6 | 7 | {% google_map src="_posts" width="100%" height="100%" %} 8 | -------------------------------------------------------------------------------- /spec/google_map_tag_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Jekyll::Maps::GoogleMapTag do 4 | let(:site) { make_site } 5 | before { site.process } 6 | 7 | context "full page rendering" do 8 | let(:content) { File.read(dest_dir("page.html")) } 9 | 10 | it "builds javascript" do 11 | expect(content).to match(%r!#{Jekyll::Maps::GoogleMapTag::JS_LIB_NAME}!) 12 | expect(content).to match(%r!(London|Paris)!) 13 | end 14 | 15 | it "does not include external js directly (should be lazy loaded)" do 16 | expect(content.scan(%r!maps\.googleapis\.com!).length).to eq(0) 17 | end 18 | 19 | it "registers Google Maps for lazy loading" do 20 | expect(content).to match(%r!js.src = "//maps.google.com/maps/!) 21 | end 22 | 23 | it "renders API key" do 24 | expect(content).to match(%r!maps/api/js\?key=GOOGLE_MAPS_API_KEY!) 25 | end 26 | 27 | it "provides fallback method when IntersectionObserver is 28 | not implemented/supported (older browsers)" do 29 | expect(content).to match(%r!('IntersectionObserver' in window)!) 30 | end 31 | end 32 | 33 | context "marker cluster disabled" do 34 | let(:site) do 35 | make_site({ 36 | "maps" => { 37 | "google" => { 38 | "marker_cluster" => { 39 | "enabled" => false 40 | } 41 | } 42 | } 43 | }) 44 | end 45 | let(:content) { File.read(dest_dir("page.html")) } 46 | before :each do 47 | site.process 48 | end 49 | 50 | it "does not load marker cluster external script" do 51 | expect(content).not_to match(%r!script.*src=.*markerclusterer\.js!) 52 | end 53 | end 54 | 55 | context "marker cluster enabled by default" do 56 | let(:site) { make_site } 57 | let(:content) { File.read(dest_dir("page.html")) } 58 | before :each do 59 | site.process 60 | end 61 | 62 | it "does load marker clusterer external script" do 63 | expect(content).to match(%r!script.*src=.*markerclusterer\.js!) 64 | end 65 | end 66 | 67 | context "options rendering" do 68 | let(:page) { make_page } 69 | let(:site) { make_site } 70 | let(:context) { make_context(:page => page, :site => site) } 71 | let(:tag) { "google_map" } 72 | 73 | context "render all attributes" do 74 | let(:options) do 75 | "id='foo' width='100' height='50%' class='baz,bar' ignored='bad' zoom='5'" 76 | end 77 | let(:output) do 78 | Liquid::Template.parse("{% #{tag} #{options} %}").render!(context, {}) 79 | end 80 | 81 | it "renders attributes" do 82 | expect(output).to match("div id='foo' style='width:100px;height:50%;'") 83 | expect(output).to match("class='baz bar jekyll-map'") 84 | end 85 | 86 | it "renders custom zoom setting" do 87 | expected = %r!"customZoom":5! 88 | expect(output).to match(expected) 89 | end 90 | end 91 | 92 | context "render default dimensions" do 93 | let(:options) { "id='foo'" } 94 | let(:output) do 95 | Liquid::Template.parse("{% #{tag} #{options} %}").render!(context, {}) 96 | end 97 | 98 | it "renders dimensions with default values" do 99 | width = Jekyll::Maps::GoogleMapTag::DEFAULT_MAP_WIDTH 100 | height = Jekyll::Maps::GoogleMapTag::DEFAULT_MAP_HEIGHT 101 | expected = %r!div id='foo' style='width:#{width}px;height:#{height}px;'! 102 | expect(output).to match(expected) 103 | end 104 | end 105 | 106 | context "render with custom styles" do 107 | let(:options) { "styles='fixture_style'" } 108 | let(:output) do 109 | Liquid::Template.parse("{% #{tag} #{options} %}").render!(context, {}) 110 | end 111 | 112 | it "renders dimensions with default values" do 113 | # styles content is loaded from fixtures/_data/maps_styles/fixture_style.json 114 | expected = '"styles":[{"elementType":"geometry","stylers":[{"color":"#1d2c4d"}]}]' 115 | expect(output).to include(expected) 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/location_finder_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Jekyll::Maps::LocationFinder do 4 | let(:site) { make_site } 5 | let(:page) { make_page } 6 | 7 | before :each do 8 | site.process 9 | end 10 | 11 | context "looking for locations in posts" do 12 | let(:options) { Jekyll::Maps::OptionsParser.parse("src='_posts'") } 13 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 14 | let(:actual) { finder.find(site, page) } 15 | 16 | it "finds posts with location" do 17 | expect(actual).to all(be_a(Hash)) 18 | expect(actual).to all(include(:latitude, :longitude, :title, :url)) 19 | end 20 | 21 | it "finds location in post" do 22 | expect(actual.find { |l| l[:title] == "London" }).to be_a(Hash) 23 | end 24 | 25 | it "finds multiple locations in single post" do 26 | # there should be 3 locations in post: fixtures/_posts/2017-06-19-multi-locations.md 27 | barcelona_main = actual.find { |l| l[:title] == "Barcelona" } 28 | expect(barcelona_main).to be_a(Hash) 29 | expect(barcelona_main[:url]).to eq("") 30 | expect(barcelona_main[:latitude]).to eq(41.3948976) 31 | expect(barcelona_main[:longitude]).to eq(2.0787279) 32 | expect(barcelona_main[:image]).to eq("/main-img.jpg") 33 | 34 | barcelona_sagrada = actual.find { |l| l[:title] == "sagrada familia" } 35 | expect(barcelona_sagrada[:url]).to eq("") 36 | expect(barcelona_sagrada[:latitude]).to eq(41.4032671) 37 | expect(barcelona_sagrada[:longitude]).to eq(2.1739832) 38 | expect(barcelona_sagrada[:image]).to eq("/main-img.jpg") 39 | 40 | barcelona_url = actual.find { |l| l[:title] == "location with url" } 41 | expect(barcelona_url[:url]).to eq("/next-post") 42 | expect(barcelona_url[:url_text]).to eq("Next Post") 43 | expect(barcelona_url[:latitude]).to eq(41.3864518) 44 | expect(barcelona_url[:longitude]).to eq(2.1890757) 45 | expect(barcelona_url[:image]).to eq("/next-img.jpg") 46 | end 47 | 48 | it "skips posts without location" do 49 | actual.each do |location| 50 | expect(location).not_to include(:title => "post without location") 51 | end 52 | end 53 | end 54 | 55 | context "looking for locations in custom collections" do 56 | let(:options) { Jekyll::Maps::OptionsParser.parse("src='_my_collection'") } 57 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 58 | let(:actual) { finder.find(site, page) } 59 | 60 | it "finds location in custom collections" do 61 | expect(actual.find { |l| l[:title] == "Tokyo" }).to be_a(Hash) 62 | end 63 | end 64 | 65 | context "looking for locations in data files with deep source (France)" do 66 | let(:options) { Jekyll::Maps::OptionsParser.parse("src='_data/france/places.yml'") } 67 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 68 | let(:actual) { finder.find(site, page) } 69 | 70 | it "finds location from France" do 71 | expect(actual.find { |l| l[:title] == "Paris" }).to be_a(Hash) 72 | end 73 | 74 | it "doesn't find location from Spain" do 75 | actual.each do |location| 76 | expect(location).not_to include(:title => "Madird") 77 | end 78 | end 79 | end 80 | 81 | context "looking for locations in data files with deep source (Spain)" do 82 | let(:options) { Jekyll::Maps::OptionsParser.parse("src='_data/spain'") } 83 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 84 | let(:actual) { finder.find(site, page) } 85 | 86 | it "finds location from Spain" do 87 | expect(actual.find { |l| l[:title] == "Madrid" }).to be_a(Hash) 88 | end 89 | 90 | it "doesn't find location from France" do 91 | actual.each do |location| 92 | expect(location).not_to include(:title => "Paris") 93 | end 94 | end 95 | end 96 | 97 | context "looking for locations in specific data file" do 98 | let(:options) { Jekyll::Maps::OptionsParser.parse("src='_data/places.yaml'") } 99 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 100 | let(:actual) { finder.find(site, page) } 101 | 102 | it "finds locations in all data files" do 103 | expect(actual.length).to eq(2) 104 | expect(actual.find { |l| l[:title] == "Tokyo" }).to be_a(Hash) 105 | expect(actual.find { |l| l[:title] == "New York" }).to be_a(Hash) 106 | end 107 | end 108 | 109 | context "looking for locations in data files with shallow source" do 110 | let(:options) { Jekyll::Maps::OptionsParser.parse("src='_data'") } 111 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 112 | let(:actual) { finder.find(site, page) } 113 | 114 | it "finds locations in all data files" do 115 | expect(actual.find { |l| l[:title] == "Paris" }).to be_a(Hash) 116 | expect(actual.find { |l| l[:title] == "Madrid" }).to be_a(Hash) 117 | end 118 | end 119 | 120 | context "filtering posts by location" do 121 | let(:options) { Jekyll::Maps::OptionsParser.parse("src='_posts' country='de'") } 122 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 123 | let(:actual) { finder.find(site, page) } 124 | 125 | it "finds only German locations" do 126 | expect(actual.empty?).to be_falsey 127 | actual.each do |location| 128 | expect(location).to include(:title => "Berlin") 129 | end 130 | end 131 | end 132 | 133 | context "filtering data by location (state filter first)" do 134 | search_data_for_pa_places("state='PA' src='_data'") 135 | end 136 | 137 | context "filtering data by location (src filter first)" do 138 | search_data_for_pa_places("src='_data' state='PA'") 139 | end 140 | 141 | context "by default look for locations on current page" do 142 | let(:location) { { "location" => { "latitude" => 1, "longitude" => -1 } } } 143 | let(:page) { make_page(location) } 144 | let(:options) { Jekyll::Maps::OptionsParser.parse("") } 145 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 146 | let(:actual) { finder.find(site, page) } 147 | 148 | it "finds only location from given page" do 149 | expect(actual.length).to eq(1) 150 | expect(actual.first[:latitude]).to eq(location["location"]["latitude"]) 151 | expect(actual.first[:longitude]).to eq(location["location"]["longitude"]) 152 | end 153 | end 154 | 155 | context "skip url if location does not have it" do 156 | let(:options) { Jekyll::Maps::OptionsParser.parse("src='_data/no_url'") } 157 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 158 | let(:actual) { finder.find(site, page) } 159 | 160 | it "finds location without link" do 161 | location = actual.find { |l| l[:title] == "No link" } 162 | expect(location).to be_a(Hash) 163 | expect(location[:url]).to eq("") 164 | end 165 | end 166 | 167 | context "take location from inline attributes first" do 168 | let(:options) do 169 | attrs = %w( 170 | latitude='42.2' 171 | longitude='3.2' 172 | marker_title='inline marker' 173 | marker_img='/marker-img.jpg' 174 | marker_url='/marker-url' 175 | ) 176 | Jekyll::Maps::OptionsParser.parse(attrs.join(" ")) 177 | end 178 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 179 | let(:actual) { finder.find(site, page) } 180 | 181 | it "finds only location from attributes" do 182 | expect(actual.empty?).to be_falsey 183 | expect(actual.length).to eq(1) 184 | location = actual.find { |l| l[:title] == "inline marker" } 185 | expect(location).to be_a(Hash) 186 | expect(location[:latitude]).to eq("42.2") 187 | expect(location[:longitude]).to eq("3.2") 188 | expect(location[:title]).to eq("inline marker") 189 | expect(location[:image]).to eq("/marker-img.jpg") 190 | expect(location[:url]).to eq("/marker-url") 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /spec/options_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Jekyll::Maps::OptionsParser do 4 | context "parses filters" do 5 | it "ignores extra whitespaces" do 6 | actual = Jekyll::Maps::OptionsParser.parse(" foo_key = 'bar' moo = 'baz'") 7 | expected = { 8 | "foo_key" => "bar", 9 | "moo" => "baz" 10 | } 11 | 12 | expect(actual[:filters]).to eq(expected) 13 | end 14 | 15 | it "parses double quotes" do 16 | actual = Jekyll::Maps::OptionsParser.parse('foo="bar"') 17 | expected = { 18 | "foo" => "bar" 19 | } 20 | 21 | expect(actual[:filters]).to eq(expected) 22 | end 23 | 24 | it "parses single argument" do 25 | actual = Jekyll::Maps::OptionsParser.parse("foo='bar'") 26 | expected = { 27 | "foo" => "bar" 28 | } 29 | 30 | expect(actual[:filters]).to eq(expected) 31 | end 32 | 33 | it "parses multiple arguments" do 34 | actual = Jekyll::Maps::OptionsParser.parse("foo='bar' moo='baz'") 35 | expected = { 36 | "foo" => "bar", 37 | "moo" => "baz" 38 | } 39 | 40 | expect(actual[:filters]).to eq(expected) 41 | end 42 | 43 | it "parses multiple values in argument" do 44 | actual = Jekyll::Maps::OptionsParser.parse("foo='bar,baz'") 45 | expected = { 46 | "foo" => %w(bar baz) 47 | } 48 | 49 | expect(actual[:filters]).to eq(expected) 50 | end 51 | 52 | it "parses multiple words in argument" do 53 | actual = Jekyll::Maps::OptionsParser.parse("foo='bar baz' moo = 'mar maz'") 54 | expected = { 55 | "foo" => "bar baz", 56 | "moo" => "mar maz" 57 | } 58 | 59 | expect(actual[:filters]).to eq(expected) 60 | end 61 | end 62 | 63 | context "parses attributes" do 64 | it "parses predefined attributes" do 65 | actual = Jekyll::Maps::OptionsParser.parse( 66 | "id='foo' width='100' height='50%' class='my-css-class,another-class'" 67 | ) 68 | expected = { 69 | :id => "foo", 70 | :width => "100", 71 | :height => "50%", 72 | :class => %w(my-css-class another-class) 73 | } 74 | 75 | expect(actual[:attributes]).to eq(expected) 76 | end 77 | end 78 | 79 | context "parses flags" do 80 | it "parses all allowed flags correctly" do 81 | actual = Jekyll::Maps::OptionsParser.parse("no_cluster") 82 | expected = { 83 | :no_cluster => true 84 | } 85 | 86 | expect(actual[:flags]).to eq(expected) 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "simplecov" 3 | SimpleCov.start 4 | 5 | require "jekyll" 6 | require "jekyll-maps" 7 | 8 | Jekyll.logger.log_level = :error 9 | 10 | RSpec.configure do |config| 11 | config.run_all_when_everything_filtered = true 12 | config.filter_run :focus 13 | config.order = "random" 14 | 15 | SOURCE_DIR = File.expand_path("../fixtures", __FILE__) 16 | DEST_DIR = File.expand_path("../dest", __FILE__) 17 | 18 | def source_dir(*files) 19 | File.join(SOURCE_DIR, *files) 20 | end 21 | 22 | def dest_dir(*files) 23 | File.join(DEST_DIR, *files) 24 | end 25 | 26 | CONFIG_DEFAULTS = { 27 | "source" => source_dir, 28 | "destination" => dest_dir, 29 | "gems" => ["jekyll-maps"], 30 | "collections" => ["my_collection"], 31 | "maps" => { 32 | "google" => { 33 | "api_key" => "GOOGLE_MAPS_API_KEY" 34 | } 35 | } 36 | }.freeze 37 | 38 | def make_page(options = {}) 39 | page = Jekyll::Page.new(site, CONFIG_DEFAULTS["source"], "", "page.md") 40 | page.data = options 41 | page 42 | end 43 | 44 | def make_site(options = {}) 45 | site_config = Jekyll.configuration(CONFIG_DEFAULTS.merge(options)) 46 | Jekyll::Site.new(site_config) 47 | end 48 | 49 | def make_context(registers = {}, environments = {}) 50 | Liquid::Context.new( 51 | environments, 52 | {}, 53 | { :site => site, :page => page }.merge(registers) 54 | ) 55 | end 56 | 57 | def finds_all_pa_locations(_options, _finder, actual) 58 | expect(actual.empty?).to be_falsey 59 | pa_places = ["Pittsburgh", "Philadelphia", "Naylor Observatory"] 60 | pa_places.each do |title| 61 | expect(actual.find { |l| l[:title] == title }).to be_a(Hash) 62 | end 63 | end 64 | 65 | def ignores_non_pa_locations(_options, _finder, actual) 66 | non_pa_places = [ 67 | "Boston", 68 | "New York", 69 | "Apache Point Observatory", 70 | "Adams Observatory", 71 | "Paris", 72 | "Madrid", 73 | "Not a place" 74 | ] 75 | non_pa_places.each do |title| 76 | expect(actual.find { |l| l[:title] == title }).to be_nil 77 | end 78 | end 79 | 80 | def search_data_for_pa_places(query) 81 | let(:options) { Jekyll::Maps::OptionsParser.parse(query) } 82 | let(:finder) { Jekyll::Maps::LocationFinder.new(options) } 83 | let(:actual) { finder.find(site, page) } 84 | 85 | it "ignores non-PA locations" do 86 | finds_all_pa_locations(options, finder, actual) 87 | end 88 | it "finds all PA locations" do 89 | ignores_non_pa_locations(options, finder, actual) 90 | end 91 | end 92 | end 93 | --------------------------------------------------------------------------------