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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +34 -0
- data/.travis.yml +16 -0
- data/Gemfile +4 -0
- data/LICENSE.md +25 -0
- data/README.md +150 -0
- data/Rakefile +18 -0
- data/bin/mdn-query +79 -0
- data/lib/mdn_query.rb +77 -0
- data/lib/mdn_query/document.rb +36 -0
- data/lib/mdn_query/entry.rb +62 -0
- data/lib/mdn_query/errors.rb +42 -0
- data/lib/mdn_query/list.rb +82 -0
- data/lib/mdn_query/search.rb +111 -0
- data/lib/mdn_query/search_result.rb +69 -0
- data/lib/mdn_query/section.rb +98 -0
- data/lib/mdn_query/table.rb +114 -0
- data/lib/mdn_query/traverse_dom.rb +176 -0
- data/lib/mdn_query/version.rb +4 -0
- data/mdn_query.gemspec +37 -0
- data/screenshots/demo.gif +0 -0
- metadata +234 -0
@@ -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
|