├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── ansible ├── Makefile ├── files │ ├── authorized_keys │ └── nginx.conf ├── hosts └── playbook.yml ├── bin ├── build-architecture-variants.rb ├── build-clean-index.rb ├── build-index-with-github.rb ├── build-linkeddata.rb ├── build-rss-feed.rb ├── build-search-index.rb ├── build-site.rb ├── build-sitemap.rb ├── fetch-github-commits.rb ├── fetch-github-repos.rb ├── fetch-github-users.rb ├── twitter-follow.rb └── twitter-publish.rb ├── lib ├── github.rb ├── helpers.rb ├── render.rb └── twitter_config.rb ├── public ├── BingSiteAuth.xml ├── favicon.ico ├── feed-style.css ├── feed.xslt ├── googleaa77a87172eb8ceb.html ├── js │ ├── fuse.min.js │ ├── search.js │ └── typeahead.jquery.min.js ├── robots.txt ├── style.css └── typeaheadjs.css └── views ├── architecture_variants.html.erb ├── author.html.erb ├── authors.html.erb ├── feed.xml.erb ├── index.html.erb ├── layout.html.erb ├── list.html.erb └── show.html.erb /.gitignore: -------------------------------------------------------------------------------- 1 | # Downloaded from Google Docs 2 | authors_extras.csv 3 | repos_extras.csv 4 | 5 | library_index_raw.json 6 | library_index_clean.json 7 | library_index_with_github.json 8 | 9 | schema_org_context.json 10 | spdx_licences.json 11 | 12 | github_commits.json 13 | github_repos.json 14 | github_users.json 15 | 16 | public/architectures 17 | public/architecture-variants.html 18 | public/authors 19 | public/categories 20 | public/feed.xml 21 | public/index.html 22 | public/libraries 23 | public/licenses 24 | public/sitemap.xml 25 | public/search-index.json 26 | public/types 27 | 28 | ansible/*.retry 29 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.1 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'addressable' 4 | gem 'json' 5 | gem 'rake' 6 | gem 'webrick', '> 1.4.0' 7 | gem 'semverly' 8 | gem 'erubis' 9 | gem 'tilt' 10 | gem 'filesize' 11 | gem 'nokogiri', '~> 1.13.4' 12 | 13 | gem 'twitter', '~> 7.0.0' 14 | gem 'faraday' 15 | 16 | group :linkeddata do 17 | gem 'rdf' 18 | gem 'rdf-turtle' 19 | gem 'json-ld', '~> 3.2.0' 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | amazing_print (1.4.0) 7 | buftok (0.2.0) 8 | domain_name (0.5.20190701) 9 | unf (>= 0.0.5, < 1.0.0) 10 | ebnf (2.3.1) 11 | amazing_print (~> 1.4) 12 | htmlentities (~> 4.3) 13 | rdf (~> 3.2) 14 | scanf (~> 1.0) 15 | sxp (~> 1.2) 16 | unicode-types (~> 1.7) 17 | equalizer (0.0.11) 18 | erubis (2.7.0) 19 | faraday (2.2.0) 20 | faraday-net_http (~> 2.0) 21 | ruby2_keywords (>= 0.0.4) 22 | faraday-net_http (2.0.2) 23 | ffi (1.15.5) 24 | ffi-compiler (1.0.1) 25 | ffi (>= 1.0.0) 26 | rake 27 | filesize (0.2.0) 28 | htmlentities (4.3.4) 29 | http (4.4.1) 30 | addressable (~> 2.3) 31 | http-cookie (~> 1.0) 32 | http-form_data (~> 2.2) 33 | http-parser (~> 1.2.0) 34 | http-cookie (1.0.4) 35 | domain_name (~> 0.5) 36 | http-form_data (2.3.0) 37 | http-parser (1.2.3) 38 | ffi-compiler (>= 1.0, < 2.0) 39 | http_parser.rb (0.6.0) 40 | json (2.6.1) 41 | json-canonicalization (0.3.0) 42 | json-ld (3.2.0) 43 | htmlentities (~> 4.3) 44 | json-canonicalization (~> 0.3) 45 | link_header (~> 0.0, >= 0.0.8) 46 | multi_json (~> 1.15) 47 | rack (~> 2.2) 48 | rdf (~> 3.2) 49 | link_header (0.0.8) 50 | matrix (0.4.2) 51 | memoizable (0.4.2) 52 | thread_safe (~> 0.3, >= 0.3.1) 53 | mini_portile2 (2.8.0) 54 | multi_json (1.15.0) 55 | multipart-post (2.1.1) 56 | naught (1.1.0) 57 | nokogiri (1.13.4) 58 | mini_portile2 (~> 2.8.0) 59 | racc (~> 1.4) 60 | nokogiri (1.13.4-x86_64-darwin) 61 | racc (~> 1.4) 62 | public_suffix (4.0.7) 63 | racc (1.6.0) 64 | rack (2.2.3) 65 | rake (13.0.6) 66 | rdf (3.2.6) 67 | link_header (~> 0.0, >= 0.0.8) 68 | rdf-turtle (3.2.0) 69 | ebnf (~> 2.3) 70 | rdf (~> 3.2) 71 | ruby2_keywords (0.0.5) 72 | scanf (1.0.0) 73 | semverly (1.0.0) 74 | simple_oauth (0.3.1) 75 | sxp (1.2.2) 76 | matrix 77 | rdf (~> 3.2) 78 | thread_safe (0.3.6) 79 | tilt (2.0.10) 80 | twitter (7.0.0) 81 | addressable (~> 2.3) 82 | buftok (~> 0.2.0) 83 | equalizer (~> 0.0.11) 84 | http (~> 4.0) 85 | http-form_data (~> 2.0) 86 | http_parser.rb (~> 0.6.0) 87 | memoizable (~> 0.4.0) 88 | multipart-post (~> 2.0) 89 | naught (~> 1.0) 90 | simple_oauth (~> 0.3.0) 91 | unf (0.1.4) 92 | unf_ext 93 | unf_ext (0.0.8.1) 94 | unicode-types (1.7.0) 95 | webrick (1.7.0) 96 | 97 | PLATFORMS 98 | ruby 99 | x86_64-darwin-18 100 | 101 | DEPENDENCIES 102 | addressable 103 | erubis 104 | faraday 105 | filesize 106 | json 107 | json-ld (~> 3.2.0) 108 | nokogiri (~> 1.13.4) 109 | rake 110 | rdf 111 | rdf-turtle 112 | semverly 113 | tilt 114 | twitter (~> 7.0.0) 115 | webrick (> 1.4.0) 116 | 117 | BUNDLED WITH 118 | 2.3.10 119 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2016 Nicholas Humfrey 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Arduino Library List 2 | ==================== 3 | 4 | This is the code repository that generates the [arduinolibraries.info] website. 5 | 6 | It is a statically generated website. 7 | 8 | The steps to generate the site are: 9 | 10 | 1. Download the ```library_index.json``` file from arduino.cc. 11 | 2. Convert the JSON file into indexes that make it easy to generate the site 12 | 3. Generate the HTML files for the homepage and each of the sections 13 | 4. Generate a sitemap listing all the files that were generated 14 | 5. Upload the files to the web server 15 | 16 | 17 | 18 | ## Development 19 | 20 | The scripts that generate the website are written in [Ruby]. You may need to install a recent version of ruby of my computer before you can run the scripts. 21 | 22 | First make sure you have [Bundler] installed: 23 | 24 | $ gem install bundler 25 | 26 | Then install all the dependencies: 27 | 28 | $ bundle install 29 | 30 | To build a copy of the website on your local machine run: 31 | 32 | $ rake build 33 | 34 | This will download the required data, and generate all the HTML files. 35 | 36 | You can then run a web-server locally on your machine to view the site: 37 | 38 | $ rake server 39 | 40 | And then open the following URL in your browser: [http://localhost:3000/] 41 | 42 | The files that define what the pages look like are in the `views` folder. 43 | 44 | 45 | ## Contributing 46 | 47 | Bug reports and pull requests are welcome on GitHub at https://github.com/njh/arduino-libraries. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 48 | 49 | 50 | ## License 51 | 52 | The gem is available as open source under the terms of the [MIT License]. 53 | 54 | 55 | 56 | [arduinolibraries.info]: https://arduinolibraries.info/ 57 | [MIT License]: http://opensource.org/licenses/MIT 58 | [Ruby]: https://www.ruby-lang.org/ 59 | [Bundler]: http://bundler.io/ 60 | [http://localhost:3000/]: http://localhost:3000/ 61 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | 4 | def download(filename, url, accept=nil) 5 | args = [ 6 | '--fail', 7 | '--silent', 8 | '--compressed', 9 | '--location', 10 | '--show-error', 11 | '--output', filename, 12 | ] 13 | args += ['--header', "Accept: #{accept}"] unless accept.nil? 14 | args << url 15 | sh 'curl', *args 16 | end 17 | 18 | desc "Download the Library Index JSON file from arduino.cc" 19 | file 'library_index_raw.json' do |task| 20 | download(task.name, 'https://downloads.arduino.cc/libraries/library_index.json') 21 | end 22 | 23 | desc "Download extra information about authors" 24 | file 'authors_extras.csv' do |task| 25 | download(task.name, 'https://docs.google.com/spreadsheets/d/1ARqkeEmVVApylSDVZ6s_97-YtvlklE8k05F2EOlO0MY/pub?gid=465469161&single=true&output=csv') 26 | end 27 | 28 | desc "Download extra information about repositories" 29 | file 'repos_extras.csv' do |task| 30 | download(task.name, 'https://docs.google.com/spreadsheets/d/1ARqkeEmVVApylSDVZ6s_97-YtvlklE8k05F2EOlO0MY/pub?gid=278607893&single=true&output=csv') 31 | end 32 | 33 | namespace :twitter do 34 | desc "Follow everyone listed in the authors file" 35 | task :follow => 'authors_extras.csv' do 36 | ruby 'bin/twitter-follow.rb' 37 | end 38 | 39 | desc "Publish tweets about latest releases" 40 | task :publish => ['library_index_with_github.json'] do 41 | ruby 'bin/twitter-publish.rb' 42 | end 43 | end 44 | 45 | desc "Create the clean index JSON file" 46 | file 'library_index_clean.json' => ['library_index_raw.json', 'authors_extras.csv', 'repos_extras.csv', 'spdx_licences.json'] do |task| 47 | ruby 'bin/build-clean-index.rb' 48 | end 49 | 50 | desc "Download information about repos from Github" 51 | file 'github_repos.json' => 'library_index_clean.json' do |task| 52 | ruby 'bin/fetch-github-repos.rb' 53 | end 54 | 55 | desc "Download information about version tags from Github" 56 | file 'github_commits.json' => 'library_index_clean.json' do |task| 57 | ruby 'bin/fetch-github-commits.rb' 58 | end 59 | 60 | desc "Download information about users from Github" 61 | file 'github_users.json' => 'library_index_clean.json' do |task| 62 | ruby 'bin/fetch-github-users.rb' 63 | end 64 | 65 | desc "Create the index JSON file with added Github info" 66 | file 'library_index_with_github.json' => ['library_index_clean.json', 'github_repos.json', 'github_commits.json', 'github_users.json'] do |task| 67 | ruby 'bin/build-index-with-github.rb' 68 | end 69 | 70 | desc "Create Linked Data files" 71 | task :build_linkeddata => ['schema_org_context.json', 'library_index_with_github.json'] do 72 | ruby 'bin/build-linkeddata.rb' 73 | end 74 | 75 | desc "Download JSON context file from schema.org" 76 | file 'schema_org_context.json' do |task| 77 | download(task.name, 'https://schema.org/docs/jsonldcontext.json') 78 | end 79 | 80 | desc "Download SPDX license data" 81 | file 'spdx_licences.json' do |task| 82 | download( 83 | task.name, 84 | 'https://raw.githubusercontent.com/spdx/license-list-data/master/json/licenses.json' 85 | ) 86 | end 87 | 88 | desc "Create HTML files" 89 | task :build_site => ['library_index_with_github.json'] do 90 | ruby 'bin/build-site.rb' 91 | end 92 | 93 | desc "Create Aritecture Variants file" 94 | file 'public/architecture-variants.html' => ['library_index_with_github.json'] do 95 | ruby 'bin/build-architecture-variants.rb' 96 | end 97 | 98 | desc "Create RSS Feed file" 99 | file 'public/feed.xml' => ['library_index_with_github.json'] do 100 | ruby 'bin/build-rss-feed.rb' 101 | end 102 | 103 | desc "Create search index JSON file" 104 | file 'public/search-index.json' => ['library_index_with_github.json'] do 105 | ruby 'bin/build-search-index.rb' 106 | end 107 | 108 | desc "Create sitemap file" 109 | file 'public/sitemap.xml' => ['library_index_with_github.json'] do 110 | ruby 'bin/build-sitemap.rb' 111 | end 112 | 113 | desc "Generate all the required files in public" 114 | task :build => [:build_linkeddata, :build_site, 'public/architecture-variants.html', 'public/feed.xml', 'public/search-index.json', 'public/sitemap.xml'] 115 | 116 | desc "Run a local web server on port 3000" 117 | task :server => :build do 118 | require 'webrick' 119 | server = WEBrick::HTTPServer.new( 120 | :Port => 3000, 121 | :DocumentRoot => File.join(Dir.pwd, 'public') 122 | ) 123 | trap('INT') { server.shutdown } 124 | server.start 125 | end 126 | 127 | desc "Upload the files in public/ to the origin server" 128 | task :upload => :build do 129 | sh 'rsync -avz --delete --exclude ".DS_Store" -e "ssh -p 5104" public/ arduino-libs@ssh.skypi.hostedpi.com:/srv/www/arduino-libraries/' 130 | end 131 | 132 | task :default => :build 133 | 134 | desc "Deleted all the generated files (based on .gitignore)" 135 | task :clean do 136 | File.foreach('.gitignore') do |line| 137 | # For safety 138 | next unless line =~ /^\w+/ 139 | sh 'rm', '-Rf', line.strip 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /ansible/Makefile: -------------------------------------------------------------------------------- 1 | deploy: 2 | ansible-playbook --ask-become-pass --inventory=./hosts playbook.yml 3 | -------------------------------------------------------------------------------- /ansible/files/authorized_keys: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDavc1/pus2kTtuwZM4RN/E0Fh2d/0AeyUeOrTlwx+GXelOwtoR62xee1dWSfEMHt0mUnUwDss0PW3FEf7xS7iT8y9k5ohR00BzjpQ1177V4XlNhbjZMJ1jBJ9O44iWriSxMtv6IDzCnULYXc9uHeNYCDy1qMxD6uzuzk2VThKRvLVi3qSZMmxks6HpxaPGtrr4XKbyh75xgB2/6VBodUheZEQTsyyeBiZ/N+3iVjTQ870lXVisM7UWkE2/hQjCe7ID+F/wKDHTokF+c9H3ria8/LunuUXrYlC4V+spjqrM+G725OmLv3DgvUqf7UolY+fP4nSWivJL/moyEznqS8GR MacBook 2 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC2UHReZld6b7ltDapWDH6JVM+N1Oa4Sf2OY9liwfmbq4f1Sx4vs86M/+miZaJhuOcNYqUL8FJMjGRvuWRw377kwUF1fJY5KDXlWdVYrlrX9nxXPeAVqCmOa7Afgq7VGg47inxMC4dC1MREAT4dNf8ibC8lzbmr9EK+I+JYCgctmEd7hDMckSyoxSNkKGGJRFrWjMk1y5iOOpBfolIVWBLGclZa36qob0S5uP9XmXVcscfbUOVxTGCIdoGyuAjMryoblGyl0U+f6xgKhw63+lYtF0hRMoEg71qF9GqOXqLgfyJzzCFm55YC/WREYGy4Fv2xsyg2jT2f7pXJbKI5phB7 jenkins 3 | -------------------------------------------------------------------------------- /ansible/files/nginx.conf: -------------------------------------------------------------------------------- 1 | # Virtual Host configuration for arduinolibraries.info 2 | 3 | server_names_hash_bucket_size 64; 4 | server_name_in_redirect on; 5 | 6 | types { 7 | text/turtle ttl; 8 | application/ld+json jsonld; 9 | } 10 | 11 | # Redirect to correct domain and scheme 12 | server { 13 | listen [2a00:1098:8:68::a11b]:80; 14 | 15 | server_name arduinolibraries.info redirect.arduinolibraries.info; 16 | 17 | root /srv/www/empty; 18 | 19 | # Set redirects to expire in 1 year 20 | add_header Cache-Control "public,max-age=31536000"; 21 | return 301 https://www.arduinolibraries.info$request_uri; 22 | } 23 | 24 | server { 25 | listen [2a00:1098:8:68::a11b]:80; 26 | 27 | server_name www.arduinolibraries.info origin.arduinolibraries.info; 28 | 29 | root /srv/www/arduino-libraries; 30 | 31 | location / { 32 | try_files $uri $uri/index.html =404; 33 | 34 | # All responses are valid for 1 hour 35 | add_header Cache-Control "public,max-age=3600"; 36 | 37 | # Redirect requests with trailing slashes 38 | rewrite ^/(.+)/$ /$1 permanent; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ansible/hosts: -------------------------------------------------------------------------------- 1 | origin.arduinolibraries.info ansible_host=ssh.skypi.hostedpi.com ansible_port=5104 2 | -------------------------------------------------------------------------------- /ansible/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: all 4 | become: true 5 | tasks: 6 | - name: Install Debian Packages 7 | apt: 8 | name: 9 | - nginx 10 | state: present 11 | 12 | - name: "Ensure group arduino-libs exists" 13 | group: 14 | name: arduino-libs 15 | state: present 16 | 17 | - name: "Ensure user arduino-libs exists" 18 | user: 19 | name: arduino-libs 20 | comment: 'Arduino Libraries' 21 | group: arduino-libs 22 | shell: /bin/bash 23 | createhome: yes 24 | home: /home/arduino-libraries 25 | system: yes 26 | 27 | - name: 'Set permissions for .ssh' 28 | file: 29 | path: /home/arduino-libraries/.ssh 30 | state: directory 31 | owner: arduino-libs 32 | group: arduino-libs 33 | mode: 0755 34 | 35 | - name: 'Copy accross SSH authorized keys file' 36 | copy: 37 | src: authorized_keys 38 | dest: /home/arduino-libraries/.ssh/authorized_keys 39 | owner: arduino-libs 40 | group: arduino-libs 41 | mode: 0644 42 | 43 | - name: 'Create /srv/www/empty' 44 | file: 45 | path: /srv/www/empty 46 | state: directory 47 | owner: root 48 | group: root 49 | mode: 0755 50 | 51 | - name: 'Create /srv/www/arduino-libraries' 52 | file: 53 | path: /srv/www/arduino-libraries 54 | state: directory 55 | owner: arduino-libs 56 | group: arduino-libs 57 | mode: 0755 58 | 59 | - name: 'Copy accross Nginx configuration file' 60 | copy: 61 | src: nginx.conf 62 | dest: /etc/nginx/sites-available/arduino-libraries 63 | notify: 64 | - 'Restart Nginx' 65 | 66 | - name: 'Enable Nginx configuration file' 67 | file: 68 | src: /etc/nginx/sites-available/arduino-libraries 69 | dest: /etc/nginx/sites-enabled/arduino-libraries 70 | state: link 71 | notify: 72 | - 'Restart Nginx' 73 | 74 | handlers: 75 | - name: 'Restart Nginx' 76 | become: true 77 | service: name=nginx state=restarted 78 | -------------------------------------------------------------------------------- /bin/build-architecture-variants.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require './lib/helpers' 5 | require './lib/render' 6 | Bundler.require(:default) 7 | 8 | # Load the library data 9 | data = JSON.parse( 10 | File.read('library_index_with_github.json'), 11 | {:symbolize_names => true} 12 | ) 13 | 14 | 15 | architectures = {} 16 | 17 | data[:libraries].each_pair do |key, library| 18 | next if library[:architectures].nil? 19 | library[:architectures].each do |architecture| 20 | if architecture =~ /^\w+$/ 21 | architectures[architecture] ||= [] 22 | architectures[architecture] << key 23 | end 24 | end 25 | end 26 | 27 | 28 | render( 29 | "architecture-variants.html", 30 | :architecture_variants, 31 | :title => "List of library architecture variants", 32 | :architectures => architectures, 33 | :sorted_architectures => architectures.keys.sort { |a,b| 34 | architectures[b].count <=> architectures[a].count 35 | }, 36 | :libraries => data[:libraries] 37 | ) 38 | -------------------------------------------------------------------------------- /bin/build-clean-index.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | Bundler.require(:default) 5 | require './lib/helpers' 6 | require 'csv' 7 | 8 | # Limit the number of versions for each library to 10 9 | # this is to reduce the number of queries to GitHub 10 | VersionLimit = 10 11 | 12 | VersionSecificKeys = [ 13 | :version, :url, :archiveFileName, :size, :checksum 14 | ] 15 | 16 | 17 | # Load the library data 18 | source_data = JSON.parse( 19 | File.read('library_index_raw.json'), 20 | {:symbolize_names => true} 21 | ) 22 | 23 | # Load the overrides files - where the github repo name doesn't mark the library name 24 | disabled = [] 25 | reponame_overrides = {} 26 | username_overrides = {} 27 | CSV.foreach('repos_extras.csv', :headers => true) do |row| 28 | if row['type'] == 'disabled' 29 | disabled << row['key'] 30 | else 31 | reponame_overrides[row['key']] = row['reponame'] 32 | username_overrides[row['key']] = row['username'] 33 | end 34 | end 35 | 36 | author_extras = {} 37 | CSV.foreach('authors_extras.csv', :headers => true) do |row| 38 | username = row['Username'].downcase 39 | author_extras[username] ||= {} 40 | row.to_hash.each_pair do |key,value| 41 | author_extras[username][key.downcase.to_sym] = value 42 | end 43 | end 44 | 45 | valid_licenses = {} 46 | spdx_licences = JSON.parse(File.read('spdx_licences.json')) 47 | spdx_licences['licenses'].each do |license| 48 | valid_licenses[license['licenseId']] = license['name'] 49 | end 50 | 51 | 52 | data = { 53 | :libraries => {}, 54 | :types => {}, 55 | :categories => {}, 56 | :architectures => {}, 57 | :authors => {}, 58 | :licenses => {}, 59 | } 60 | 61 | # First collate the versions 62 | source_data[:libraries].each do |entry| 63 | key = entry[:name].keyize 64 | next if key.nil? or key.empty? 65 | next if disabled.include?(key) 66 | 67 | entry[:key] = key 68 | entry[:types].map! {|t| t == 'Arduino' ? 'Official' : t } 69 | entry[:semver] = SemVer.parse(entry[:version]) 70 | entry[:sentence] = strip_html(entry[:sentence]) 71 | entry[:website] = fix_url(entry[:website]) 72 | data[:libraries][key] ||= {} 73 | data[:libraries][key][:versions] ||= [] 74 | data[:libraries][key][:versions] << entry 75 | end 76 | 77 | # Sort each library by the version number 78 | data[:libraries].each_pair do |key, library| 79 | library[:versions] = library[:versions]. 80 | sort_by {|item| item[:semver]}. 81 | reverse. 82 | first(VersionLimit) 83 | end 84 | 85 | # Then take the metadata for each library from the newest version 86 | data[:libraries].each_pair do |key, library| 87 | # Copy over the non-specific version keys from the newest 88 | newest = library[:versions].first 89 | library[:version] = newest[:version] 90 | newest.keys.each do |key| 91 | unless VersionSecificKeys.include?(key) 92 | library[key] = newest[key] 93 | end 94 | end 95 | 96 | # Delete the non-specific version keys from each version 97 | library[:versions].each do |version| 98 | version.keys.each do |key| 99 | version.delete(key) unless VersionSecificKeys.include?(key) 100 | end 101 | end 102 | 103 | # Work out the Github repository location 104 | if library[:repository] =~ %r|https?://github\.com/([\w\-]+)/([\w\-]+)(\.git)?|i 105 | username, reponame = $1, $2 106 | 107 | # Check if an username override is set 108 | if username_overrides.has_key?(key) 109 | username = username_overrides[key] 110 | end 111 | 112 | # Check if an repo name override is set 113 | if reponame_overrides.has_key?(key) 114 | reponame = reponame_overrides[key] 115 | elsif library[:website] =~ %r|github\.com/#{username}/([\w\-\.]+)|i 116 | # If a website is given try using that if preference to download name 117 | reponame = $1 118 | end 119 | 120 | library[:username] = username.downcase 121 | library[:reponame] = reponame.downcase 122 | library[:github] = "https://github.com/#{username}/#{reponame}" 123 | else 124 | data[:libraries].delete(key) 125 | 126 | if library[:repository] =~ %r|https?://bitbucket\.org|i 127 | puts "Ignoring BitBucket project: #{library[:name]}" 128 | elsif library[:repository] =~ %r|https?://gitlab\.com/|i 129 | puts "Ignoring GitLab project: #{library[:name]}" 130 | else 131 | puts "Ignoring project that isn't GitHub: #{library[:name]}" 132 | end 133 | end 134 | end 135 | 136 | # Remove invalid licenses 137 | data[:libraries].each_pair do |key, library| 138 | license = library[:license] 139 | next if license.nil? 140 | unless valid_licenses.has_key?(library[:license]) 141 | $stderr.puts "Warning: removing invalid license '#{license}' for #{key}" 142 | library.delete(:license) 143 | end 144 | end 145 | 146 | # Create an index of types 147 | data[:libraries].each_pair do |key, library| 148 | library[:types].each do |type| 149 | data[:types][type] ||= [] 150 | data[:types][type] << key 151 | end 152 | end 153 | 154 | # Create an index of categories 155 | data[:libraries].each_pair do |key, library| 156 | data[:categories][library[:category]] ||= [] 157 | data[:categories][library[:category]] << key 158 | end 159 | 160 | # Create an index of architectures 161 | data[:libraries].each_pair do |key, library| 162 | next if library[:architectures].nil? 163 | library[:architectures].each do |architecture| 164 | if architecture == '*' 165 | architecture = 'Any' 166 | elsif architecture =~ /^\w+$/ 167 | architecture = architecture.downcase 168 | data[:architectures][architecture] ||= [] 169 | data[:architectures][architecture] << key 170 | end 171 | end 172 | end 173 | 174 | EMAIL_REGEXP = Regexp.new('\s*<.*>\s*') 175 | 176 | # Create an index of the Authors 177 | data[:libraries].each_pair do |key, library| 178 | # Remove email addresses 179 | library[:author].gsub!(EMAIL_REGEXP, '') 180 | library[:maintainer].gsub!(EMAIL_REGEXP, '') 181 | 182 | username = library[:username] 183 | data[:authors][username] ||= {} 184 | data[:authors][username][:name] = library[:author] 185 | data[:authors][username][:github] = "https://github.com/#{username}" 186 | data[:authors][username][:libraries] ||= [] 187 | data[:authors][username][:libraries] << key 188 | 189 | extras = author_extras[username] 190 | if extras.nil? 191 | $stderr.puts "Warning: author not found in extras file: #{username}" 192 | else 193 | data[:authors][username][:twitter] = extras[:twitter] 194 | data[:authors][username][:homepage] = fix_url(extras[:homepage]) 195 | end 196 | 197 | end 198 | 199 | 200 | # Finally, write to back to disk 201 | File.open('library_index_clean.json', 'wb') do |file| 202 | file.write JSON.pretty_generate(data) 203 | end 204 | -------------------------------------------------------------------------------- /bin/build-index-with-github.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | Bundler.require(:default) 5 | require './lib/helpers' 6 | 7 | COPY_REPO_PROPERTIES = [ 8 | :stargazers_count, 9 | :watchers_count, 10 | :forks 11 | ] 12 | 13 | # Load the library data 14 | data = JSON.parse( 15 | File.read('library_index_clean.json'), 16 | {:symbolize_names => true} 17 | ) 18 | 19 | github_repos = JSON.parse( 20 | File.read('github_repos.json'), 21 | {:symbolize_names => true} 22 | ) 23 | 24 | github_users = JSON.parse( 25 | File.read('github_users.json'), 26 | {:symbolize_names => true} 27 | ) 28 | 29 | github_commits = JSON.parse( 30 | File.read('github_commits.json'), 31 | {:symbolize_names => true} 32 | ) 33 | 34 | data[:libraries].each_pair do |key,library| 35 | github_key = "#{library[:username]}/#{library[:reponame]}" 36 | github = github_repos[github_key.to_sym] 37 | unless github.nil? 38 | COPY_REPO_PROPERTIES.each do |prop| 39 | library[prop] = github[prop] 40 | end 41 | unless github[:license].nil? 42 | library[:license] ||= github[:license][:spdx_id] 43 | end 44 | end 45 | 46 | library[:versions].each do |version| 47 | github_version_key = "#{github_key}/#{version[:version]}" 48 | github = github_commits[github_version_key.to_sym] 49 | unless github.nil? 50 | version[:github] = "#{library[:github]}/commits/#{github[:tag]}" 51 | version[:git_sha] = github[:sha] 52 | version[:release_date] = github[:commit][:committer][:date] 53 | end 54 | end 55 | 56 | if library[:versions].first[:release_date] 57 | library[:release_date] = library[:versions].first[:release_date] 58 | end 59 | end 60 | 61 | data[:authors].each_pair do |username,author| 62 | github = github_users[username.to_sym] 63 | unless github.nil? 64 | if !github[:name].nil? and github[:name] != username 65 | author[:name] = github[:name] 66 | end 67 | author[:homepage] = fix_url(github[:blog]) unless github[:blog].nil? 68 | author[:location] = github[:location] 69 | author[:company] = github[:company] 70 | if github[:twitter_username] 71 | if author[:twitter] and author[:twitter] != github[:twitter_username] 72 | $stderr.puts "Warning: differing twitter accounts" 73 | $stderr.puts " Author extras: #{author[:twitter]}" 74 | $stderr.puts " Github: #{github[:twitter_username]}" 75 | else 76 | author[:twitter] = github[:twitter_username] 77 | end 78 | end 79 | end 80 | end 81 | 82 | # Create an index of licenses 83 | data[:licenses] = {} 84 | data[:libraries].each_pair do |key, library| 85 | next unless library[:license] =~ /^[\w\-\.]+$/ 86 | license = library[:license] 87 | data[:licenses][license] ||= [] 88 | data[:licenses][license] << key 89 | end 90 | 91 | 92 | # Finally, write to back to disk 93 | File.open('library_index_with_github.json', 'wb') do |file| 94 | file.write JSON.pretty_generate(data) 95 | end 96 | -------------------------------------------------------------------------------- /bin/build-linkeddata.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require './lib/helpers' 5 | Bundler.require(:default, :linkeddata) 6 | 7 | 8 | # Load the library data 9 | data = JSON.parse( 10 | File.read('library_index_with_github.json'), 11 | {:symbolize_names => true} 12 | ) 13 | 14 | # Load the schema.org context data 15 | JSON::LD::Context.add_preloaded( 16 | 'http://schema.org/', 17 | JSON::LD::Context.new.parse('schema_org_context.json') 18 | ) 19 | 20 | FileUtils.mkdir_p("public/libraries") 21 | 22 | data[:libraries].each_pair do |key,library| 23 | newest = library[:versions].first 24 | 25 | jsonld = { 26 | '@context' => 'http://schema.org/', 27 | '@type' => 'SoftwareApplication', 28 | 'name' => library[:name], 29 | 'description' => library[:sentence], 30 | 'url' => library[:website], 31 | 'author' => { 32 | '@type' => 'Person', 33 | 'name' => library[:author], 34 | }, 35 | 'applicationCategory' => library[:category], 36 | 'operatingSystem' => 'Arduino', 37 | 'downloadUrl' => newest[:url], 38 | 'softwareVersion' => newest[:version], 39 | 'fileSize' => newest[:size].to_i / 1024, 40 | } 41 | 42 | if newest[:release_date] 43 | jsonld['datePublished'] = Time.parse(newest[:release_date]).strftime('%Y-%m-%d') 44 | end 45 | 46 | if library[:license] 47 | jsonld['license'] = "https://spdx.org/licenses/"+library[:license] 48 | end 49 | 50 | File.open("public/libraries/#{key}.json", 'wb') do |file| 51 | file.write JSON.pretty_generate(jsonld) 52 | end 53 | 54 | RDF::Turtle::Writer.open("public/libraries/#{key}.ttl") do |writer| 55 | JSON::LD::API.toRdf(jsonld) do |statement| 56 | writer << statement 57 | end 58 | end 59 | end 60 | 61 | 62 | FileUtils.mkdir_p("public/authors") 63 | 64 | data[:authors].each_pair do |key,author| 65 | jsonld = { 66 | '@context' => 'http://schema.org/', 67 | '@type' => 'Person', 68 | 'name' => author[:name], 69 | 'sameAs' => [author[:github]] 70 | } 71 | 72 | jsonld['sameAs'] << "https://twitter.com/#{author[:twitter]}" unless author[:twitter].nil? 73 | jsonld['url'] = author[:homepage] unless author[:homepage].nil? 74 | jsonld['homeLocation'] = author[:location] unless author[:location].nil? 75 | 76 | File.open("public/authors/#{key}.json", 'wb') do |file| 77 | file.write JSON.pretty_generate(jsonld) 78 | end 79 | 80 | RDF::Turtle::Writer.open("public/authors/#{key}.ttl") do |writer| 81 | JSON::LD::API.toRdf(jsonld) do |statement| 82 | writer << statement 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /bin/build-rss-feed.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require './lib/helpers' 5 | Bundler.require(:default) 6 | 7 | # Load the library data 8 | data = JSON.parse( 9 | File.read('library_index_with_github.json'), 10 | {:symbolize_names => true} 11 | ) 12 | 13 | libraries = library_sort(data[:libraries], :release_date, 50) 14 | 15 | template = Tilt::ErubisTemplate.new("views/feed.xml.erb", :escape_html => true) 16 | 17 | File.open('public/feed.xml', 'wb') do |file| 18 | file.puts template.render(self, 19 | :libraries => libraries, 20 | :self_url => "https://www.arduinolibraries.info/feed.xml", 21 | :pub_date => Time.now.iso8601.to_s 22 | ) 23 | end 24 | -------------------------------------------------------------------------------- /bin/build-search-index.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | Bundler.require(:default) 5 | 6 | # Load the library data 7 | data = JSON.parse( 8 | File.read('library_index_with_github.json'), 9 | {:symbolize_names => true} 10 | ) 11 | 12 | # Extract the fields we want in the search index 13 | index = data[:libraries].values.map do |library| 14 | { 15 | :key => library[:key], 16 | :name => library[:name], 17 | :sentence => library[:sentence] 18 | } 19 | end 20 | 21 | File.open('public/search-index.json', 'wb') do |file| 22 | file.write index.to_json 23 | end 24 | -------------------------------------------------------------------------------- /bin/build-site.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require './lib/helpers' 5 | require './lib/render' 6 | Bundler.require(:default) 7 | 8 | 9 | # Load the library data 10 | data = JSON.parse( 11 | File.read('library_index_with_github.json'), 12 | {:symbolize_names => true} 13 | ) 14 | 15 | @count = data[:libraries].keys.count 16 | @types = data[:types] 17 | @categories = data[:categories] 18 | @architectures = data[:architectures] 19 | @licenses = data[:licenses] 20 | @authors = data[:authors] 21 | 22 | render( 23 | 'index.html', 24 | :index, 25 | :title => "Arduino Library List", 26 | :description => "A catalogue of the #{@count} Arduino Libraries", 27 | :rss_url => "https://www.arduinolibraries.info/feed.xml", 28 | :most_recent => library_sort(data[:libraries], :release_date), 29 | :most_stars => library_sort(data[:libraries], :stargazers_count), 30 | :most_forked => library_sort(data[:libraries], :forks) 31 | ) 32 | 33 | render( 34 | 'libraries/index.html', 35 | :list, 36 | :title => 'All Libraries', 37 | :synopsis => "A list of the #{@count} "+ 38 | "libraries registered in the Arduino Library Manager.", 39 | :keys => data[:libraries].keys, 40 | :libraries => data[:libraries] 41 | ) 42 | 43 | data[:types].each_pair do |type,libraries| 44 | render( 45 | "types/#{type.to_s.keyize}/index.html", 46 | :list, 47 | :title => type, 48 | :synopsis => "A list of the #{libraries.count} "+ 49 | "libraries of the type #{type}.", 50 | :keys => libraries.map {|key| key.to_sym}, 51 | :libraries => data[:libraries] 52 | ) 53 | end 54 | 55 | data[:categories].each_pair do |category,libraries| 56 | render( 57 | "categories/#{category.to_s.keyize}/index.html", 58 | :list, 59 | :title => category, 60 | :synopsis => "A list of the #{libraries.count} "+ 61 | "libraries in the category #{category}.", 62 | :keys => libraries.map {|key| key.to_sym}, 63 | :libraries => data[:libraries] 64 | ) 65 | end 66 | 67 | data[:architectures].each_pair do |architecture,libraries| 68 | render( 69 | "architectures/#{architecture.to_s.keyize}/index.html", 70 | :list, 71 | :title => architecture.capitalize, 72 | :synopsis => "A list of the #{libraries.count} "+ 73 | "libraries in the architecture #{architecture}.", 74 | :keys => libraries.map {|key| key.to_sym}, 75 | :libraries => data[:libraries] 76 | ) 77 | end 78 | 79 | data[:licenses].each_pair do |license,libraries| 80 | render( 81 | "licenses/#{license.to_s.keyize}/index.html", 82 | :list, 83 | :title => license.to_s.gsub('-',' '), 84 | :synopsis => "A list of the #{libraries.count} "+ 85 | "libraries that are licensed with the #{link_to_license(license)} license.", 86 | :keys => libraries.map {|key| key.to_sym}, 87 | :libraries => data[:libraries] 88 | ) 89 | end 90 | 91 | render( 92 | "authors/index.html", 93 | :authors, 94 | :title => "List of Aurduino Library Authors", 95 | :authors => data[:authors] 96 | ) 97 | 98 | data[:authors].each_pair do |username,author| 99 | render( 100 | "authors/#{username}/index.html", 101 | :author, 102 | :title => author[:name], 103 | :username => username, 104 | :author => author, 105 | :jsonld => File.read("public/authors/#{username}.json"), 106 | :libraries => data[:libraries] 107 | ) 108 | end 109 | 110 | data[:libraries].each_pair do |key,library| 111 | render( 112 | "libraries/#{key}/index.html", 113 | :show, 114 | :title => library[:name], 115 | :description => library[:sentence], 116 | :jsonld => File.read("public/libraries/#{key}.json"), 117 | :library => library 118 | ) 119 | end 120 | -------------------------------------------------------------------------------- /bin/build-sitemap.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'find' 5 | Bundler.require(:default) 6 | 7 | # Load the library data 8 | data = JSON.parse( 9 | File.read('library_index_with_github.json'), 10 | {:symbolize_names => true} 11 | ) 12 | 13 | 14 | File.open('public/sitemap.xml', 'wb') do |file| 15 | builder = Nokogiri::XML::Builder.new 16 | builder.urlset( 17 | 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 18 | 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 19 | 'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd' 20 | ) { 21 | Find.find('public') do |path| 22 | next unless File.file?(path) and path.match(/\.html$/) 23 | path.sub!(%r|^public|, '') 24 | path.sub!(%r|/index\.html$|, '') 25 | path = '/' if path.empty? 26 | 27 | builder.url do 28 | builder.loc("https://www.arduinolibraries.info" + path) 29 | if path =~ %r|/libraries/(.+)| 30 | library = data[:libraries][$1.to_sym] 31 | if library and library[:versions].first[:release_date] 32 | builder.lastmod(library[:versions].first[:release_date]) 33 | end 34 | elsif path == '/' 35 | builder.lastmod(Time.now.utc.iso8601) 36 | builder.changefreq('daily') 37 | end 38 | end 39 | end 40 | } 41 | 42 | file.write builder.to_xml 43 | end 44 | -------------------------------------------------------------------------------- /bin/fetch-github-commits.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require './lib/github' 5 | Bundler.require(:default) 6 | 7 | 8 | 9 | # Load the library data 10 | data = JSON.parse( 11 | File.read('library_index_clean.json'), 12 | {:symbolize_names => true} 13 | ) 14 | 15 | Commits = {} 16 | if File.exist?('github_commits.json') and !File.zero?('github_commits.json') 17 | begin 18 | Commits = JSON.parse( 19 | File.read('github_commits.json'), 20 | {:symbolize_names => true} 21 | ) 22 | rescue JSON::ParserError => exp 23 | puts "Failed to parse existing commits file: #{exp}" 24 | Commits = {} 25 | end 26 | end 27 | 28 | $tags = {} 29 | def find_tag(username, reponame, version) 30 | key = [username, reponame].join('/') 31 | if $tags[key].nil? 32 | result = get_github("/repos/#{username}/#{reponame}/tags") 33 | if result.include?(:message) 34 | raise result[:message] 35 | end 36 | $tags[key] = result.map {|tag| tag[:name]} 37 | end 38 | $tags[key].each do |tag| 39 | majorminor = version.sub(/\.0$/, '') 40 | if tag =~ /^v?_?#{version}$/i 41 | return tag 42 | elsif tag =~ /^v?_?#{majorminor}$/i 43 | return tag 44 | end 45 | end 46 | 47 | return nil 48 | end 49 | 50 | do_write = false 51 | data[:libraries].each_pair do |name,library| 52 | library[:versions].each do |version| 53 | key = "#{library[:username]}/#{library[:reponame]}/#{version[:version]}" 54 | unless Commits.has_key?(key.to_sym) 55 | puts "Looking up #{name}: #{key}" 56 | 57 | tag = find_tag(library[:username], library[:reponame], version[:version]) 58 | if tag.nil? 59 | puts " => Failed to find tag for version" 60 | Commits[key] = nil 61 | do_write = true 62 | next 63 | end 64 | 65 | response = get_github("/repos/#{library[:username]}/#{library[:reponame]}/commits/#{tag}") 66 | if response.is_a?(Hash) and response[:message].nil? 67 | Commits[key] = response 68 | Commits[key][:tag] = tag 69 | do_write = true 70 | puts " => Ok" 71 | else 72 | puts " => #{response}" 73 | exit(-1) 74 | end 75 | end 76 | 77 | # Regularly write to disk, so we can re-start the script 78 | if do_write 79 | File.open('github_commits.json', 'wb') do |file| 80 | file.write JSON.pretty_generate(Commits) 81 | end 82 | do_write = false 83 | end 84 | end 85 | 86 | end 87 | -------------------------------------------------------------------------------- /bin/fetch-github-repos.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require './lib/github' 5 | Bundler.require(:default) 6 | 7 | 8 | 9 | # Load the library data 10 | data = JSON.parse( 11 | File.read('library_index_clean.json'), 12 | {:symbolize_names => true} 13 | ) 14 | 15 | if File.exist?('github_repos.json') 16 | Repos = JSON.parse( 17 | File.read('github_repos.json'), 18 | {:symbolize_names => true} 19 | ) 20 | else 21 | Repos = {} 22 | end 23 | 24 | 25 | data[:libraries].each_pair do |name,library| 26 | key = "#{library[:username]}/#{library[:reponame]}" 27 | unless Repos.has_key?(key.to_sym) 28 | puts "Fetching: #{name} => #{key}" 29 | response = get_github("/repos/#{key}") 30 | if response.is_a?(Hash) and response[:message].nil? 31 | Repos[key] = response 32 | puts " => OK" 33 | 34 | # Regularly write to disk, so we can re-start the script 35 | File.open('github_repos.json', 'wb') do |file| 36 | file.write JSON.pretty_generate(Repos) 37 | end 38 | else 39 | puts " => #{response}" 40 | exit(-1) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /bin/fetch-github-users.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require './lib/github' 5 | Bundler.require(:default) 6 | 7 | 8 | 9 | # Load the library data 10 | data = JSON.parse( 11 | File.read('library_index_clean.json'), 12 | {:symbolize_names => true} 13 | ) 14 | 15 | if File.exist?('github_users.json') 16 | Users = JSON.parse( 17 | File.read('github_users.json'), 18 | {:symbolize_names => true} 19 | ) 20 | else 21 | Users = {} 22 | end 23 | 24 | 25 | data[:authors].each_pair do |username,user| 26 | unless Users.has_key?(username.to_sym) 27 | puts "Fetching: #{username}" 28 | response = get_github("/users/#{username}") 29 | if response.is_a?(Hash) and response[:message].nil? 30 | unless response[:name].nil? 31 | response[:name].strip! 32 | response[:name] = nil if response[:name] == '' 33 | end 34 | Users[username] = response 35 | puts " => OK" 36 | 37 | # Regularly write to disk, so we can re-start the script 38 | File.open('github_users.json', 'wb') do |file| 39 | file.write JSON.pretty_generate(Users) 40 | end 41 | else 42 | puts " => #{response}" 43 | exit(-1) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /bin/twitter-follow.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | Bundler.require(:default) 5 | require './lib/twitter_config' 6 | require 'csv' 7 | 8 | following = [] 9 | $twitter.friends(:count => 200, :skip_status => true).each do |friend| 10 | following << friend.screen_name.downcase 11 | end 12 | 13 | CSV.foreach('authors_extras.csv', :headers => true) do |row| 14 | screenname = row['Twitter'] 15 | next unless screenname =~ /\w+/ 16 | screenname.downcase! 17 | 18 | puts "Following: #{screenname}" 19 | if following.include?(screenname) 20 | puts " => Already following" 21 | else 22 | begin 23 | user = $twitter.user(screenname) 24 | if user.protected? 25 | puts " => User protected" 26 | else 27 | result = $twitter.follow!(user) 28 | unless result.empty? 29 | puts " => Ok" 30 | end 31 | end 32 | rescue Twitter::Error::NotFound 33 | puts " => Not Found" 34 | end 35 | sleep 1 36 | end 37 | 38 | puts 39 | end 40 | -------------------------------------------------------------------------------- /bin/twitter-publish.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | Bundler.require(:default) 5 | require './lib/helpers' 6 | require './lib/twitter_config' 7 | 8 | # Length of a Twitter short URL 9 | TWEET_MAX_LENGTH = 280 10 | SHORT_URL_LENGTH = 23 11 | 12 | 13 | # Load the library data 14 | data = JSON.parse( 15 | File.read('library_index_with_github.json'), 16 | {:symbolize_names => true} 17 | ) 18 | 19 | # Get our recent tweets 20 | first_lines = $twitter.user_timeline( 21 | 'arduinolibs', :count => 50, :include_rts => false, :exclude_replies => true 22 | ).map {|tweet| tweet.text.split("\n").first} 23 | 24 | 25 | libraries = library_sort(data[:libraries], :release_date, 25) 26 | libraries.reverse.each do |library| 27 | author = data[:authors][library[:username].to_sym] 28 | mention = unless author[:twitter].nil? 29 | '@' + author[:twitter] 30 | else 31 | '#' + library[:username].gsub(/[^a-z0-9]/, '') 32 | end 33 | 34 | lines = ["#{library[:name]} (#{library[:version]}) for #arduino by #{mention}"] 35 | puts lines.first 36 | if first_lines.include?(lines.first) 37 | puts " => already tweeted" 38 | else 39 | remaining = TWEET_MAX_LENGTH - lines.first.length - SHORT_URL_LENGTH - 4 40 | lines << "https://arduinolibraries.info/libraries/#{library[:key]}" 41 | if remaining < 1 42 | raise "Tweet is too long" 43 | elsif remaining > 20 44 | lines << remove_links(library[:sentence].strip) 45 | if lines[2].length > remaining 46 | # Trim third line if it is too long 47 | lines[2] = lines[2][0..remaining].sub(/\s*\w+$/, ' …') 48 | end 49 | end 50 | 51 | # Send tweet 52 | puts " => sending" 53 | $twitter.update(lines.join("\n")) 54 | sleep 1 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/github.rb: -------------------------------------------------------------------------------- 1 | require 'net/https' 2 | require 'json' 3 | 4 | GITHUB_API = 'https://api.github.com/' 5 | 6 | def http_get_recursive(uri, count = 0) 7 | http = Net::HTTP.new(uri.host, uri.inferred_port) 8 | if uri.scheme == 'https' 9 | http.use_ssl = true 10 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 11 | end 12 | 13 | request = Net::HTTP::Get.new(uri.request_uri) 14 | request['Accept'] = 'application/json' 15 | request['User-Agent'] = 'arduniolibraries.info fetcher' 16 | if ENV['GITHUB_API_TOKEN'] 17 | request['Authorization'] = 'token ' + ENV['GITHUB_API_TOKEN'] 18 | else 19 | $stderr.puts "Warning: GITHUB_API_TOKEN environment variable is not set" 20 | end 21 | 22 | response = http.request(request) 23 | if response.code =~ /^3/ 24 | location = Addressable::URI.parse(response['Location']) 25 | warn "Being redirected from '#{uri}' to '#{location}'" 26 | raise "Redirected too many times" if count > 5 27 | http_get_recursive(location, count + 1) 28 | elsif response.code =~ /^2/ 29 | response 30 | else 31 | raise response.inspect 32 | end 33 | end 34 | 35 | def get_github(path, parameters=nil) 36 | uri = Addressable::URI.parse(GITHUB_API) 37 | uri.path = path 38 | uri.query_values = parameters unless parameters.nil? 39 | 40 | response = http_get_recursive(uri) 41 | if response.content_type == 'application/json' 42 | JSON.parse( 43 | response.body, 44 | {:symbolize_names => true} 45 | ) 46 | else 47 | response.body 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /lib/helpers.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def keyize 3 | self.gsub(/([A-Z]+)([A-Z][a-z])/,'\1-\2'). 4 | gsub(/([a-z\d])([A-Z])/,'\1-\2'). 5 | gsub(/\W+/,'-'). 6 | downcase 7 | end 8 | end 9 | 10 | 11 | def days_ago(timestamp) 12 | days = (Time.now - Time.parse(timestamp)) / 86400 13 | if days <= 1 14 | 'today' 15 | elsif days <= 2 16 | 'yesterday' 17 | else 18 | "#{days.floor} days ago" 19 | end 20 | end 21 | 22 | def fix_url(url) 23 | if url.nil? or url.strip == '' 24 | return nil 25 | end 26 | 27 | # Add http:// if there isn't one 28 | unless url =~ /^http/ 29 | url = "http://#{url}" 30 | end 31 | 32 | # Add a trailing slash, if there isn't one 33 | unless url =~ %r[^https?://.+/] 34 | url += '/' 35 | end 36 | 37 | if url =~ /github\.com/ 38 | # Remove www. from github URLs 39 | url.sub!(%r[https?://(www\.)?github\.com/], 'https://github.com/') 40 | 41 | # Remove .git from the end of Github urls 42 | url.sub!(%r[\.git$], '') 43 | end 44 | 45 | return url 46 | end 47 | 48 | def link_to(text, attributes={}) 49 | attributes['href'] ||= text 50 | str = attributes.to_a.map {|k,v| "#{k}='#{v}'"}.join(' ') 51 | "#{text}" 52 | end 53 | 54 | def link_to_category(category) 55 | link_to(category, :href => "/categories/#{category.keyize}") 56 | end 57 | 58 | def link_to_license(license, title=nil) 59 | if license.nil? 60 | "Unknown" 61 | else 62 | title = license.to_s.gsub('-', ' ') if title.nil? 63 | link_to(title, :href => "https://choosealicense.com/licenses/#{license.downcase}/") 64 | end 65 | end 66 | 67 | def pretty_list(list) 68 | if list.nil? 69 | "Unknown" 70 | elsif list == ['*'] 71 | "Any" 72 | else 73 | list.join(', ') 74 | end 75 | end 76 | 77 | def format_filesize(bytes) 78 | Filesize.new(bytes).pretty 79 | end 80 | 81 | def library_sort(libraries, key, limit=10) 82 | libraries.values. 83 | reject {|library| library[key].nil?}. 84 | sort_by {|library| library[key]}. 85 | reverse. 86 | slice(0, limit) 87 | end 88 | 89 | def strip_html(html) 90 | Nokogiri::HTML(html).inner_text 91 | end 92 | 93 | def remove_links(text) 94 | text.gsub(%r|(\w+)://|, '').gsub(/(\w+)\.(\w+)/, '\\1․\\2') 95 | end 96 | -------------------------------------------------------------------------------- /lib/render.rb: -------------------------------------------------------------------------------- 1 | require 'tilt' 2 | 3 | # Load the ERB templates 4 | Templates = {} 5 | Dir.foreach('views') do |filename| 6 | if filename =~ /^(\w+)\.(\w+)\.erb$/ 7 | template_key = $1.to_sym 8 | Templates[template_key] = Tilt::ErubisTemplate.new( 9 | "views/#{filename}", 10 | :escape_html => true 11 | ) 12 | end 13 | end 14 | 15 | 16 | def render(filename, template, args={}) 17 | publicpath = "public/#{filename}" 18 | dirname = File.dirname(publicpath) 19 | FileUtils.mkdir_p(dirname) unless Dir.exist?(dirname) 20 | 21 | args[:url] ||= "https://www.arduinolibraries.info/#{filename}".sub!(%r|/index.html$|, '') 22 | args[:rss_url] ||= nil 23 | args[:description] ||= nil 24 | args[:jsonld] ||= nil 25 | 26 | File.open(publicpath, 'wb') do |file| 27 | file.write Templates[:layout].render(self, args) { 28 | Templates[template].render(self, args) 29 | } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/twitter_config.rb: -------------------------------------------------------------------------------- 1 | $twitter = Twitter::REST::Client.new do |config| 2 | config.consumer_key = ENV['ARDUINOLIBS_CONSUMER_KEY'] or raise "ARDUINOLIBS_CONSUMER_KEY is not set" 3 | config.consumer_secret = ENV['ARDUINOLIBS_CONSUMER_SECRET'] or raise "ARDUINOLIBS_CONSUMER_SECRET is not set" 4 | config.access_token = ENV['ARDUINOLIBS_ACCESS_TOKEN'] or raise "ARDUINOLIBS_ACCESS_TOKEN is not set" 5 | config.access_token_secret = ENV['ARDUINOLIBS_ACCESS_TOKEN_SECRET'] or raise "ARDUINOLIBS_ACCESS_TOKEN_SECRET is not set" 6 | end 7 | -------------------------------------------------------------------------------- /public/BingSiteAuth.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27CFD8D75177D91D01A34E738DDDA3AA 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/arduino-libraries/725af21d0bfacd5713dd624de9bae64f4c36299e/public/favicon.ico -------------------------------------------------------------------------------- /public/feed-style.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | font: 92%/1.4 arial, helvetica, sans-serif; 4 | color: #2F2C2C; 5 | margin: 0px; 6 | padding: 0px; 7 | } 8 | 9 | .main 10 | { 11 | white-space: pre; 12 | padding: 5px; 13 | margin: 5px; 14 | } 15 | 16 | a 17 | { 18 | color: #35568F; 19 | text-decoration: none; 20 | } 21 | 22 | a:hover { text-decoration: underline; } 23 | 24 | .indent { margin-left: 1em; } 25 | 26 | .start-tag, .end-tag 27 | { 28 | color: purple; 29 | font-weight: bold; 30 | } 31 | 32 | .text { font-weight: normal; } 33 | 34 | .attribute-name 35 | { 36 | color: black; 37 | font-weight: bold; 38 | } 39 | 40 | .attribute-value, .attribute-quote 41 | { 42 | color: blue; 43 | font-weight: normal; 44 | } 45 | -------------------------------------------------------------------------------- /public/feed.xslt: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | XML Viewer 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 |
24 | < 25 | 26 | 27 | /> 28 |
29 |
30 | 31 | 32 |
33 | < 34 | 35 | 36 | > 37 | 38 | 39 | 40 | </ 41 | 42 | > 43 |
44 |
45 | 46 | 47 | 48 | 49 | = 50 | "" 51 | 52 | 53 | 54 | 55 | 56 | = 57 | "" 58 | 59 | 60 | 61 | 62 |
63 |
64 |
65 | 66 | 67 | 68 | 69 |
70 | -------------------------------------------------------------------------------- /public/googleaa77a87172eb8ceb.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googleaa77a87172eb8ceb.html -------------------------------------------------------------------------------- /public/js/fuse.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Fuse - Lightweight fuzzy-search 4 | * 5 | * Copyright (c) 2012-2016 Kirollos Risk . 6 | * All Rights Reserved. Apache Software License 2.0 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License") 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | !function(t){"use strict";function e(){console.log.apply(console,arguments)}function s(t,e){var s,n,o,i;for(this.list=t,this.options=e=e||{},s=0,i=["sort","shouldSort","verbose","tokenize"],n=i.length;n>s;s++)o=i[s],this.options[o]=o in e?e[o]:r[o];for(s=0,i=["searchFn","sortFn","keys","getFn","include","tokenSeparator"],n=i.length;n>s;s++)o=i[s],this.options[o]=e[o]||r[o]}function n(t,e,s){var i,r,h,a,c,p;if(e){if(h=e.indexOf("."),-1!==h?(i=e.slice(0,h),r=e.slice(h+1)):i=e,a=t[i],null!==a&&void 0!==a)if(r||"string"!=typeof a&&"number"!=typeof a)if(o(a))for(c=0,p=a.length;p>c;c++)n(a[c],r,s);else r&&n(a,r,s);else s.push(a)}else s.push(t);return s}function o(t){return"[object Array]"===Object.prototype.toString.call(t)}function i(t,e){e=e||{},this.options=e,this.options.location=e.location||i.defaultOptions.location,this.options.distance="distance"in e?e.distance:i.defaultOptions.distance,this.options.threshold="threshold"in e?e.threshold:i.defaultOptions.threshold,this.options.maxPatternLength=e.maxPatternLength||i.defaultOptions.maxPatternLength,this.pattern=e.caseSensitive?t:t.toLowerCase(),this.patternLen=t.length,this.patternLen<=this.options.maxPatternLength&&(this.matchmask=1<o;o++)this.tokenSearchers.push(new s(n[o],t));this.fullSeacher=new s(e,t)},s.prototype._startSearch=function(){var t,e,s,n,o=this.options,i=o.getFn,r=this.list,h=r.length,a=this.options.keys,c=a.length,p=null;if("string"==typeof r[0])for(s=0;h>s;s++)this._analyze("",r[s],s,s);else for(this._keyMap={},s=0;h>s;s++)for(p=r[s],n=0;c>n;n++){if(t=a[n],"string"!=typeof t){if(e=1-t.weight||1,this._keyMap[t.name]={weight:e},t.weight<=0||t.weight>1)throw new Error("Key weight has to be > 0 and <= 1");t=t.name}else this._keyMap[t]={weight:1};this._analyze(t,i(p,t,[]),p,s)}},s.prototype._analyze=function(t,s,n,i){var r,h,a,c,p,l,u,f,d,g,m,y,k,v,S,b=this.options,_=!1;if(void 0!==s&&null!==s){h=[];var M=0;if("string"==typeof s){if(r=s.split(b.tokenSeparator),b.verbose&&e("---------\nKey:",t),this.options.tokenize){for(v=0;vv;v++)c+=h[v];c/=l,b.verbose&&e("Token score average:",c)}u=this.fullSeacher.search(s),b.verbose&&e("Full text score:",u.score),p=u.score,void 0!==c&&(p=(p+c)/2),b.verbose&&e("Score average:",p),k=this.options.tokenize&&this.options.matchAllTokens?M>=this.tokenSearchers.length:!0,b.verbose&&e("Check Matches",k),(_||u.isMatch)&&k&&(a=this.resultMap[i],a?a.output.push({key:t,score:p,matchedIndices:u.matchedIndices}):(this.resultMap[i]={item:n,output:[{key:t,score:p,matchedIndices:u.matchedIndices}]},this.results.push(this.resultMap[i])))}else if(o(s))for(v=0;vs;s++)r=o[s].score,h=p?p[o[s].key].weight:1,c=r*h,1!==h?a=Math.min(a,c):(n+=c,o[s].nScore=c);1===a?l[t].score=n/i:l[t].score=a,this.options.verbose&&e(l[t])}},s.prototype._sort=function(){var t=this.options;t.shouldSort&&(t.verbose&&e("\n\nSorting...."),this.results.sort(t.sortFn))},s.prototype._format=function(){var t,s,n,o,i,r=this.options,h=r.getFn,a=[],c=this.results,p=r.include;for(r.verbose&&e("\n\nOutput:\n\n",c),o=r.id?function(t){c[t].item=h(c[t].item,r.id,[])[0]}:function(){},i=function(t){var e,s,n,o,i,r=c[t];if(p.length>0){if(e={item:r.item},-1!==p.indexOf("matches"))for(n=r.output,e.matches=[],s=0;ss;s++)o(s),t=i(s),a.push(t);return a},i.defaultOptions={location:0,distance:100,threshold:.6,maxPatternLength:32},i.prototype._calculatePatternAlphabet=function(){var t={},e=0;for(e=0;eM.maxPatternLength){if(y=t.match(new RegExp(this.pattern.replace(M.tokenSeparator,"|"))),k=!!y)for(S=[],e=0,b=y.length;b>e;e++)_=y[e],S.push([t.indexOf(_),_.length-1]);return{isMatch:k,score:k?.5:1,matchedIndices:S}}for(o=M.location,n=t.length,i=M.threshold,r=t.indexOf(this.pattern,o),v=[],e=0;n>e;e++)v[e]=0;for(-1!=r&&(i=Math.min(this._bitapScore(0,r),i),r=t.lastIndexOf(this.pattern,o+this.patternLen),-1!=r&&(i=Math.min(this._bitapScore(0,r),i))),r=-1,g=1,m=[],c=this.patternLen+n,e=0;eh;)this._bitapScore(e,o+a)<=i?h=a:c=a,a=Math.floor((c-h)/2+h);for(c=a,p=Math.max(1,o-a+1),l=Math.min(o+a,n)+this.patternLen,u=Array(l+2),u[l+1]=(1<=p;s--)if(d=this.patternAlphabet[t.charAt(s-1)],d&&(v[s-1]=1),0===e?u[s]=(u[s+1]<<1|1)&d:u[s]=(u[s+1]<<1|1)&d|((f[s+1]|f[s])<<1|1)|f[s+1],u[s]&this.matchmask&&(g=this._bitapScore(e,s-1),i>=g)){if(i=g,r=s-1,m.push(r),!(r>o))break;p=Math.max(1,2*o-r)}if(this._bitapScore(e+1,o)>i)break;f=u}return S=this._getMatchedIndices(v),{isMatch:r>=0,score:0===g?.001:g,matchedIndices:S}},i.prototype._getMatchedIndices=function(t){for(var e,s=[],n=-1,o=-1,i=0,r=t.length;r>i;i++)e=t[i],e&&-1===n?n=i:e||-1===n||(o=i-1,s.push([n,o]),n=-1);return t[i-1]&&s.push([n,i-1]),s},"object"==typeof exports?module.exports=s:"function"==typeof define&&define.amd?define(function(){return s}):t.Fuse=s}(this); -------------------------------------------------------------------------------- /public/js/search.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fuse = new Fuse([], { 4 | keys: ['key', 'name', 'sentence'], 5 | minMatchCharLength: 2, 6 | shouldSort: true, 7 | threshold: 0.2, 8 | tokenize: true 9 | }); 10 | 11 | /* FIXME: provide feedback on AJAX progress/failure? */ 12 | $.getJSON( "/search-index.json", function( data ) { 13 | fuse.set(data); 14 | }); 15 | 16 | $('#search-box').typeahead({ 17 | minLength: 2, 18 | hint: false, 19 | highlight: true, 20 | }, { 21 | name: "arduino-libraries", 22 | limit: 10, 23 | source: function(query, syncResults) { 24 | syncResults(fuse.search(query)); 25 | }, 26 | display: function (library) { 27 | return library.name 28 | }, 29 | templates: { 30 | suggestion: function (library) { 31 | return "
" + library.name + "
"; 32 | }, 33 | empty: function () { 34 | return "

