mdn_query 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,62 @@
1
+ module MdnQuery
2
+ # An entry of the Mozilla Developer Network documentation.
3
+ class Entry
4
+ # @return [String]
5
+ attr_reader :title, :description, :url
6
+
7
+ # Creates a new entry.
8
+ #
9
+ # @param title [String] the title of the entry
10
+ # @param description [String] a small excerpt of the entry
11
+ # @param url [String] the URL to the entry on the web
12
+ # @return [MdnQuery::Entry]
13
+ def initialize(title, description, url)
14
+ @title = title
15
+ @description = description
16
+ @url = url
17
+ end
18
+
19
+ # Returns the string representation of the entry.
20
+ #
21
+ # @return [String]
22
+ def to_s
23
+ "#{title}\n#{description}\n#{url}"
24
+ end
25
+
26
+ # Opens the entry in the default web browser.
27
+ #
28
+ # @return [void]
29
+ def open
30
+ Launchy.open(@url)
31
+ end
32
+
33
+ # Returns the content of the entry.
34
+ #
35
+ # The content is fetched from the Mozilla Developer Network's documentation.
36
+ # The fetch occurs only once and subsequent calls return the previously
37
+ # fetched content.
38
+ #
39
+ # @raise [MdnQuery::HttpRequestFailed] if a HTTP request fails
40
+ # @return [MdnQuery::Document] the content of the entry
41
+ def content
42
+ return @content unless @content.nil?
43
+ @content = retrieve(url)
44
+ end
45
+
46
+ private
47
+
48
+ def retrieve(url)
49
+ begin
50
+ response = RestClient::Request.execute(method: :get, url: url,
51
+ headers: { accept: 'text/html' })
52
+ rescue RestClient::Exception, SocketError => e
53
+ raise MdnQuery::HttpRequestFailed.new(url, e),
54
+ 'Could not retrieve entry'
55
+ end
56
+ dom = Nokogiri::HTML(response.body)
57
+ title = dom.css('h1').text
58
+ article = dom.css('article')
59
+ MdnQuery::TraverseDom.create_document(article, title, url)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,42 @@
1
+ module MdnQuery
2
+ # The standard error.
3
+ class Error < StandardError; end
4
+
5
+ # The error when no entries were found.
6
+ class NoEntryFound < MdnQuery::Error
7
+ # @return [String] the query that was searched for
8
+ attr_reader :query
9
+
10
+ # @return [Hash] the options used for the search
11
+ attr_reader :options
12
+
13
+ # Creates a new NoEntryFound error.
14
+ #
15
+ # @param query [String] the query that was searched for
16
+ # @param options [Hash] the options used for the search
17
+ # @return [MdnQuery::NoEntryFound]
18
+ def initialize(query, options = {})
19
+ @query = query
20
+ @options = options
21
+ end
22
+ end
23
+
24
+ # The error for failed HTTP request of any kind.
25
+ class HttpRequestFailed < MdnQuery::Error
26
+ # @return [String] the URL of the request
27
+ attr_reader :url
28
+
29
+ # @return [SocketError, RestClient::Exception] the original error
30
+ attr_reader :http_error
31
+
32
+ # Creates a new HttpRequestFailed error.
33
+ #
34
+ # @param url [String] the URL of the request
35
+ # @param error [SocketError, RestClient::Exception] the original error
36
+ # @return [MdnQuery::HttpRequestFailed]
37
+ def initialize(url, error)
38
+ @url = url
39
+ @http_error = error
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,82 @@
1
+ module MdnQuery
2
+ # A list from a search result.
3
+ class List
4
+ # @return [String] the query that was searched for
5
+ attr_reader :query
6
+
7
+ # @return [Array<MdnQuery::Entry>]
8
+ attr_reader :items
9
+
10
+ # Creates a new list of search results.
11
+ #
12
+ # @param query [String] the query that was searched for
13
+ # @param items [MdnQuery::Entry] the items in the list
14
+ # @return [MdnQuery::List]
15
+ def initialize(query, *items)
16
+ items = [] if items.nil?
17
+ @query = query
18
+ @items = items
19
+ end
20
+
21
+ # Returns the first item in the list.
22
+ #
23
+ # @return [MdnQuery::Entry] the first item
24
+ def first
25
+ items.first
26
+ end
27
+
28
+ # Retrieves the item at the given position.
29
+ #
30
+ # @param pos [Fixnum] the position of the item
31
+ # @return [MdnQuery::Entry] the item at position `pos`
32
+ def [](pos)
33
+ items[pos]
34
+ end
35
+
36
+ # Returns whether the list is empty.
37
+ #
38
+ # @return [Boolean] whether the list is empty
39
+ def empty?
40
+ items.empty?
41
+ end
42
+
43
+ # Returns the number of items in the list.
44
+ #
45
+ # @return [Fixnum] the number of items
46
+ def size
47
+ items.size
48
+ end
49
+
50
+ # Calls the given block for every item.
51
+ #
52
+ # @param block [Block] block to be executed for every item.
53
+ # @return [void]
54
+ def each(&block)
55
+ items.each(&block)
56
+ end
57
+
58
+ # Returns the string representation of the list.
59
+ #
60
+ # @return [String]
61
+ def to_s
62
+ return "No results for '#{query}'" if empty?
63
+ "Results for '#{query}':\n#{number_items(items).join("\n")}\n"
64
+ end
65
+
66
+ private
67
+
68
+ def number_items(items)
69
+ num_width = items.size / 10 + 1
70
+
71
+ items.map.with_index do |item, index|
72
+ entry = "#{(index + 1).to_s.rjust(num_width)}) "
73
+ entry << pad_left(item.to_s, num_width + 2)
74
+ end
75
+ end
76
+
77
+ def pad_left(str, num)
78
+ pad = ' ' * num
79
+ str.gsub("\n", "\n#{pad}")
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,111 @@
1
+ module MdnQuery
2
+ # A search request to the Mozilla Developer Network documentation.
3
+ class Search
4
+ # @return [String] a search option (see {#initialize})
5
+ attr_accessor :css_classnames, :locale, :highlight, :html_attributes,
6
+ :query, :result, :topics
7
+
8
+ # rubocop:disable Metrics/LineLength
9
+
10
+ # Creates a new search.
11
+ #
12
+ # The search request is not automatically executed (use {#execute}).
13
+ #
14
+ # @param query [String] the query to search for
15
+ # @param options [Hash] the search query options (more informations on
16
+ # {https://developer.mozilla.org/en-US/docs/MDN/Contribute/Tools/Search#Search_query_format})
17
+ # @option options :css_classnames [String] the CSS classes to match
18
+ # @option options :highlight [Boolean] whether the query is highlighted
19
+ # @option options :html_attributes [String] the HTML attribute text to match
20
+ # @option options :locale [String] the locale to match against
21
+ # @option options :topics [Array<String>] the topics to search in
22
+ # @return [MdnQuery::Search]
23
+ def initialize(query, options = {})
24
+ @url = "#{MdnQuery::BASE_URL}.json"
25
+ @query = query
26
+ @css_classnames = options[:css_classnames]
27
+ @locale = options[:locale] || 'en-US'
28
+ @highlight = options[:highlight] || false
29
+ @html_attributes = options[:html_attributes]
30
+ @topics = options[:topics] || ['js']
31
+ end
32
+ # rubocop:enable Metrics/LineLength
33
+
34
+ # Creates the URL used for the request.
35
+ #
36
+ # @return [String] the full URL
37
+ def url
38
+ query_url = "#{@url}?q=#{@query}&locale=#{@locale}"
39
+ query_url << @topics.map { |t| "&topic=#{t}" }.join
40
+ unless @css_classnames.nil?
41
+ query_url << "&css_classnames=#{@css_classnames}"
42
+ end
43
+ unless @html_attributes.nil?
44
+ query_url << "&html_attributes=#{@html_attributes}"
45
+ end
46
+ query_url << "&highlight=#{@highlight}" unless @highlight.nil?
47
+ query_url
48
+ end
49
+
50
+ # Executes the search request.
51
+ #
52
+ # @return [MdnQuery::SearchResult] the search result
53
+ def execute
54
+ @result = retrieve(url, @query)
55
+ end
56
+
57
+ # Fetches the next page of the search result.
58
+ #
59
+ # If there is no search result yet, {#execute} will be called instead.
60
+ #
61
+ # @return [MdnQuery::SearchResult] if a new result has been acquired
62
+ # @return [nil] if there is no next page
63
+ def next_page
64
+ if @result.nil?
65
+ execute
66
+ elsif @result.next?
67
+ query_url = url
68
+ query_url << "&page=#{@result.current_page + 1}"
69
+ @result = retrieve(query_url, @query)
70
+ end
71
+ end
72
+
73
+ # Fetches the previous page of the search result.
74
+ #
75
+ # If there is no search result yet, {#execute} will be called instead.
76
+ #
77
+ # @return [MdnQuery::SearchResult] if a new result has been acquired
78
+ # @return [nil] if there is no previous page
79
+ def previous_page
80
+ if @result.nil?
81
+ execute
82
+ elsif @result.previous?
83
+ query_url = url
84
+ query_url << "&page=#{@result.current_page - 1}"
85
+ @result = retrieve(query_url, @query)
86
+ end
87
+ end
88
+
89
+ # Opens the search in the default web browser.
90
+ #
91
+ # @return [void]
92
+ def open
93
+ html_url = url.sub('.json?', '?')
94
+ Launchy.open(html_url)
95
+ end
96
+
97
+ private
98
+
99
+ def retrieve(url, query)
100
+ begin
101
+ response = RestClient::Request.execute(method: :get, url: url,
102
+ headers: { accept: 'json' })
103
+ rescue RestClient::Exception, SocketError => e
104
+ raise MdnQuery::HttpRequestFailed.new(url, e),
105
+ 'Could not retrieve search result'
106
+ end
107
+ json = JSON.parse(response.body, symbolize_names: true)
108
+ MdnQuery::SearchResult.new(query, json)
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,69 @@
1
+ module MdnQuery
2
+ # A result from a search query.
3
+ class SearchResult
4
+ # @return [Array<Hash>] the raw items of the search result
5
+ attr_reader :items
6
+
7
+ # @return [Hash] information about the pages
8
+ attr_reader :pages
9
+
10
+ # @return [String] the query that was searched for
11
+ attr_reader :query
12
+
13
+ # @return [Fixnum] the total number of entries
14
+ attr_reader :total
15
+
16
+ # Creates a new search result.
17
+ #
18
+ # @param query [String] the query that was searched for
19
+ # @param json [Hash] the hash version of the JSON response
20
+ # @return [MdnQuery::SearchResult]
21
+ def initialize(query, json)
22
+ @query = query
23
+ @pages = {
24
+ count: json[:pages] || 0,
25
+ current: json[:page]
26
+ }
27
+ @total = json[:count]
28
+ @items = json[:documents]
29
+ end
30
+
31
+ # Returns whether there are any entries.
32
+ #
33
+ # @return [Boolean]
34
+ def empty?
35
+ @pages[:count].zero?
36
+ end
37
+
38
+ # Returns whether there is a next page.
39
+ #
40
+ # @return [Boolean]
41
+ def next?
42
+ !empty? && @pages[:current] < @pages[:count]
43
+ end
44
+
45
+ # Returns whether there is a previous page.
46
+ #
47
+ # @return [Boolean]
48
+ def previous?
49
+ !empty? && @pages[:current] > 1
50
+ end
51
+
52
+ # Returns the number of the current page.
53
+ #
54
+ # @return [Fixnum]
55
+ def current_page
56
+ @pages[:current]
57
+ end
58
+
59
+ # Creates a list with the items.
60
+ #
61
+ # @return [MdnQuery::List]
62
+ def to_list
63
+ items = @items.map do |i|
64
+ MdnQuery::Entry.new(i[:title], i[:excerpt], i[:url])
65
+ end
66
+ MdnQuery::List.new(query, *items)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,98 @@
1
+ module MdnQuery
2
+ # A section of an entry of the Mozilla Developer Network documentation.
3
+ class Section
4
+ # @return [Array<MdnQuery::Section>] the list of child sections
5
+ attr_reader :children
6
+
7
+ # @return [String] the name and title of the section
8
+ attr_reader :name
9
+
10
+ # @return [Fixnum] the level of the section
11
+ attr_reader :level
12
+
13
+ # @return [MdnQuery::Section] the parent section
14
+ attr_reader :parent
15
+
16
+ # @return [Array<String>] the text segments of the section
17
+ attr_reader :text
18
+
19
+ # Creates a new section.
20
+ #
21
+ # @param name [String] the name and title of the section
22
+ # @param level [Fixnum] the level of the section
23
+ # @param parent [MdnQuery::Section] the parent section
24
+ # @return [MdnQuery::Section]
25
+ def initialize(name, level: 1, parent: nil)
26
+ @name = name
27
+ @level = level
28
+ @parent = parent
29
+ @text = []
30
+ @children = []
31
+ end
32
+
33
+ # Creates a new child section.
34
+ #
35
+ # @param name [String] the name and title of the child section
36
+ # @return [MdnQuery::Section] the new child section
37
+ def create_child(name)
38
+ child = MdnQuery::Section.new(name, parent: self, level: @level + 1)
39
+ @children << child
40
+ child
41
+ end
42
+
43
+ # Appends a text segment to the section.
44
+ #
45
+ # Spaces before and after newlines are removed. If the text segment is empty
46
+ # (i.e. consists of just whitespaces), it is not appended.
47
+ #
48
+ # @param text [String] the text segment to append
49
+ # @return [void]
50
+ def append_text(text)
51
+ trimmed_text = text.gsub(/\n[[:blank:]]+|[[:blank:]]+\n/, "\n")
52
+ @text << trimmed_text unless text_empty?(trimmed_text)
53
+ end
54
+
55
+ # Appends a code segment to the section.
56
+ #
57
+ # If the code segment is empty (i.e. consists of just whitespaces), it is
58
+ # not appended. The given snippet is embedded in a Markdown code block.
59
+ #
60
+ # @example Add a JavaScript snippet
61
+ # append_code("const name = 'My Name';", language: 'javascript')
62
+ # # adds the following text:
63
+ # # ```javascript
64
+ # # const name = 'My Name';
65
+ # # ```
66
+ #
67
+ # @param snippet [String] the code segment to append
68
+ # @param language [String] the language of the code
69
+ # @return [void]
70
+ def append_code(snippet, language: '')
71
+ @text << "\n```#{language}\n#{snippet}\n```\n" unless text_empty?(snippet)
72
+ end
73
+
74
+ # Returns the string representation of the section.
75
+ #
76
+ # @return [String]
77
+ def to_s
78
+ str = "#{'#' * level} #{name}\n\n#{join_text}\n\n#{join_children}\n"
79
+ str.gsub!(/\n+[[:blank:]]+\n+|\n{3,}/, "\n\n")
80
+ str.strip!
81
+ str
82
+ end
83
+
84
+ private
85
+
86
+ def join_text
87
+ text.join("\n")
88
+ end
89
+
90
+ def join_children
91
+ children.map(&:to_s).join("\n\n")
92
+ end
93
+
94
+ def text_empty?(text)
95
+ !text.match(/\A\s*\z/).nil?
96
+ end
97
+ end
98
+ end