urban_dictionary 0.0.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MmQ5NjJjOTM5OTBmYWRkMjI0Nzc5ODE5ZGNlZGZiYTljZTNmZjFjZA==
5
+ data.tar.gz: !binary |-
6
+ MzQxMDhhNDMxZmNlM2U1ZWUyNjI4OGNmMWEwMjkwOWRlYzQxMjhhZg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NGRmMWY0Y2MwMWM2NWMyYmMwMjI3NTU2ZDcyNjJjOWJkNGI2NDFlZjNlOGVl
10
+ MGI1ODIyZTAyN2FhODNkYjIyODhiNGQ5NWEzMmI0OTNlZGQ3Yzg4ZDA5Mzg2
11
+ Y2M3ZjQ4ZDY2ZjZlNDhjNDE4NWEyOWI2YTU0MjI2OWJkMjI2M2Y=
12
+ data.tar.gz: !binary |-
13
+ MjZlMWZhMjA2OWMzY2NhZGI0YzFlZGRmNmIwNTA4YzNlZDQyNTFjMzAxODY0
14
+ NmZiNmJkOWU5MWI0ZDgxNjFmNTQxNzFiYTRjMTM1NDQzZTg5MTYyZDRhNzdl
15
+ MWVjZTM5ZjVmMmM3NDExOGRmMGY3MzM5ZjRkOTA2YjBhYTZhNmQ=
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  *.gem
2
+ Gemfile.lock
2
3
  pkg/*
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ - 2.2.4
5
+ - 2.1.8
6
+ - 2.0.0
7
+ - 1.9.3
8
+ - jruby-19mode # JRuby in 1.9 mode
9
+ - rbx-3.14
10
+ before_install:
11
+ - gem install bundler
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 - 2016-02-15
4
+ - Fixed broken parsing of Urban Dictionary's new layout
5
+ - Fixed bug where undefined words are displayed as the shrug emoticon (`¯\_(ツ)_/¯`)
6
+ - Added support for --format=json to output JSON
7
+ - Added MIT License
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in urban_dictionary.gemspec
4
4
  gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Ryan Greenberg
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.
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  `urban_dictionary` is a Ruby gem to access word definitions and examples from [Urban Dictionary](http://www.urbandictionary.com/). It also provides a command-line tool for getting definitions.
4
4
 
5
+ [![Build Status](https://travis-ci.org/ryangreenberg/urban_dictionary.svg?branch=master)](https://travis-ci.org/ryangreenberg/urban_dictionary)
6
+
5
7
  ## Installation ##
6
8
 
7
9
  Run `gem install urban_dictionary`.
@@ -35,9 +37,21 @@ You can use the `--random` flag to get the definition of random word:
35
37
 
36
38
  > urban_dictionary --random
37
39
 
40
+ Specify the output format with `--format`. The default is `plain`; `json` is also supported.
41
+
42
+ > urban_dictionary --format=json one hundred | jq .
43
+ {
44
+ "word": "one hundred",
45
+ "entries": [
46
+ {
47
+ "definition": "keepin it real to the fullest and super tight.",
48
+ "example": "end of the convo\rLuke: aight man, peace\rQ: kool homie, keep it one hundred (100)"
49
+ },
50
+ ...
51
+ }
52
+
38
53
  ## Tests ##
39
54
 
40
55
  Run examples with:
41
56
 
42
57
  rspec
43
-
data/Rakefile CHANGED
@@ -1 +1,5 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
data/bin/urban_dictionary CHANGED
@@ -1,38 +1,5 @@
1
1
  #!/usr/bin/env ruby -KU
2
- require 'urban_dictionary'
3
- require 'optparse'
2
+ require_relative '../lib/urban_dictionary'
4
3
 
5
- options = {}
6
- option_parser = OptionParser.new do |opts|
7
- opts.banner = "Usage: urban_dictionary <word or phrase>"
8
- opts.version = UrbanDictionary::VERSION
9
-
10
- opts.on("-r", "--random", "Define a random word") do |r|
11
- options[:random] = r
12
- end
13
- end
14
- option_parser.parse(ARGV)
15
-
16
- if ARGV.empty? && !options[:random]
17
- puts option_parser.help
18
- exit(0)
19
- end
20
-
21
- word = if options[:random]
22
- UrbanDictionary.random_word
23
- else
24
- UrbanDictionary.define(ARGV.join(" "))
25
- end
26
-
27
- output = []
28
- output << word
29
- output << '-' * word.size
30
- output << ''
31
- word.entries.each_with_index do |entry, i|
32
- output << "#{i + 1}. #{entry.definition}"
33
- output << ""
34
- output << "Example: #{entry.example}"
35
- output << ""
36
- output << ""
37
- end
38
- puts output.join("\n")
4
+ config = UrbanDictionary::CLI::Config.new(:args => ARGV)
5
+ UrbanDictionary::CLI.new(config).run
@@ -0,0 +1,106 @@
1
+ require 'optparse'
2
+
3
+ module UrbanDictionary
4
+ class CLI
5
+ class Config
6
+ # Constructor accepts an option hash with the following properties:
7
+ PROPERTIES = [
8
+ :args, # ARGV input from the user (required)
9
+ :stdout, # IO object for writing to stdout (optional)
10
+ :stderr, # IO object for writing to stderr (optional)
11
+ :dictionary, # Word-lookup object that implements .random_word and .define (optional)
12
+ ]
13
+
14
+ attr_reader *PROPERTIES
15
+
16
+ def initialize(options)
17
+ required(:args, options)
18
+ optional(:stdout, options, STDOUT)
19
+ optional(:stderr, options, STDERR)
20
+ optional(:dictionary, options, UrbanDictionary)
21
+ end
22
+
23
+ def update(property, value)
24
+ if PROPERTIES.include?(property)
25
+ set(property, value)
26
+ else
27
+ raise ArgumentError, "#{property} is not a valid property (#{PROPERTIES.inspect})."
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def required(property, hsh)
34
+ unless hsh.include?(property)
35
+ raise ArgumentError, "#{hsh.inspect} does not include required property #{property}"
36
+ end
37
+ set(property, hsh[property])
38
+ end
39
+
40
+ def optional(property, hsh, default)
41
+ value = hsh.include?(property) ? hsh[property] : default
42
+ set(property, value)
43
+ end
44
+
45
+ def set(property, value)
46
+ instance_variable_set("@#{property}", value)
47
+ end
48
+ end
49
+
50
+ DEFAULT_FORMAT = :plain
51
+
52
+ attr_reader :options
53
+
54
+ def initialize(config)
55
+ @args = config.args
56
+ @stdout = config.stdout
57
+ @stderr = config.stderr
58
+ @dictionary = config.dictionary
59
+ @options = {}
60
+ end
61
+
62
+ def option_parser
63
+ @option_parser ||= OptionParser.new do |opts|
64
+ opts.banner = "Usage: urban_dictionary <word or phrase>"
65
+ opts.version = UrbanDictionary::VERSION
66
+
67
+ opts.on("-r", "--random", "Define a random word") do |r|
68
+ options[:random] = r
69
+ end
70
+
71
+ options[:format] = DEFAULT_FORMAT
72
+ opts.on("-f", "--format=FORMAT", "Output format (plain, json)") do |f|
73
+ format = f.downcase.to_sym
74
+ unless UrbanDictionary::Formatter.registered.include?(format)
75
+ raise OptionParser::InvalidOption, "#{f} is not a valid format"
76
+ end
77
+ options[:format] = format
78
+ end
79
+ end
80
+ end
81
+
82
+ def run
83
+ options[:remaining] = option_parser.parse(@args)
84
+
85
+ if options[:remaining].empty? && !options[:random]
86
+ @stdout.puts option_parser.help
87
+ return
88
+ end
89
+
90
+ term = options[:remaining].join(" ")
91
+ word = if options[:random]
92
+ @dictionary.random_word
93
+ else
94
+ @dictionary.define(term)
95
+ end
96
+
97
+ if word.nil?
98
+ @stderr.puts "No definition found for '#{term}'"
99
+ exit(1)
100
+ end
101
+
102
+ formatter = UrbanDictionary::Formatter.for(options[:format])
103
+ @stdout.puts(formatter.format(word))
104
+ end
105
+ end
106
+ end
@@ -10,5 +10,9 @@ module UrbanDictionary
10
10
  def to_s
11
11
  "#{definition}\n#{example}"
12
12
  end
13
+
14
+ def inspect
15
+ "<#{self.class} @definition=#{definition.inspect}, @example=#{example.inspect}>"
16
+ end
13
17
  end
14
18
  end
@@ -0,0 +1,58 @@
1
+ require 'multi_json'
2
+
3
+ module UrbanDictionary
4
+ class Formatter
5
+ def self.register(name, klass)
6
+ @formatters ||= {}
7
+ if @formatters.include?(name)
8
+ raise RuntimeError, "Formatter #{@formatters[name]} already registered for '#{name}'"
9
+ else
10
+ @formatters[name] = klass
11
+ end
12
+ end
13
+
14
+ def self.for(name)
15
+ @formatters[name]
16
+ end
17
+
18
+ def self.registered
19
+ @formatters.keys.map()
20
+ end
21
+
22
+ def self.format(word)
23
+ raise NotImplementedError, "#format has not been implemented by #{self}"
24
+ end
25
+ end
26
+
27
+ class PlainFormatter < Formatter
28
+ def self.format(word)
29
+ output = []
30
+ output << word
31
+ output << '-' * word.size
32
+ output << ''
33
+ word.entries.each_with_index do |entry, i|
34
+ output << "#{i + 1}. #{entry.definition}"
35
+ output << ""
36
+ output << "Example: #{entry.example}"
37
+ output << ""
38
+ output << ""
39
+ end
40
+ output.join("\n")
41
+ end
42
+ end
43
+
44
+ class JsonFormatter < Formatter
45
+ def self.format(word)
46
+ hsh = {
47
+ :word => word.word,
48
+ :entries => word.entries.map do |entry|
49
+ {
50
+ :definition => entry.definition,
51
+ :example => entry.example
52
+ }
53
+ end
54
+ }
55
+ MultiJson.dump(hsh)
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,3 @@
1
1
  module UrbanDictionary
2
- VERSION = "0.0.2"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,3 +1,5 @@
1
+ # encoding: UTF-8
2
+
1
3
  require 'nokogiri'
2
4
  require 'open-uri'
3
5
 
@@ -5,20 +7,36 @@ module UrbanDictionary
5
7
  class Word
6
8
  attr_reader :word, :entries
7
9
 
8
- # Can raise SocketError if unable to connect to specified URL
10
+ # Can raise SocketError if unable to connect to specified URL
9
11
  def self.from_url(url)
10
- html = open(url).read
11
- doc = Nokogiri::HTML(html)
12
+ html = open(url) {|f| f.read }
13
+ from_html(html)
14
+ end
15
+
16
+ def self.from_html(html)
17
+ doc = Nokogiri.HTML(html)
18
+ words = doc.css('.word')
19
+ return nil if words.empty?
20
+
21
+ word = words.first.content.strip
22
+ definitions = doc.css('div.meaning').map {|d| d.content.strip }
23
+ examples = doc.css('.example').map {|e| e.content.strip }
24
+ entries = definitions.zip(examples).map {|d,e| Entry.new(d, e) }
12
25
 
13
- if doc.css('.word').any?
14
- word = doc.css('.word').first.content.strip
15
- definitions = doc.css('.definition').map{|d| d.content.strip }
16
- examples = doc.css('.example').map{|e| e.content.strip }
17
- entries = definitions.zip(examples).map{|d,e| Entry.new(d, e)}
26
+ defined_word?(word, definitions) ? Word.new(word, entries) : nil
27
+ end
18
28
 
19
- Word.new(word, entries)
20
- end
29
+ # Currently when a word has no definition the result page returns the
30
+ # shrug emoticon (¯\_(ツ)_/¯) and the text "There aren't any definitions
31
+ # for [word] yet."
32
+ def self.defined_word?(word, definitions)
33
+ undefined_word = (
34
+ definitions.size == 1 &&
35
+ definitions[0] =~ /^There aren't any definitions for/
36
+ )
37
+ !undefined_word
21
38
  end
39
+ private_class_method :defined_word?
22
40
 
23
41
  def initialize(word, entries)
24
42
  @word = word
@@ -29,9 +47,14 @@ module UrbanDictionary
29
47
  @word
30
48
  end
31
49
 
50
+ def inspect
51
+ "<#{self.class} @word=#{word.inspect}, @entries=#{entries.inspect}>"
52
+ end
53
+
32
54
  def size
33
55
  @word.size
34
56
  end
35
57
  alias :length :size
36
58
  end
37
- end
59
+ end
60
+
@@ -1,9 +1,11 @@
1
1
  require 'uri'
2
2
  require 'net/http'
3
3
 
4
- require 'urban_dictionary/version'
5
- require 'urban_dictionary/word'
6
- require 'urban_dictionary/entry'
4
+ require_relative 'urban_dictionary/version'
5
+ require_relative 'urban_dictionary/cli'
6
+ require_relative 'urban_dictionary/entry'
7
+ require_relative 'urban_dictionary/formatters'
8
+ require_relative 'urban_dictionary/word'
7
9
 
8
10
  module UrbanDictionary
9
11
  DEFINE_URL = 'http://www.urbandictionary.com/define.php'
@@ -16,10 +18,13 @@ module UrbanDictionary
16
18
  def self.random_word
17
19
  url = URI.parse(RANDOM_URL)
18
20
  req = Net::HTTP::Get.new(url.path)
19
- rsp = Net::HTTP.start(url.host, url.port) {|http|
21
+ rsp = Net::HTTP.start(url.host, url.port) do |http|
20
22
  http.request(req)
21
- }
23
+ end
22
24
 
23
- define(rsp['location'])
25
+ Word.from_url(rsp['location'])
24
26
  end
25
- end
27
+ end
28
+
29
+ UrbanDictionary::Formatter.register(:json, UrbanDictionary::JsonFormatter)
30
+ UrbanDictionary::Formatter.register(:plain, UrbanDictionary::PlainFormatter)
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Save a given URL as a test case in the spec/html directory
4
+
5
+ require 'nokogiri'
6
+ require 'open-uri'
7
+ require 'uri'
8
+ require 'cgi'
9
+ require 'time'
10
+
11
+ USAGE = "Usage: #{$PROGRAM_NAME} url"
12
+
13
+ url = ARGV[0]
14
+ abort USAGE unless url
15
+
16
+ html = open(url) {|f| f.read }
17
+ file_name = begin
18
+ uri = URI.parse(url)
19
+ params = CGI.parse(uri.query || "")
20
+ base_name = if params.include?('term')
21
+ params['term'][0]
22
+ else
23
+ "#{uri.host}#{uri.path}"
24
+ end
25
+
26
+ %|#{base_name.downcase.gsub(/[^a-z0-9]/i, '_')}_#{Time.now.strftime("%Y-%m-%d")}.html|
27
+ end
28
+ file_path = File.join(File.expand_path('..', __FILE__), file_name)
29
+
30
+ doc = Nokogiri.HTML(html)
31
+
32
+ # Remove script tags and SVG elements to cut down on size
33
+ doc.css('script,g').remove
34
+
35
+ File.open(file_path, 'w') { |f| f << doc.to_html }