fede 0.1.2
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/bin/fede +24 -0
- data/lib/fede/generator.rb +286 -0
- data/lib/fede/xml_feed.rb +29 -0
- data/lib/fede/xml_node.rb +76 -0
- data/lib/fede.rb +33 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a752e40bbfd39587eb3274f7218b258fab48beecb7980ac2dbe94e860c068949
|
4
|
+
data.tar.gz: 2681a19b6935dd2362b5221d2d64200b2fffcae18308621c8aa3a9cc915fb64e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e823a5bbf61bc740129397248f508c050cd8d2273c763aad3fd2e97631a4cdb6b043703c83915e963feb6d57d6c77e9d021bc7f4c38e128f3ee956dae1777505
|
7
|
+
data.tar.gz: 2a4cb095a593c5faa6a404ad17bf8920b1a439b1deecd64ba088b91bf25749721428673ec074f63e443612ef978117abf71382d1ae2d1fbdf405a3d8c8a7bb71
|
data/bin/fede
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fede'
|
4
|
+
|
5
|
+
def show_help
|
6
|
+
puts 'Usage: fede <config_file> <data_dir> [mode]'
|
7
|
+
puts ' modes:'
|
8
|
+
puts ' generate: generate feed from scratch'
|
9
|
+
puts ' append-available: append the items that have the audio files present'
|
10
|
+
puts ' append[-n]: appends the last -n items to the end of the feed, default is 1'
|
11
|
+
puts ' example: fede config_file data_dir append-3 (append last 3 items)'
|
12
|
+
exit
|
13
|
+
end
|
14
|
+
|
15
|
+
show_help unless ARGV[0]
|
16
|
+
show_help unless ARGV[1]
|
17
|
+
mode = ARGV[2] || 'generate'
|
18
|
+
|
19
|
+
case ARGV[0]
|
20
|
+
when '-h'
|
21
|
+
show_help
|
22
|
+
end
|
23
|
+
|
24
|
+
Fede.run config_file: ARGV[0], data_dir: ARGV[1], mode: mode
|
@@ -0,0 +1,286 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
class Fede
|
5
|
+
class FeedGenerator
|
6
|
+
def initialize(site_config, data_directory)
|
7
|
+
@config = parse_yaml site_config
|
8
|
+
@feed_file = get_setting 'feed_file'
|
9
|
+
@data = parse_data data_directory
|
10
|
+
@episode_list = []
|
11
|
+
@ep_name = get_setting 'ep_name'
|
12
|
+
@ep_pub_date = get_setting 'ep_pub_date'
|
13
|
+
@ep_url = get_setting 'ep_url'
|
14
|
+
@ep_desc = get_setting 'ep_desc'
|
15
|
+
@ep_img = get_setting 'ep_img'
|
16
|
+
@ep_details = get_setting 'ep_details'
|
17
|
+
@season_name = get_setting 'season_name'
|
18
|
+
@season_episode_list = get_setting 'season_episode_list'
|
19
|
+
load_episode_list
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate
|
23
|
+
output_feed
|
24
|
+
puts "#{@feed_file} written!"
|
25
|
+
end
|
26
|
+
|
27
|
+
def append(item_count = 1)
|
28
|
+
last_n_episodes = []
|
29
|
+
item_count.times.sort_by(&:-@).each do |i|
|
30
|
+
last_n_episodes << generate_episode_item(@episode_list[-i - 1])
|
31
|
+
end
|
32
|
+
do_append last_n_episodes
|
33
|
+
end
|
34
|
+
|
35
|
+
def append_available_files
|
36
|
+
episodes = []
|
37
|
+
@episode_list.each do |ep|
|
38
|
+
next unless File.file? ep['url']
|
39
|
+
|
40
|
+
episodes << ep
|
41
|
+
end
|
42
|
+
do_append episodes
|
43
|
+
end
|
44
|
+
|
45
|
+
def do_append(last_n_episodes)
|
46
|
+
File.open(@feed_file, 'r+') do |file|
|
47
|
+
insert_position = Kernel.loop do
|
48
|
+
pos = file.pos
|
49
|
+
break pos if file.gets.include?('</channel>') || file.eof?
|
50
|
+
end
|
51
|
+
file.seek(insert_position, IO::SEEK_SET)
|
52
|
+
footer = file.read
|
53
|
+
file.seek(insert_position, IO::SEEK_SET)
|
54
|
+
episodes_string = last_n_episodes.reduce('') { |prev, ep| "#{prev}#{ep.to_s(2)}" }
|
55
|
+
file.write("#{episodes_string}#{footer}")
|
56
|
+
end
|
57
|
+
puts "Last #{last_n_episodes.length} episode(s) appended to #{@feed_file}!"
|
58
|
+
rescue Errno::ENOENT
|
59
|
+
puts "Cannot append if feed doesn't exist"
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def parse_data(path)
|
65
|
+
data = {}
|
66
|
+
Dir.entries(path).each do |file|
|
67
|
+
next if ['.', '..'].include? file
|
68
|
+
|
69
|
+
data[file.split('.')[0]] = YAML.load_file "#{path}/#{file}"
|
70
|
+
end
|
71
|
+
data
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_yaml(file)
|
75
|
+
YAML.load_file file
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_setting(setting_name)
|
79
|
+
setting = @config['podcast'][setting_name] || @config[setting_name]
|
80
|
+
if setting.nil?
|
81
|
+
raise StandardError, "Error: setting #{setting_name} is not defined in the config file, cannot continue"
|
82
|
+
end
|
83
|
+
|
84
|
+
setting
|
85
|
+
end
|
86
|
+
|
87
|
+
def output_feed
|
88
|
+
build_date = DateTime.now.strftime(get_setting('datetime_format_string'))
|
89
|
+
base_url = get_setting 'url'
|
90
|
+
managing_editor = get_setting 'managing_editor'
|
91
|
+
editor_email = get_setting 'editor_email'
|
92
|
+
desc = get_setting 'description'
|
93
|
+
|
94
|
+
feed = XMLFeed.new
|
95
|
+
channel = XMLNode.new 'channel'
|
96
|
+
atom_link = XMLNode.new 'atom:link'
|
97
|
+
atom_link.set_propperty 'href', "#{base_url}/#{@feed_file}"
|
98
|
+
atom_link.set_propperty 'rel', 'self'
|
99
|
+
atom_link.set_propperty 'type', 'application/rss+xml'
|
100
|
+
channel.add_child atom_link
|
101
|
+
channel.add_child XMLNode.new 'title', content: get_setting('title')
|
102
|
+
channel.add_child XMLNode.new 'pubDate', content: @episode_list[0]['pub_date']
|
103
|
+
channel.add_child XMLNode.new 'lastBuildDate', content: build_date
|
104
|
+
channel.add_child XMLNode.new 'link', content: base_url
|
105
|
+
channel.add_child XMLNode.new 'language', content: 'pt'
|
106
|
+
channel.add_child(
|
107
|
+
XMLNode.new(
|
108
|
+
'copyright',
|
109
|
+
cdata: true,
|
110
|
+
content: "#{get_setting('title')} #{DateTime.now.year}, #{get_setting('copyright')}."
|
111
|
+
)
|
112
|
+
)
|
113
|
+
channel.add_child XMLNode.new('docs', content: base_url)
|
114
|
+
channel.add_child XMLNode.new('managingEditor', content: "#{editor_email} (#{managing_editor})")
|
115
|
+
channel.add_child XMLNode.new('itunes:summary', cdata: true, content: desc)
|
116
|
+
image = XMLNode.new 'image'
|
117
|
+
image.add_child XMLNode.new('url', content: "#{base_url}#{get_setting('logo')}")
|
118
|
+
image.add_child XMLNode.new('title', content: get_setting('title'))
|
119
|
+
image.add_child XMLNode.new('link', cdata: true, content: base_url)
|
120
|
+
channel.add_child image
|
121
|
+
channel.add_child XMLNode.new('itunes:author', content: get_setting('author'))
|
122
|
+
category = XMLNode.new 'itunes:category'
|
123
|
+
category.set_propperty 'text', 'Society & Culture'
|
124
|
+
channel.add_child category
|
125
|
+
channel.add_child XMLNode.new('itunes:keywords', content: get_setting('keywords'))
|
126
|
+
channel.add_child XMLNode.new('itunes:image', propperties: { href: "#{base_url}#{get_setting('logo')}" })
|
127
|
+
channel.add_child XMLNode.new('itunes:explicit', content: true)
|
128
|
+
owner = XMLNode.new 'itunes:owner'
|
129
|
+
owner.add_child XMLNode.new('itunes:email', content: get_setting('email'))
|
130
|
+
owner.add_child XMLNode.new('itunes:name', content: get_setting('title'))
|
131
|
+
channel.add_child owner
|
132
|
+
channel.add_child(XMLNode.new('description', content: desc, cdata: true))
|
133
|
+
channel.add_child(
|
134
|
+
XMLNode.new(
|
135
|
+
'itunes:subtitle',
|
136
|
+
content: get_setting('short_description'),
|
137
|
+
cdata: true
|
138
|
+
)
|
139
|
+
)
|
140
|
+
channel.add_child XMLNode.new('itunes:type', content: 'episodic')
|
141
|
+
channel.add_child XMLNode.new('itunes:new-feed-url', content: "#{base_url}/#{@feed_file}")
|
142
|
+
channel.add_children generate_all_episode_items
|
143
|
+
|
144
|
+
feed.nodes << channel
|
145
|
+
File.open(@feed_file, 'w') { |file| file.write(feed) }
|
146
|
+
end
|
147
|
+
|
148
|
+
def generate_episode_item(episode)
|
149
|
+
description = format_description episode[@ep_desc], details: episode[@ep_details], indent_level: 3
|
150
|
+
subtitle = format_subtitle episode[@ep_desc]
|
151
|
+
|
152
|
+
current_item = XMLNode.new 'item'
|
153
|
+
current_item.add_child XMLNode.new('guid', content: "#{get_setting('url')}#{episode[@ep_url]}")
|
154
|
+
current_item.add_child XMLNode.new('title', content: episode[@ep_name])
|
155
|
+
current_item.add_child XMLNode.new('pubDate', content: episode[@ep_pub_date])
|
156
|
+
current_item.add_child(XMLNode.new('link', cdata: true, content: "#{get_setting('url')}#{episode[@ep_url]}"))
|
157
|
+
current_item.add_child XMLNode.new(
|
158
|
+
'itunes:image',
|
159
|
+
propperties: { href: "#{get_setting('url')}#{episode_image(episode)}" }
|
160
|
+
)
|
161
|
+
current_item.add_child XMLNode.new('description', cdata: true, content: description)
|
162
|
+
current_item.add_child(
|
163
|
+
XMLNode.new(
|
164
|
+
'enclosure',
|
165
|
+
propperties: {
|
166
|
+
length: episode_bytes_length(episode),
|
167
|
+
type: 'audio/mpeg',
|
168
|
+
url: "#{get_setting('url')}#{episode[@ep_url]}"
|
169
|
+
}
|
170
|
+
)
|
171
|
+
)
|
172
|
+
current_item.add_child XMLNode.new 'itunes:duration', content: episode_duration(episode)
|
173
|
+
current_item.add_child XMLNode.new 'itunes:explicit', content: true
|
174
|
+
current_item.add_child XMLNode.new(
|
175
|
+
'itunes:keywords',
|
176
|
+
content: get_setting('keywords')
|
177
|
+
)
|
178
|
+
current_item.add_child XMLNode.new 'itunes:subtitle', content: subtitle, cdata: true
|
179
|
+
current_item.add_child XMLNode.new 'itunes:episodeType', content: 'full'
|
180
|
+
|
181
|
+
current_item
|
182
|
+
end
|
183
|
+
|
184
|
+
def generate_all_episode_items
|
185
|
+
episode_list = []
|
186
|
+
@episode_list.map do |episode|
|
187
|
+
next if episode['hide']
|
188
|
+
|
189
|
+
episode_list << generate_episode_item(episode)
|
190
|
+
rescue StandardError => e
|
191
|
+
puts "Failed to generate item for #{episode[@ep_name]}: #{e}"
|
192
|
+
end
|
193
|
+
episode_list
|
194
|
+
end
|
195
|
+
|
196
|
+
def format_description(description, details: '', indent_level: 0, strip_all: false)
|
197
|
+
indentation = "\t" * indent_level
|
198
|
+
description = "#{description}\n#{indentation}#{details}".gsub '</br>', "\n#{indentation}"
|
199
|
+
description.gsub! '<p>', ''
|
200
|
+
description.gsub! '</p>', "\n#{indentation}"
|
201
|
+
description.gsub! '<ul>', ''
|
202
|
+
description.gsub! '<li>', "\n#{indentation} + "
|
203
|
+
description.gsub! '</li>', ''
|
204
|
+
description.gsub! '</ul>', "\n#{indentation}"
|
205
|
+
|
206
|
+
if strip_all
|
207
|
+
description.gsub!(/<.?[^>]+>/, '')
|
208
|
+
else
|
209
|
+
# strip rest of html tags (a tags are allowed)
|
210
|
+
description.gsub!(%r{<[^a][^a]/?[^>]+>}, '')
|
211
|
+
end
|
212
|
+
description.gsub!(" \n", "\n")
|
213
|
+
description.strip
|
214
|
+
end
|
215
|
+
|
216
|
+
def format_subtitle(subtitle)
|
217
|
+
desc = format_description(subtitle, strip_all: true)
|
218
|
+
desc.length > 255 ? "#{desc.slice(0, 252)}..." : desc
|
219
|
+
end
|
220
|
+
|
221
|
+
def episode_bytes_length(episode)
|
222
|
+
return episode['bytes_length'] if episode['bytes_length']
|
223
|
+
|
224
|
+
File.new("#{Dir.getwd}#{episode[@ep_url]}").size
|
225
|
+
end
|
226
|
+
|
227
|
+
def episode_duration(episode)
|
228
|
+
return episode['duration'] if episode['duration']
|
229
|
+
|
230
|
+
raise 'FFMPEG not found. ffmpeg is needed to fech episode length' unless which('ffmpeg')
|
231
|
+
|
232
|
+
cmd = "ffmpeg -i #{Dir.getwd}#{episode[@ep_url]} 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/\.[0-9]*,//"
|
233
|
+
`#{cmd}`.strip!
|
234
|
+
end
|
235
|
+
|
236
|
+
def episode_image(episode)
|
237
|
+
episode[@ep_img] || get_setting('logo')
|
238
|
+
end
|
239
|
+
|
240
|
+
def which(cmd)
|
241
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
242
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
243
|
+
exts.each do |ext|
|
244
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
245
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
nil
|
249
|
+
end
|
250
|
+
|
251
|
+
def load_episode_list
|
252
|
+
episodes
|
253
|
+
episodes_from_seasons
|
254
|
+
@episode_list.sort_by! do |episode|
|
255
|
+
DateTime.strptime(
|
256
|
+
episode[@ep_pub_date],
|
257
|
+
get_setting('datetime_format_string')
|
258
|
+
)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def episodes
|
263
|
+
episode_data = get_setting 'episode_data'
|
264
|
+
puts 'No episodes found' && return if episode_data.nil?
|
265
|
+
|
266
|
+
episode_data.each do |file|
|
267
|
+
puts "No data found for #{file}" && next unless @data[file]
|
268
|
+
|
269
|
+
@episode_list += @data[file]
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def episodes_from_seasons
|
274
|
+
season_data = get_setting 'season_data'
|
275
|
+
puts 'No seasons found' && return if season_data.nil?
|
276
|
+
|
277
|
+
season_data.each do |file|
|
278
|
+
puts "No data found for #{file}" && next unless @data[file]
|
279
|
+
|
280
|
+
@data[file].each do |season|
|
281
|
+
@episode_list += season[@season_episode_list]
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Fede
|
2
|
+
class XMLFeed
|
3
|
+
attr_accessor :nodes
|
4
|
+
|
5
|
+
def initialize(nodes = [])
|
6
|
+
@nodes = nodes
|
7
|
+
end
|
8
|
+
|
9
|
+
def header
|
10
|
+
"<?xml version='1.0' encoding='UTF-8'?>\n<rss version='2.0' xmlns:atom='http://www.w3.org/2005/Atom'"\
|
11
|
+
" xmlns:cc='http://web.resource.org/cc/' xmlns:itunes='http://www.itunes.com/dtds/podcast-1.0.dtd'"\
|
12
|
+
" xmlns:media='http://search.yahoo.com/mrss/' xmlns:content='http://purl.org/rss/1.0/modules/content/' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n"
|
13
|
+
end
|
14
|
+
|
15
|
+
def footer
|
16
|
+
"</rss>\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
'' unless @nodes
|
21
|
+
|
22
|
+
nodes_string = @nodes.reduce('') do |prev, node|
|
23
|
+
"#{prev}#{node.to_s(1)}"
|
24
|
+
end
|
25
|
+
|
26
|
+
"#{header}#{nodes_string}#{footer}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
class Fede
|
2
|
+
class XMLNode
|
3
|
+
attr_reader :children, :propperties
|
4
|
+
|
5
|
+
def initialize(tag_name, content: nil, cdata: false, propperties: {})
|
6
|
+
@tag_name = tag_name
|
7
|
+
@content = content
|
8
|
+
@cdata = cdata
|
9
|
+
@propperties = propperties
|
10
|
+
@children = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def parent?
|
14
|
+
!@children.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_propperty(name, value)
|
18
|
+
@propperties[name] = value
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_child(child)
|
22
|
+
@children << child
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_children(children)
|
26
|
+
raise TypeError 'Children must be an Array of nodes' unless children.is_a? Array
|
27
|
+
|
28
|
+
@children += children
|
29
|
+
end
|
30
|
+
|
31
|
+
def open_tag(indent_level)
|
32
|
+
if @cdata
|
33
|
+
"<#{@tag_name}>\n#{"\t" * indent_level}<![CDATA["
|
34
|
+
elsif @propperties.empty?
|
35
|
+
"<#{@tag_name}>"
|
36
|
+
else
|
37
|
+
prop_string = @propperties.reduce('') { |prev, values| "#{prev} #{values[0]}='#{values[1]}'" }
|
38
|
+
"<#{@tag_name} #{prop_string.strip}#{@content ? '' : ' /'}>"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def prop_string
|
43
|
+
return '' if @propperties.empty?
|
44
|
+
|
45
|
+
@propperties.reduce('') { |prev, values| "#{prev} #{values[0]}='#{values[1]}'" }
|
46
|
+
end
|
47
|
+
|
48
|
+
def children_string(indent_level)
|
49
|
+
@children.reduce('') { |prev, value| "#{prev}#{value.to_s indent_level}" }
|
50
|
+
end
|
51
|
+
|
52
|
+
def tag_open
|
53
|
+
"<#{@tag_name}#{prop_string}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def tag_middle(indent_level)
|
57
|
+
return '' unless parent? || @content
|
58
|
+
|
59
|
+
if @cdata
|
60
|
+
">\n#{"\t" * (indent_level + 1)}<![CDATA[#{@content}]]>\n#{"\t" * indent_level}"
|
61
|
+
elsif parent?
|
62
|
+
">\n#{children_string(indent_level + 1)}#{"\t" * indent_level}"
|
63
|
+
else
|
64
|
+
">#{@content}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def tag_close
|
69
|
+
parent? || @content ? "</#{@tag_name}>\n" : " />\n"
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s(indent_level = 0)
|
73
|
+
"#{"\t" * indent_level}#{tag_open}#{tag_middle(indent_level)}#{tag_close}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/fede.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
class Fede
|
2
|
+
def self.run(config_file:, data_dir:, mode:)
|
3
|
+
@generator = Fede::FeedGenerator.new config_file, data_dir
|
4
|
+
if mode.include? 'append'
|
5
|
+
append mode
|
6
|
+
elsif mode == 'generate'
|
7
|
+
generate
|
8
|
+
elsif mode == 'append-available'
|
9
|
+
append_available
|
10
|
+
else
|
11
|
+
puts "Invalid mode #{mode}. Valid modes are 'generate' or 'append'"
|
12
|
+
end
|
13
|
+
rescue StandardError => e
|
14
|
+
puts e
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.append_available
|
18
|
+
@generator.append_available_files
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.generate
|
22
|
+
@generator.generate
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.append(mode)
|
26
|
+
mode_info = mode.split('-')
|
27
|
+
@generator.append(mode_info.length > 1 ? Integer(mode_info[1]) : 1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'fede/generator'
|
32
|
+
require 'fede/xml_feed'
|
33
|
+
require 'fede/xml_node'
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fede
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lucca Augusto
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-04-18 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Very Simple XML feed generator from yaml data files
|
14
|
+
email: lucca@luccaaugusto.xyz
|
15
|
+
executables:
|
16
|
+
- fede
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/fede
|
21
|
+
- lib/fede.rb
|
22
|
+
- lib/fede/generator.rb
|
23
|
+
- lib/fede/xml_feed.rb
|
24
|
+
- lib/fede/xml_node.rb
|
25
|
+
homepage: https://github.com/luccaugusto/fede
|
26
|
+
licenses:
|
27
|
+
- MIT
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '3.0'
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubygems_version: 3.3.26
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: XML from yaml
|
48
|
+
test_files: []
|