open_graph_reader 0.7.2 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47077321ba2e81ce2661a44db8908de870d1d2722a2669133bb35be087fd1004
4
- data.tar.gz: db84da7bdf562ac50597da8222ed2109643db75ec47e6e3d8204f7bafb5e2872
3
+ metadata.gz: 0e7c1f4b9ce43af3f0ba0a120f14bdd14bf5f91aacac242e3c9d8f000d06b861
4
+ data.tar.gz: f8300efb032e1f687f5c99d8b53b2a553c7b6de9cd45f6c323ac86b8b9761646
5
5
  SHA512:
6
- metadata.gz: 76806aa4f7106ae2767f3c457b0a095a92d36c5647b4d36a6edb341d7d908dd53d66662d364a2803b8e7e1ffd41673729edad049dde7b2988b4da90584d94146
7
- data.tar.gz: 37dc4ecd1ba0ed18a48105820342c516d622a62bcb6857784f4c3fec880d43d6add6ab24eee1868137eb90e269e2c99fc4a746ca0ea8fa49570366de15d5fafe
6
+ metadata.gz: da884cc904c76766f0fa5e6cce33e224511be0c489c7ae99cc589a639efdfdb8c48aa90b4c51ad312f86a8133012c96efbb11f001b4a9d134c913884b697d277
7
+ data.tar.gz: '028e2821d6a628a266d8c365254518d3e3758f2ea94ab612addae5a875dd280eecc287e7c087aef3d957288581cdd4839bf6177ec21c2c576d17a07c798aa44c'
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # OpenGraphReader [![Gem Version](https://badge.fury.io/rb/open_graph_reader.svg)](http://badge.fury.io/rb/open_graph_reader) [![Build Status](https://travis-ci.org/jhass/open_graph_reader.svg?branch=master)](https://travis-ci.org/jhass/open_graph_reader)
1
+ # OpenGraphReader [![Gem Version](https://badge.fury.io/rb/open_graph_reader.svg)](http://badge.fury.io/rb/open_graph_reader)
2
2
 
3
3
  A library to fetch and parse OpenGraph properties from an URL or a given string.
4
4
 
@@ -17,14 +17,14 @@ A library to fetch and parse OpenGraph properties from an URL or a given string.
17
17
  * Objects and their properties are defined in code, not by parsing the response at the namespace identifier.
18
18
 
19
19
 
20
- Ruby 2.0 and later are supported.
20
+ Ruby 3.1 and later are supported. Compatibility with earlier versions might exist but is not verified.
21
21
 
22
22
  ## Installation
23
23
 
24
24
  Add this line to your application's Gemfile:
25
25
 
26
26
  ```ruby
27
- gem 'open_graph_reader'
27
+ gem "open_graph_reader"
28
28
  ```
29
29
 
30
30
  And then execute:
@@ -38,13 +38,13 @@ Or install it yourself as:
38
38
 
39
39
  Install the following gems the same way for a higher success rate at fetching websites:
40
40
 