No results found.

"; 35 | } 36 | } 37 | }); 38 | 39 | $('#search-box').bind('typeahead:select', function (ev, suggestion) { 40 | window.location = "/libraries/" + suggestion.key; 41 | }); 42 | -------------------------------------------------------------------------------- /public/js/typeahead.jquery.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * typeahead.js 0.11.1 3 | * https://github.com/twitter/typeahead.js 4 | * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT 5 | */ 6 | 7 | !function(a,b){"function"==typeof define&&define.amd?define("typeahead.js",["jquery"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){var b=function(){"use strict";return{isMsie:function(){return/(msie|trident)/i.test(navigator.userAgent)?navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]:!1},isBlankString:function(a){return!a||/^\s*$/.test(a)},escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isString:function(a){return"string"==typeof a},isNumber:function(a){return"number"==typeof a},isArray:a.isArray,isFunction:a.isFunction,isObject:a.isPlainObject,isUndefined:function(a){return"undefined"==typeof a},isElement:function(a){return!(!a||1!==a.nodeType)},isJQuery:function(b){return b instanceof a},toStr:function(a){return b.isUndefined(a)||null===a?"":a+""},bind:a.proxy,each:function(b,c){function d(a,b){return c(b,a)}a.each(b,d)},map:a.map,filter:a.grep,every:function(b,c){var d=!0;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?void 0:!1}),!!d):d},some:function(b,c){var d=!1;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?!1:void 0}),!!d):d},mixin:a.extend,identity:function(a){return a},clone:function(b){return a.extend(!0,{},b)},getIdGenerator:function(){var a=0;return function(){return a++}},templatify:function(b){function c(){return String(b)}return a.isFunction(b)?b:c},defer:function(a){setTimeout(a,0)},debounce:function(a,b,c){var d,e;return function(){var f,g,h=this,i=arguments;return f=function(){d=null,c||(e=a.apply(h,i))},g=c&&!d,clearTimeout(d),d=setTimeout(f,b),g&&(e=a.apply(h,i)),e}},throttle:function(a,b){var c,d,e,f,g,h;return g=0,h=function(){g=new Date,e=null,f=a.apply(c,d)},function(){var i=new Date,j=b-(i-g);return c=this,d=arguments,0>=j?(clearTimeout(e),e=null,g=i,f=a.apply(c,d)):e||(e=setTimeout(h,j)),f}},stringify:function(a){return b.isString(a)?a:JSON.stringify(a)},noop:function(){}}}(),c=function(){"use strict";function a(a){var g,h;return h=b.mixin({},f,a),g={css:e(),classes:h,html:c(h),selectors:d(h)},{css:g.css,html:g.html,classes:g.classes,selectors:g.selectors,mixin:function(a){b.mixin(a,g)}}}function c(a){return{wrapper:'',menu:'
'}}function d(a){var c={};return b.each(a,function(a,b){c[b]="."+a}),c}function e(){var a={wrapper:{position:"relative",display:"inline-block"},hint:{position:"absolute",top:"0",left:"0",borderColor:"transparent",boxShadow:"none",opacity:"1"},input:{position:"relative",verticalAlign:"top",backgroundColor:"transparent"},inputWithNoHint:{position:"relative",verticalAlign:"top"},menu:{position:"absolute",top:"100%",left:"0",zIndex:"100",display:"none"},ltr:{left:"0",right:"auto"},rtl:{left:"auto",right:" 0"}};return b.isMsie()&&b.mixin(a.input,{backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"}),a}var f={wrapper:"twitter-typeahead",input:"tt-input",hint:"tt-hint",menu:"tt-menu",dataset:"tt-dataset",suggestion:"tt-suggestion",selectable:"tt-selectable",empty:"tt-empty",open:"tt-open",cursor:"tt-cursor",highlight:"tt-highlight"};return a}(),d=function(){"use strict";function c(b){b&&b.el||a.error("EventBus initialized without el"),this.$el=a(b.el)}var d,e;return d="typeahead:",e={render:"rendered",cursorchange:"cursorchanged",select:"selected",autocomplete:"autocompleted"},b.mixin(c.prototype,{_trigger:function(b,c){var e;return e=a.Event(d+b),(c=c||[]).unshift(e),this.$el.trigger.apply(this.$el,c),e},before:function(a){var b,c;return b=[].slice.call(arguments,1),c=this._trigger("before"+a,b),c.isDefaultPrevented()},trigger:function(a){var b;this._trigger(a,[].slice.call(arguments,1)),(b=e[a])&&this._trigger(b,[].slice.call(arguments,1))}}),c}(),e=function(){"use strict";function a(a,b,c,d){var e;if(!c)return this;for(b=b.split(i),c=d?h(c,d):c,this._callbacks=this._callbacks||{};e=b.shift();)this._callbacks[e]=this._callbacks[e]||{sync:[],async:[]},this._callbacks[e][a].push(c);return this}function b(b,c,d){return a.call(this,"async",b,c,d)}function c(b,c,d){return a.call(this,"sync",b,c,d)}function d(a){var b;if(!this._callbacks)return this;for(a=a.split(i);b=a.shift();)delete this._callbacks[b];return this}function e(a){var b,c,d,e,g;if(!this._callbacks)return this;for(a=a.split(i),d=[].slice.call(arguments,1);(b=a.shift())&&(c=this._callbacks[b]);)e=f(c.sync,this,[b].concat(d)),g=f(c.async,this,[b].concat(d)),e()&&j(g);return this}function f(a,b,c){function d(){for(var d,e=0,f=a.length;!d&&f>e;e+=1)d=a[e].apply(b,c)===!1;return!d}return d}function g(){var a;return a=window.setImmediate?function(a){setImmediate(function(){a()})}:function(a){setTimeout(function(){a()},0)}}function h(a,b){return a.bind?a.bind(b):function(){a.apply(b,[].slice.call(arguments,0))}}var i=/\s+/,j=g();return{onSync:c,onAsync:b,off:d,trigger:e}}(),f=function(a){"use strict";function c(a,c,d){for(var e,f=[],g=0,h=a.length;h>g;g++)f.push(b.escapeRegExChars(a[g]));return e=d?"\\b("+f.join("|")+")\\b":"("+f.join("|")+")",c?new RegExp(e):new RegExp(e,"i")}var d={node:null,pattern:null,tagName:"strong",className:null,wordsOnly:!1,caseSensitive:!1};return function(e){function f(b){var c,d,f;return(c=h.exec(b.data))&&(f=a.createElement(e.tagName),e.className&&(f.className=e.className),d=b.splitText(c.index),d.splitText(c[0].length),f.appendChild(d.cloneNode(!0)),b.parentNode.replaceChild(f,d)),!!c}function g(a,b){for(var c,d=3,e=0;e