burly 0.3.1 → 0.4.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 +4 -4
- data/README.md +24 -2
- data/burly.gemspec +1 -1
- data/lib/burly/parsers/base.rb +38 -0
- data/lib/burly/parsers/html.rb +78 -0
- data/lib/burly/parsers/json.rb +29 -0
- data/lib/burly/parsers/{plaintext_parser.rb → plaintext.rb} +2 -4
- data/lib/burly.rb +6 -18
- metadata +8 -9
- data/lib/burly/exceptions.rb +0 -5
- data/lib/burly/parser.rb +0 -25
- data/lib/burly/parsers/html_parser.rb +0 -82
- data/lib/burly/parsers/json_parser.rb +0 -38
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f2cd8eabc523f6e281042e03a815b8e71738e8befa93ec0e33805803cc3f90a
|
|
4
|
+
data.tar.gz: 68a5db717b7d48c863aaaeccf89885ecf87fb7ec34d43682d3926e4015f45a02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32401d0e85a4c205ef96bf54e21cda6f1c3ce09353bd8c18d983b618e0dfef33869cf97e2386b934ebf730188e4bb088033d0f3abf9ab79645198652c8acdc57
|
|
7
|
+
data.tar.gz: e07cd49d7f9439af7334def57a8b49c5c0023ef10f742c3879e25de8c06f96f251ae5889cab06ac0a58db1cf8e27c5ffe6c81335e9b2cd2b5ea30a1e49b5d3d5
|
data/README.md
CHANGED
|
@@ -8,9 +8,9 @@ A Ruby gem for extracting URLs from HTML, JSON, and plaintext documents.
|
|
|
8
8
|
|
|
9
9
|
## Getting Started
|
|
10
10
|
|
|
11
|
-
Before installing and using Burly, you'll want to have [Ruby](https://www.ruby-lang.org) 2.6 (or newer) installed. Using a Ruby version managment tool like [
|
|
11
|
+
Before installing and using Burly, you'll want to have [Ruby](https://www.ruby-lang.org) 2.6 (or newer) installed. Using a Ruby version managment tool like [rv](https://github.com/spinel-coop/rv) or [rbenv](https://github.com/rbenv/rbenv) is recommended.
|
|
12
12
|
|
|
13
|
-
Burly is developed using Ruby
|
|
13
|
+
Burly is developed using Ruby 4.0 and is tested against additional Ruby versions using [Forgejo Actions](https://codeberg.org/jgarber/burly/actions).
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
@@ -72,6 +72,28 @@ In all cases, Burly will search for nodes matching the provided selector(s) and
|
|
|
72
72
|
> Burly.parse(response, context: [".h-entry .e-content", ".h-entry", "body"], mime_type: "text/html")
|
|
73
73
|
> ```
|
|
74
74
|
|
|
75
|
+
## Extending Burly
|
|
76
|
+
|
|
77
|
+
New parsers may be added to Burly by subclassing `Burly::Parsers::Base` and implementing an instance method named `parse` that returns an Array of Strings.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
require "burly"
|
|
81
|
+
require "csv"
|
|
82
|
+
|
|
83
|
+
class CsvParser < Burly::Parsers::Base
|
|
84
|
+
@mime_types = ["text/csv"]
|
|
85
|
+
|
|
86
|
+
def parse
|
|
87
|
+
doc = CSV.parse(@document, headers: true)
|
|
88
|
+
|
|
89
|
+
doc.flat_map(&:fields).grep(uri_regexp)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> [!TIP]
|
|
95
|
+
> The private `uri_regexp` method available to `Burly::Parsers::Base` subclasses memoizes the regular expression provided by [`URI::RFC2396_Parser#make_regexp`](https://docs.ruby-lang.org/en/master/URI/RFC2396_Parser.html#method-i-make_regexp).
|
|
96
|
+
|
|
75
97
|
## License
|
|
76
98
|
|
|
77
99
|
This project is freely available under the [MIT License](https://opensource.org/license/MIT).
|
data/burly.gemspec
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Burly
|
|
4
|
+
module Parsers
|
|
5
|
+
# @api private
|
|
6
|
+
class Base
|
|
7
|
+
class << self
|
|
8
|
+
# @return [Array<String>]
|
|
9
|
+
attr_reader :mime_types
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [Array<Class>]
|
|
13
|
+
def self.descendants
|
|
14
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param document (see Burly.parse)
|
|
18
|
+
def initialize(document, **_kwargs)
|
|
19
|
+
@document = document.to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @raise [NotImplementedError]
|
|
23
|
+
def parse
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def uri_parser
|
|
30
|
+
@uri_parser ||= URI::RFC2396_Parser.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def uri_regexp
|
|
34
|
+
@uri_regexp ||= uri_parser.make_regexp(["http", "https"])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Burly
|
|
4
|
+
module Parsers
|
|
5
|
+
# @api private
|
|
6
|
+
class Html < Base
|
|
7
|
+
@mime_types = ["text/html"]
|
|
8
|
+
|
|
9
|
+
# An array of HTML +srcset+ attributes.
|
|
10
|
+
#
|
|
11
|
+
# @see https://html.spec.whatwg.org/#srcset-attributes HTML Standard § 4.8.4.2.1 Srcset attributes
|
|
12
|
+
SRCSET_ATTRIBUTES = ["imagesrcset", "srcset"].freeze
|
|
13
|
+
|
|
14
|
+
# A map of HTML URL attributes and their associated element names.
|
|
15
|
+
#
|
|
16
|
+
# @see https://html.spec.whatwg.org/#attributes-3 HTML Standard § Attributes
|
|
17
|
+
#
|
|
18
|
+
# rubocop:disable-next Layout/HashAlignment
|
|
19
|
+
URL_ATTRIBUTES_MAP = {
|
|
20
|
+
"action" => ["form"],
|
|
21
|
+
"cite" => ["blockquote", "del", "ins", "q"],
|
|
22
|
+
"data" => ["object"],
|
|
23
|
+
"formaction" => ["button", "input"],
|
|
24
|
+
"href" => ["a", "area", "base", "link"],
|
|
25
|
+
"imagesrcset" => ["link"],
|
|
26
|
+
"ping" => ["a", "area"],
|
|
27
|
+
"poster" => ["video"],
|
|
28
|
+
"src" => ["audio", "embed", "iframe", "img", "input", "script", "source", "track", "video"],
|
|
29
|
+
"srcset" => ["img", "source"],
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# @param document (see Burly.parse)
|
|
33
|
+
# @param context [String, Array<String>]
|
|
34
|
+
def initialize(document, context: nil)
|
|
35
|
+
@context = context
|
|
36
|
+
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Parse an HTML document for absolute or relative URLs.
|
|
41
|
+
#
|
|
42
|
+
# @return [Array<String>]
|
|
43
|
+
def parse
|
|
44
|
+
context_node
|
|
45
|
+
.xpath(*xpaths_from_url_attributes_map)
|
|
46
|
+
.lazy
|
|
47
|
+
.flat_map { |node| parse_attribute_node(node) }
|
|
48
|
+
.to_a
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# @return [Nokogiri::HTML5::Document, Nokogiri::XML::Element]
|
|
54
|
+
def context_node
|
|
55
|
+
@context_node ||= doc.search(*Array(@context)).first || doc
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Nokogiri::HTML5::Document]
|
|
59
|
+
def doc
|
|
60
|
+
@doc ||= Nokogiri.HTML5(@document)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [String, Array<String>]
|
|
64
|
+
def parse_attribute_node(node)
|
|
65
|
+
return node.value.strip unless SRCSET_ATTRIBUTES.include?(node.name)
|
|
66
|
+
|
|
67
|
+
node.value.split(/\s*,\s*/).lazy.flat_map { |value| value.strip.split(/\s|$/)[0] }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Array<String>]
|
|
71
|
+
def xpaths_from_url_attributes_map
|
|
72
|
+
URL_ATTRIBUTES_MAP.each_with_object([]) do |(attribute, names), xpaths|
|
|
73
|
+
xpaths.concat(names.map { |name| ".//#{name}/@#{attribute}" })
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Burly
|
|
4
|
+
module Parsers
|
|
5
|
+
# @api private
|
|
6
|
+
class Json < Base
|
|
7
|
+
@mime_types = ["application/json"]
|
|
8
|
+
|
|
9
|
+
# Parse a JSON document for string values whose content are absolute URLs.
|
|
10
|
+
#
|
|
11
|
+
# @return [Array<String>]
|
|
12
|
+
def parse
|
|
13
|
+
recursive_parse(JSON.parse(@document)).to_a.compact
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def recursive_parse(*objs)
|
|
19
|
+
objs.lazy.flat_map do |obj|
|
|
20
|
+
case obj
|
|
21
|
+
when Array then recursive_parse(*obj)
|
|
22
|
+
when Hash then recursive_parse(obj.values)
|
|
23
|
+
when String then obj if obj.match?(uri_regexp)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -3,16 +3,14 @@
|
|
|
3
3
|
module Burly
|
|
4
4
|
module Parsers
|
|
5
5
|
# @api private
|
|
6
|
-
class
|
|
6
|
+
class Plaintext < Base
|
|
7
7
|
@mime_types = ["text/plain"]
|
|
8
8
|
|
|
9
|
-
Burly.register_parser(self)
|
|
10
|
-
|
|
11
9
|
# Parse a plaintext document for absolute URLs.
|
|
12
10
|
#
|
|
13
11
|
# @return [Array<String>]
|
|
14
12
|
def parse
|
|
15
|
-
|
|
13
|
+
uri_parser.extract(@document, ["http", "https"])
|
|
16
14
|
end
|
|
17
15
|
end
|
|
18
16
|
end
|
data/lib/burly.rb
CHANGED
|
@@ -5,16 +5,13 @@ require "uri"
|
|
|
5
5
|
|
|
6
6
|
require "nokogiri"
|
|
7
7
|
|
|
8
|
-
require_relative "burly/
|
|
9
|
-
require_relative "burly/
|
|
8
|
+
require_relative "burly/parsers/base"
|
|
9
|
+
require_relative "burly/parsers/html"
|
|
10
|
+
require_relative "burly/parsers/json"
|
|
11
|
+
require_relative "burly/parsers/plaintext"
|
|
10
12
|
|
|
11
13
|
module Burly
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class << self
|
|
15
|
-
# @api private
|
|
16
|
-
attr_reader :registered_parsers
|
|
17
|
-
end
|
|
14
|
+
class UnsupportedMimeType < StandardError; end
|
|
18
15
|
|
|
19
16
|
# Parse a document for URLs.
|
|
20
17
|
#
|
|
@@ -34,19 +31,10 @@ module Burly
|
|
|
34
31
|
#
|
|
35
32
|
# @return [Array<String>]
|
|
36
33
|
def self.parse(document, mime_type: "text/plain", **options)
|
|
37
|
-
parser =
|
|
34
|
+
parser = Parsers::Base.descendants.find { |parser| parser.mime_types.include?(mime_type) }
|
|
38
35
|
|
|
39
36
|
raise UnsupportedMimeType unless parser
|
|
40
37
|
|
|
41
38
|
parser.new(document, **options).parse
|
|
42
39
|
end
|
|
43
|
-
|
|
44
|
-
# @api private
|
|
45
|
-
def self.register_parser(klass)
|
|
46
|
-
klass.mime_types.each { |mime_type| @registered_parsers[mime_type] = klass }
|
|
47
|
-
end
|
|
48
40
|
end
|
|
49
|
-
|
|
50
|
-
require_relative "burly/parsers/html_parser"
|
|
51
|
-
require_relative "burly/parsers/json_parser"
|
|
52
|
-
require_relative "burly/parsers/plaintext_parser"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: burly
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jason Garber
|
|
@@ -34,21 +34,20 @@ files:
|
|
|
34
34
|
- README.md
|
|
35
35
|
- burly.gemspec
|
|
36
36
|
- lib/burly.rb
|
|
37
|
-
- lib/burly/
|
|
38
|
-
- lib/burly/
|
|
39
|
-
- lib/burly/parsers/
|
|
40
|
-
- lib/burly/parsers/
|
|
41
|
-
- lib/burly/parsers/plaintext_parser.rb
|
|
37
|
+
- lib/burly/parsers/base.rb
|
|
38
|
+
- lib/burly/parsers/html.rb
|
|
39
|
+
- lib/burly/parsers/json.rb
|
|
40
|
+
- lib/burly/parsers/plaintext.rb
|
|
42
41
|
homepage: https://codeberg.org/jgarber/burly
|
|
43
42
|
licenses:
|
|
44
43
|
- MIT
|
|
45
44
|
metadata:
|
|
46
45
|
bug_tracker_uri: https://codeberg.org/jgarber/burly/issues
|
|
47
|
-
changelog_uri: https://codeberg.org/jgarber/burly/releases/tag/v0.
|
|
48
|
-
documentation_uri: https://rubydoc.info/gems/burly/0.
|
|
46
|
+
changelog_uri: https://codeberg.org/jgarber/burly/releases/tag/v0.4.0
|
|
47
|
+
documentation_uri: https://rubydoc.info/gems/burly/0.4.0
|
|
49
48
|
homepage_uri: https://codeberg.org/jgarber/burly
|
|
50
49
|
rubygems_mfa_required: 'true'
|
|
51
|
-
source_code_uri: https://codeberg.org/jgarber/burly/src/tag/v0.
|
|
50
|
+
source_code_uri: https://codeberg.org/jgarber/burly/src/tag/v0.4.0
|
|
52
51
|
rdoc_options: []
|
|
53
52
|
require_paths:
|
|
54
53
|
- lib
|
data/lib/burly/exceptions.rb
DELETED
data/lib/burly/parser.rb
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Burly
|
|
4
|
-
# @api private
|
|
5
|
-
class Parser
|
|
6
|
-
URI_PARSER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::Generic::DEFAULT_PARSER
|
|
7
|
-
|
|
8
|
-
URI_REGEXP = URI_PARSER.make_regexp(["http", "https"])
|
|
9
|
-
|
|
10
|
-
class << self
|
|
11
|
-
# @return [Array<String>]
|
|
12
|
-
attr_reader :mime_types
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# @param document (see Burly.parse)
|
|
16
|
-
def initialize(document, **_kwargs)
|
|
17
|
-
@document = document.to_s
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# @raise [NotImplementedError]
|
|
21
|
-
def parse
|
|
22
|
-
raise NotImplementedError
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Burly
|
|
4
|
-
module Parsers
|
|
5
|
-
# @api private
|
|
6
|
-
class HtmlParser < Parser
|
|
7
|
-
@mime_types = ["text/html"]
|
|
8
|
-
|
|
9
|
-
Burly.register_parser(self)
|
|
10
|
-
|
|
11
|
-
# A map of HTML +srcset+ attributes and their associated element names.
|
|
12
|
-
#
|
|
13
|
-
# @see https://html.spec.whatwg.org/#srcset-attributes
|
|
14
|
-
# @see https://html.spec.whatwg.org/#attributes-3
|
|
15
|
-
SRCSET_ATTRIBUTES_MAP = {
|
|
16
|
-
"imagesrcset" => ["link"],
|
|
17
|
-
"srcset" => ["img", "source"],
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
20
|
-
# A map of HTML URL attributes and their associated element names.
|
|
21
|
-
#
|
|
22
|
-
# @see https://html.spec.whatwg.org/#attributes-3
|
|
23
|
-
URL_ATTRIBUTES_MAP = {
|
|
24
|
-
"action" => ["form"],
|
|
25
|
-
"cite" => ["blockquote", "del", "ins", "q"],
|
|
26
|
-
"data" => ["object"],
|
|
27
|
-
"formaction" => ["button", "input"],
|
|
28
|
-
"href" => ["a", "area", "base", "link"],
|
|
29
|
-
"ping" => ["a", "area"],
|
|
30
|
-
"poster" => ["video"],
|
|
31
|
-
"src" => ["audio", "embed", "iframe", "img", "input", "script", "source", "track", "video"],
|
|
32
|
-
}.freeze
|
|
33
|
-
|
|
34
|
-
ATTRIBUTES_XPATHS =
|
|
35
|
-
URL_ATTRIBUTES_MAP.merge(SRCSET_ATTRIBUTES_MAP).flat_map do |attribute, names|
|
|
36
|
-
names.map { |name| ".//#{name} / @#{attribute}" }
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# @param document (see Burly.parse)
|
|
40
|
-
# @param context [String, Array<String>]
|
|
41
|
-
def initialize(document, context: nil)
|
|
42
|
-
@context = context
|
|
43
|
-
|
|
44
|
-
super
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Parse an HTML document for absolute or relative URLs.
|
|
48
|
-
#
|
|
49
|
-
# @return [Array<String>]
|
|
50
|
-
def parse
|
|
51
|
-
attr_nodes.flat_map do |attr_node|
|
|
52
|
-
if SRCSET_ATTRIBUTES_MAP.key?(attr_node.name)
|
|
53
|
-
urls_from_candidate_strings(attr_node.value.split(/\s*,\s*/))
|
|
54
|
-
else
|
|
55
|
-
attr_node.value.strip
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
# @return [Nokogiri::XML::NodeSet]
|
|
63
|
-
def attr_nodes
|
|
64
|
-
@attr_nodes ||= context_node.xpath(*ATTRIBUTES_XPATHS)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# @return [Nokogiri::HTML5::Document, Nokogiri::XML::Element]
|
|
68
|
-
def context_node
|
|
69
|
-
@context_node ||= doc.search(*Array(@context)).first || doc
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# @return [Nokogiri::HTML5::Document]
|
|
73
|
-
def doc
|
|
74
|
-
@doc ||= Nokogiri.HTML5(@document)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def urls_from_candidate_strings(candidate_strings)
|
|
78
|
-
candidate_strings.flat_map { |candidate_string| candidate_string.strip.split(/\s|$/)[0] }
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Burly
|
|
4
|
-
module Parsers
|
|
5
|
-
# @api private
|
|
6
|
-
class JsonParser < Parser
|
|
7
|
-
@mime_types = ["application/json"]
|
|
8
|
-
|
|
9
|
-
Burly.register_parser(self)
|
|
10
|
-
|
|
11
|
-
# Parse a JSON document for string values whose contents are absolute
|
|
12
|
-
# URLs.
|
|
13
|
-
#
|
|
14
|
-
# @return [Array<String>]
|
|
15
|
-
def parse
|
|
16
|
-
recursive_parse(JSON.parse(@document))
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def recursive_parse(*objs)
|
|
22
|
-
objs.flat_map do |obj|
|
|
23
|
-
if obj.is_a?(Array)
|
|
24
|
-
obj.map! { |value| recursive_parse(value) }
|
|
25
|
-
obj.flatten!
|
|
26
|
-
obj.compact!
|
|
27
|
-
|
|
28
|
-
return obj
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
return recursive_parse(obj.values) if obj.is_a?(Hash)
|
|
32
|
-
|
|
33
|
-
obj if obj.is_a?(String) && obj.match?(URI_REGEXP)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|