41
- * [faraday_middleware](https://github.com/lostisland/faraday_middleware)
41
+ * [faraday-follow_redirects](https://github.com/tisba/faraday-follow-redirects)
42
42
  * [faraday-cookie_jar](https://github.com/miyagawa/faraday-cookie_jar)
43
43
 
44
44
  ## Usage
45
45
 
46
46
  ```ruby
47
- require 'open_graph_reader'
47
+ require "open_graph_reader"
48
48
 
49
49
  # Returns nil if anything on the object is invalid
50
50
  object = OpenGraphReader.fetch("http://examples.opengraphprotocol.us/article.html")
@@ -52,18 +52,18 @@ module OpenGraphReader
52
52
  end
53
53
 
54
54
  # @private
55
- def respond_to_missing?(method, _include_private=false)
55
+ def respond_to_missing?(method, _include_private = false)
56
56
  @bases.has_key? method.to_s
57
57
  end
58
58
 
59
59
  # Makes the found root objects available.
60
60
  # @return [Object]
61
- def method_missing(method, *args, &block)
61
+ def method_missing(method, ...)
62
62
  name = method.to_s
63
63
  if respond_to_missing? name
64
64
  @bases[name]
65
65
  else
66
- super(method, *args, &block)
66
+ super
67
67
  end
68
68
  end
69
69
  end
@@ -7,7 +7,7 @@ module OpenGraphReader
7
7
  # Well-known types from
8
8
  #
9
9
  # @see http://ogp.me
10
- KNOWN_TYPES = %w(website article book profile).freeze
10
+ KNOWN_TYPES = %w[website article book profile].freeze
11
11
 
12
12
  # Create a new builder.
13
13
  #
@@ -188,7 +188,7 @@ module OpenGraphReader
188
188
 
189
189
  def extra_properties object, type, verticals
190
190
  valid_properties = verticals[type]
191
- set_properties = object.class.available_properties.select {|property| object[property] }
191
+ set_properties = object.class.available_properties.select { |property| object[property] }
192
192
 
193
193
  set_properties - valid_properties
194
194
  end
@@ -98,15 +98,15 @@ module OpenGraphReader
98
98
 
99
99
  # Reset configuration to their defaults
100
100
  def reset_to_defaults!
101
- @strict = false
102
- @validate_required = true
103
- @validate_references = true
101
+ @strict = false
102
+ @validate_required = true
103
+ @validate_references = true
104
104
  @discard_invalid_optional_properties = false
105
- @synthesize_title = false
106
- @synthesize_url = false
107
- @synthesize_full_url = false
108
- @synthesize_image_url = false
109
- @guess_datetime_format = false
105
+ @synthesize_title = false
106
+ @synthesize_url = false
107
+ @synthesize_full_url = false
108
+ @synthesize_image_url = false
109
+ @guess_datetime_format = false
110
110
  end
111
111
  end
112
112
  end
@@ -9,7 +9,7 @@ module OpenGraphReader
9
9
 
10
10
  # @!macro property
11
11
  # @return [String]
12
- string :type, required: true, downcase: true, default: "website"
12
+ string :type, required: true, downcase: true, default: "website"
13
13
 
14
14
  # @!macro property
15
15
  # @return [String]
@@ -74,6 +74,10 @@ module OpenGraphReader
74
74
  # @return [Integer, nil]
75
75
  integer :height
76
76
 
77
+ # @!macro property
78
+ # @return [String, nil]
79
+ string :alt
80
+
77
81
  # @return [String, nil]
78
82
  def url
79
83
  secure_url || properties[:url] || content
@@ -169,7 +173,7 @@ module OpenGraphReader
169
173
 
170
174
  # @!macro property
171
175
  # @return [String, nil]
172
- enum :gender, %w(male female)
176
+ enum :gender, %w[male female]
173
177
 
174
178
  # @!macro property
175
179
  # @return [String, nil]
@@ -224,38 +228,38 @@ module OpenGraphReader
224
228
  # @return [Array<Profile>]
225
229
  # @!macro property
226
230
  # @return [Profile, nil]
227
- url :actor, to: Profile, verticals: %w(movie episode tv_show other), collection: true
231
+ url :actor, to: Profile, verticals: %w[movie episode tv_show other], collection: true
228
232
 
229
233
  # @!attribute [r] directors
230
234
  # @return [Array<Profile>]
231
235
  # @!macro property
232
236
  # @return [Profile, nil]
233
- url :director, to: Profile, verticals: %w(movie episode tv_show other), collection: true
237
+ url :director, to: Profile, verticals: %w[movie episode tv_show other], collection: true
234
238
 
235
239
  # @!attribute [r] writers
236
240
  # @return [Array<Profile>]
237
241
  # @!macro property
238
242
  # @return [Profile, nil]
239
- url :writer, to: Profile, verticals: %w(movie episode tv_show other), collection: true
243
+ url :writer, to: Profile, verticals: %w[movie episode tv_show other], collection: true
240
244
 
241
245
  # @!macro property
242
246
  # @return [Integer, nil]
243
- integer :duration, verticals: %w(movie episode tv_show other)
247
+ integer :duration, verticals: %w[movie episode tv_show other]
244
248
 
245
249
  # @!macro property
246
250
  # @return [DateTime, nil]
247
- datetime :release_date, verticals: %w(movie episode tv_show other)
251
+ datetime :release_date, verticals: %w[movie episode tv_show other]
248
252
 
249
253
  # @!attribute [r] tags
250
254
  # @return [Array<String>]
251
255
  # @!macro property
252
256
  # @return [String, nil]
253
- string :tag, verticals: %w(movie episode tv_show other), collection: true
257
+ string :tag, verticals: %w[movie episode tv_show other], collection: true
254
258
 
255
259
  # @todo validate that target vertical is video.tv_show ?
256
260
  # @!macro property
257
261
  # @return [Sring, nil]
258
- url :series, to: Video, verticals: %w(episode)
262
+ url :series, to: Video, verticals: %w[episode]
259
263
  end
260
264
 
261
265
  # @see http://ogp.me/#type_book
@@ -294,39 +298,39 @@ module OpenGraphReader
294
298
 
295
299
  # @!macro property
296
300
  # @return [Integer, nil]
297
- integer :duration, verticals: %w(song)
301
+ integer :duration, verticals: %w[song]
298
302
 
299
303
  # @todo validate that target vertical is music.album/music.song ?
300
304
  # @!attribute [r] albums
301
305
  # @return [Array<Music>]
302
306
  # @macro property
303
307
  # @return [Music, nil]
304
- url :album, to: Music, verticals: %w(song), collection: true
308
+ url :album, to: Music, verticals: %w[song], collection: true
305
309
 
306
310
  # @macro property
307
311
  # @return [Integer, nil]
308
- integer :disc, verticals: %w(song album playlist)
312
+ integer :disc, verticals: %w[song album playlist]
309
313
 
310
314
  # @macro property
311
315
  # @return [Integer, nil]
312
- integer :track, verticals: %w(song album playlist)
316
+ integer :track, verticals: %w[song album playlist]
313
317
 
314
318
  # @!attribute [r] musicians
315
319
  # @return [Array<Profile>]
316
320
  # @!macro property
317
321
  # @return [Profile, nil]
318
- url :musician, to: Profile, verticals: %w(song album), collection: true
322
+ url :musician, to: Profile, verticals: %w[song album], collection: true
319
323
 
320
324
  # @macro property
321
325
  # @return [Music, nil]
322
- url :song, to: Music, verticals: %w(album playlist)
326
+ url :song, to: Music, verticals: %w[album playlist]
323
327
 
324
328
  # @macro property
325
329
  # @return [DateTime, nil]
326
- datetime :release_date, verticals: %w(album)
330
+ datetime :release_date, verticals: %w[album]
327
331
 
328
332
  # @macro property
329
333
  # @return [Profile, nil]
330
- url :creator, to: Profile, verticals: %w(playlist radio_station)
334
+ url :creator, to: Profile, verticals: %w[playlist radio_station]
331
335
  end
332
336
  end
@@ -1,7 +1,7 @@
1
1
  require "faraday"
2
2
 
3
3
  begin
4
- require "faraday_middleware/response/follow_redirects"
4
+ require "faraday/follow_redirects"
5
5
  rescue LoadError; end
6
6
 
7
7
  begin
@@ -16,7 +16,7 @@ module OpenGraphReader
16
16
  # @api private
17
17
  class Fetcher
18
18
  HEADERS = {
19
- "Accept" => "text/html",
19
+ "Accept" => "text/html",
20
20
  "User-Agent" => "OpenGraphReader/#{OpenGraphReader::VERSION} (+https://github.com/jhass/open_graph_reader)"
21
21
  }.freeze
22
22
 
@@ -25,6 +25,7 @@ module OpenGraphReader
25
25
  # @param [URI] uri the URI to fetch.
26
26
  def initialize uri
27
27
  raise ArgumentError, "url needs to be an instance of URI" unless uri.is_a? URI
28
+
28
29
  @uri = uri
29
30
  @fetch_failed = false
30
31
  @connection = Faraday.default_connection.dup
@@ -33,7 +34,7 @@ module OpenGraphReader
33
34
  @get_response = nil
34
35
 
35
36
  prepend_middleware Faraday::CookieJar if defined? Faraday::CookieJar
36
- prepend_middleware FaradayMiddleware::FollowRedirects if defined? FaradayMiddleware
37
+ prepend_middleware Faraday::FollowRedirects::Middleware if defined? Faraday::FollowRedirects
37
38
  end
38
39
 
39
40
  # The URL to fetch
@@ -71,6 +72,7 @@ module OpenGraphReader
71
72
  fetch_body unless fetched?
72
73
  raise NoOpenGraphDataError, "No response body received for #{@uri}" if fetch_failed?
73
74
  raise NoOpenGraphDataError, "Did not receive a HTML site at #{@uri}" unless html?
75
+
74
76
  @get_response.body
75
77
  end
76
78
 
@@ -84,6 +86,7 @@ module OpenGraphReader
84
86
  return false unless response
85
87
  return false unless response.success?
86
88
  return false unless response["content-type"]
89
+
87
90
  response["content-type"].include? "text/html"
88
91
  end
89
92
 
@@ -40,8 +40,8 @@ module OpenGraphReader
40
40
  rescue
41
41
  next unless options[:required] || !OpenGraphReader.config.discard_invalid_optional_properties
42
42
  raise InvalidObjectError,
43
- "URL #{value.inspect} does not start with http:// or https:// and failed to "\
44
- "synthesize a full URL"
43
+ "URL #{value.inspect} does not start with http:// or https:// and failed to " \
44
+ "synthesize a full URL"
45
45
  end
46
46
  elsif options.has_key?(:to) && OpenGraphReader.config.validate_references
47
47
  next unless options[:required] || !OpenGraphReader.config.discard_invalid_optional_properties
@@ -67,32 +67,28 @@ module OpenGraphReader
67
67
  end
68
68
 
69
69
  # @see http://ogp.me/#integer
70
- define_type :integer do |value, options|
71
- begin
72
- Integer(value)
73
- rescue ArgumentError
74
- next unless options[:required] || !OpenGraphReader.config.discard_invalid_optional_properties
75
- raise InvalidObjectError, "Integer expected, but was #{value.inspect}"
76
- end
70
+ define_type :integer do |value, options|
71
+ Integer(value)
72
+ rescue ArgumentError
73
+ next unless options[:required] || !OpenGraphReader.config.discard_invalid_optional_properties
74
+ raise InvalidObjectError, "Integer expected, but was #{value.inspect}"
77
75
  end
78
76
 
79
77
  # @see http://ogp.me/#datetime
80
78
  define_type :datetime do |value, options|
81
- begin
82
- if OpenGraphReader.config.guess_datetime_format
83
- DateTime.parse value
84
- else
85
- DateTime.iso8601 value
86
- end
87
- rescue ArgumentError
88
- next unless options[:required] || !OpenGraphReader.config.discard_invalid_optional_properties
89
- raise InvalidObjectError, "ISO8601 datetime expected, but was #{value.inspect}"
79
+ if OpenGraphReader.config.guess_datetime_format
80
+ DateTime.parse value
81
+ else
82
+ DateTime.iso8601 value
90
83
  end
84
+ rescue ArgumentError
85
+ next unless options[:required] || !OpenGraphReader.config.discard_invalid_optional_properties
86
+ raise InvalidObjectError, "ISO8601 datetime expected, but was #{value.inspect}"
91
87
  end
92
88
 
93
89
  # @see http://ogp.me/#bool
94
90
  define_type :boolean do |value, options|
95
- {"true" => true, "false" => false, "1" => true, "0" => false}[value].tap {|bool|
91
+ {"true" => true, "false" => false, "1" => true, "0" => false}[value].tap { |bool|
96
92
  if bool.nil?
97
93
  next unless options[:required] || !OpenGraphReader.config.discard_invalid_optional_properties
98
94
  raise InvalidObjectError, "Boolean expected, but was #{value.inspect}"
@@ -102,12 +98,10 @@ module OpenGraphReader
102
98
 
103
99
  # @see http://ogp.me/#float
104
100
  define_type :float do |value, options|
105
- begin
106
- Float(value)
107
- rescue ArgumentError
108
- next unless options[:required] || !OpenGraphReader.config.discard_invalid_optional_properties
109
- raise InvalidObjectError, "Float expected, but was #{value.inspect}"
110
- end
101
+ Float(value)
102
+ rescue ArgumentError
103
+ next unless options[:required] || !OpenGraphReader.config.discard_invalid_optional_properties
104
+ raise InvalidObjectError, "Float expected, but was #{value.inspect}"
111
105
  end
112
106
  end
113
107
  end
@@ -72,7 +72,7 @@ module OpenGraphReader
72
72
  define_method(name) do
73
73
  value = children[name.to_s].first
74
74
  # @todo figure out a sane way to distinguish subobject properties
75
- value.content if value && value.is_a?(Object)
75
+ value.content if value&.is_a?(Object)
76
76
  value || options[:default]
77
77
  end
78
78
  end
@@ -130,7 +130,7 @@ module OpenGraphReader
130
130
  options = args.pop if args.last.is_a? Hash
131
131
  options ||= {}
132
132
 
133
- @content_processor = proc {|value|
133
+ @content_processor = proc { |value|
134
134
  value.downcase! if options[:downcase]
135
135
  options[:to] ||= self
136
136
  DSL.processors[type].call(value, *args, options)
@@ -164,7 +164,7 @@ module OpenGraphReader
164
164
  # @api private
165
165
  # @return [{String => Array<Strin>}]
166
166
  def verticals
167
- @verticals ||= Hash.new {|h, k| h[k] = [] }
167
+ @verticals ||= Hash.new { |h, k| h[k] = [] }
168
168
  end
169
169
  end
170
170
  end
@@ -1,6 +1,5 @@
1
1
  require "singleton"
2
2
  require "forwardable"
3
- require "set"
4
3
 
5
4
  module OpenGraphReader
6
5
  module Object
@@ -44,7 +43,7 @@ module OpenGraphReader
44
43
 
45
44
  def_delegators :@namespaces, :[]=, :has_key?
46
45
  alias_method :register, :[]=
47
- alias_method :registered?, :has_key?
46
+ alias_method :registered?, :has_key?
48
47
 
49
48
  # @see Registry.verticals
50
49
  attr_reader :verticals
@@ -39,7 +39,7 @@ module OpenGraphReader
39
39
  # Create a new object. If your class overrides this don't forget to call <tt>super</tt>.
40
40
  def initialize
41
41
  @properties = {}
42
- @children = Hash.new {|h, k| h[k] = [] }
42
+ @children = Hash.new { |h, k| h[k] = [] }
43
43
  end
44
44
 
45
45
  # Whether this object has the given property
@@ -48,7 +48,7 @@ module OpenGraphReader
48
48
  #
49
49
  # @return [String]
50
50
  def namespace
51
- parent.fullname if parent
51
+ parent&.fullname
52
52
  end
53
53
 
54
54
  # Get node's namespace as array.
@@ -69,7 +69,7 @@ module OpenGraphReader
69
69
  def inspect
70
70
  "#{super.chop} children=#{children.inspect}>"
71
71
  end
72
- alias to_s inspect
72
+ alias_method :to_s, :inspect
73
73
  end
74
74
 
75
75
  extend Forwardable
@@ -109,8 +109,8 @@ module OpenGraphReader
109
109
  # @return [Bool] Whether the given property exists in the graph.
110
110
  def exist? property
111
111
  path = property.split(":")
112
- child = path.inject(root) {|node, name|
113
- node.children.find {|child| child.name == name } || break
112
+ child = path.inject(root) { |node, name|
113
+ node.children.find { |child| child.name == name } || break
114
114
  }
115
115
  !child.nil? && !child.empty?
116
116
  end
@@ -121,7 +121,7 @@ module OpenGraphReader
121
121
  # @param [String] default The default in case the a value is not found.
122
122
  # @yield Return a default in case the value is not found. Supersedes the default parameter.
123
123
  # @return [String, Bool, Integer, Float, DateTime, nil]
124
- def fetch property, default=nil
124
+ def fetch property, default = nil
125
125
  node = find_by(property)
126
126
  return yield if node.nil? && block_given?
127
127
  return default if node.nil?
@@ -134,7 +134,7 @@ module OpenGraphReader
134
134
  # @return [Node, nil]
135
135
  def find_by property
136
136
  property = normalize_property property
137
- find {|node| node.fullname == property }
137
+ find { |node| node.fullname == property }
138
138
  end
139
139
 
140
140
  # Fetch all nodes
@@ -143,12 +143,12 @@ module OpenGraphReader
143
143
  # @return [Array<Node>]
144
144
  def select_by property
145
145
  property = normalize_property property
146
- select {|node| node.fullname == property }
146
+ select { |node| node.fullname == property }
147
147
  end
148
148
 
149
149
  def find_or_create_path path
150
- path.inject(root) {|node, name|
151
- child = node.children.reverse.find {|child| child.name == name }
150
+ path.inject(root) { |node, name|
151
+ child = node.children.reverse.find { |child| child.name == name }
152
152
 
153
153
  unless child
154
154
  child = Node.new name
@@ -11,7 +11,7 @@ module OpenGraphReader
11
11
  module XPathHelpers
12
12
  # Helper to lowercase all given properties
13
13
  def self.ci_starts_with node_set, string
14
- node_set.select {|node|
14
+ node_set.select { |node|
15
15
  node.to_s.downcase.start_with? string.downcase
16
16
  }
17
17
  end
@@ -56,7 +56,7 @@ module OpenGraphReader
56
56
  def build_graph
57
57
  graph = Graph.new
58
58
 
59
- meta_tags.each do |tag|
59
+ sorted_meta_tags.each do |tag|
60
60
  *path, leaf = tag["property"].downcase.split(":")
61
61
  node = graph.find_or_create_path path
62
62
 
@@ -67,6 +67,11 @@ module OpenGraphReader
67
67
  graph
68
68
  end
69
69
 
70
+ # Ensure the tags are sorted by their hierarchy, that is sort a:b before a:b:c.
71
+ def sorted_meta_tags
72
+ meta_tags.sort_by { |tag| tag["property"] }
73
+ end
74
+
70
75
  def meta_tags
71
76
  head = @doc.xpath("/html/head").first
72
77
 
@@ -80,7 +85,7 @@ module OpenGraphReader
80
85
 
81
86
  if head["prefix"]
82
87
  @additional_namespaces = head["prefix"].scan(/(\w+):\s*([^ ]+)/)
83
- @additional_namespaces.map! {|prefix, _| prefix.downcase }
88
+ @additional_namespaces.map! { |prefix, _| prefix.downcase }
84
89
  @additional_namespaces.each do |additional_namespace|
85
90
  next if additional_namespace == "og"
86
91
  condition << " or ci_starts_with(@property, '#{additional_namespace}')"
@@ -1,4 +1,4 @@
1
1
  module OpenGraphReader
2
- # Tbe library version
3
- VERSION = "0.7.2"
2
+ # The library version
3
+ VERSION = "0.8.0".freeze
4
4
  end
@@ -37,11 +37,11 @@ module OpenGraphReader
37
37
  # @return [Base] The base object from which you can obtain the root objects.
38
38
  # @raise [NoOpenGraphDataError] {include:NoOpenGraphDataError}
39
39
  # @raise [InvalidObjectError] {include:InvalidObjectError}
40
- def self.parse! html, origin=nil
40
+ def self.parse! html, origin = nil
41
41
  self.current_origin = origin
42
42
  parser = Parser.new html
43
43
  raise NoOpenGraphDataError, "#{origin || html} does not contain any OpenGraph tags" unless parser.any_tags?
44
- Builder.new(parser).base.tap {|base|
44
+ Builder.new(parser).base.tap { |base|
45
45
  base.origin = origin.to_s if origin
46
46
  self.current_origin = nil
47
47
  }
@@ -65,7 +65,7 @@ module OpenGraphReader
65
65
  # @param [#to_s] origin The source from where the given document was fetched.
66
66
  # @return [Base, nil] The base object from which you can obtain the root objects.
67
67
  # @see OpenGraphReader.parse!
68
- def self.parse html, origin=nil
68
+ def self.parse html, origin = nil
69
69
  parse! html, origin
70
70
  rescue NoOpenGraphDataError, InvalidObjectError
71
71
  end
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta property="og:title" content="title">
6
+ <meta property="og:description" content="desc">
7
+ <meta property="og:url" content="https://example.com">
8
+
9
+ <meta property="og:image:alt" content="image:alt">
10
+ <meta property="og:image" content="https://example.com/example.png">
11
+ </head>
12
+ <body></body>
13
+ </html>