mdn_query 0.1.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.
@@ -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