comicinfo 1.0.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/.ruby-version +1 -0
- data/CHANGELOG.md +41 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.md +21 -0
- data/README.md +295 -0
- data/Rakefile +10 -0
- data/doc/development.md +255 -0
- data/lib/comicinfo/enums.rb +169 -0
- data/lib/comicinfo/errors.rb +64 -0
- data/lib/comicinfo/issue.rb +332 -0
- data/lib/comicinfo/page.rb +182 -0
- data/lib/comicinfo/version.rb +3 -0
- data/lib/comicinfo.rb +15 -0
- data/schemas/1.0/ComicInfo.xsd +77 -0
- data/schemas/2.0/ComicInfo.xsd +123 -0
- data/schemas/2.1-draft/ComicInfo.xsd +127 -0
- data/script/schema +95 -0
- data/sig/comicinfo.rbs +4 -0
- metadata +79 -0
@@ -0,0 +1,332 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'date'
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
require_relative 'enums'
|
6
|
+
require_relative 'errors'
|
7
|
+
require_relative 'page'
|
8
|
+
|
9
|
+
module ComicInfo
|
10
|
+
# Main class for parsing and accessing ComicInfo.xml data
|
11
|
+
# Follows the ComicInfo XSD schema v2.0 specification
|
12
|
+
class Issue
|
13
|
+
# String fields from ComicInfo schema
|
14
|
+
attr_reader :title, :series, :number, :alternate_series, :alternate_number,
|
15
|
+
:summary, :notes, :writer, :penciller, :inker, :colorist,
|
16
|
+
:letterer, :cover_artist, :editor, :translator, :publisher,
|
17
|
+
:imprint, :genre, :web, :language_iso, :format, :character,
|
18
|
+
:team, :location, :scan_information, :story_arc, :story_arc_number,
|
19
|
+
:series_group, :main_character_or_team, :review
|
20
|
+
|
21
|
+
# Integer fields from ComicInfo schema
|
22
|
+
attr_reader :count, :volume, :alternate_count, :year, :month, :day, :page_count
|
23
|
+
|
24
|
+
# Enum fields from ComicInfo schema
|
25
|
+
attr_reader :black_and_white, :manga, :age_rating
|
26
|
+
|
27
|
+
# Decimal fields from ComicInfo schema
|
28
|
+
attr_reader :community_rating
|
29
|
+
|
30
|
+
# Array fields from ComicInfo schema
|
31
|
+
attr_reader :pages
|
32
|
+
|
33
|
+
# Class method to load ComicInfo from file path or XML string
|
34
|
+
def self.load file_path_or_xml_string
|
35
|
+
raise Errors::ParseError, 'Input cannot be nil' if file_path_or_xml_string.nil?
|
36
|
+
|
37
|
+
input = file_path_or_xml_string.to_s
|
38
|
+
return new(input) if input.empty?
|
39
|
+
|
40
|
+
if looks_like_xml?(input)
|
41
|
+
new(input)
|
42
|
+
else
|
43
|
+
load_from_file(input)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private_class_method def self.looks_like_xml? input
|
48
|
+
input.strip.start_with?('<')
|
49
|
+
end
|
50
|
+
|
51
|
+
private_class_method def self.load_from_file input
|
52
|
+
validate_file_path(input)
|
53
|
+
raise Errors::FileError, "File does not exist: '#{input}'" unless File.exist?(input)
|
54
|
+
|
55
|
+
begin
|
56
|
+
xml_content = File.read(input)
|
57
|
+
new(xml_content)
|
58
|
+
rescue Errors::ParseError
|
59
|
+
# Re-raise parse errors from XML parsing
|
60
|
+
raise
|
61
|
+
rescue StandardError => e
|
62
|
+
raise Errors::FileError, "Failed to read file '#{input}': #{e.message}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private_class_method def self.validate_file_path input
|
67
|
+
return unless input.match?(/^\d+$/) ||
|
68
|
+
(!input.include?('.') && !input.include?('/') && !input.include?('\\'))
|
69
|
+
|
70
|
+
raise Errors::ParseError, "Input '#{input}' does not appear to be valid XML or a file path"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Initialize from XML string
|
74
|
+
def initialize xml_string
|
75
|
+
raise Errors::ParseError, 'XML string cannot be nil or empty' if xml_string.nil? || xml_string.empty?
|
76
|
+
|
77
|
+
begin
|
78
|
+
@doc = Nokogiri::XML(xml_string) do |config|
|
79
|
+
config.strict.nonet
|
80
|
+
end
|
81
|
+
|
82
|
+
raise Errors::ParseError, "XML parsing failed: #{@doc.errors.first.message}" if @doc.errors.any?
|
83
|
+
|
84
|
+
@root = @doc.at_css('ComicInfo')
|
85
|
+
raise Errors::ParseError, 'No ComicInfo root element found' if @root.nil?
|
86
|
+
rescue Nokogiri::XML::SyntaxError => e
|
87
|
+
raise Errors::ParseError, "Invalid XML syntax: #{e.message}"
|
88
|
+
end
|
89
|
+
|
90
|
+
parse_fields
|
91
|
+
end
|
92
|
+
|
93
|
+
# Convenience methods for boolean checks
|
94
|
+
def manga?
|
95
|
+
Enums::Helpers.yes_value?(@manga)
|
96
|
+
end
|
97
|
+
|
98
|
+
def right_to_left?
|
99
|
+
Enums::Helpers.manga_right_to_left?(@manga)
|
100
|
+
end
|
101
|
+
|
102
|
+
def black_and_white?
|
103
|
+
Enums::Helpers.yes_value?(@black_and_white)
|
104
|
+
end
|
105
|
+
|
106
|
+
def pages?
|
107
|
+
@pages && !@pages.empty?
|
108
|
+
end
|
109
|
+
|
110
|
+
# Get only cover pages
|
111
|
+
def cover_pages
|
112
|
+
return [] unless pages?
|
113
|
+
|
114
|
+
@pages.select(&:cover?)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Get only story pages
|
118
|
+
def story_pages
|
119
|
+
return [] unless pages?
|
120
|
+
|
121
|
+
@pages.select(&:story?)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Get publication date as Date object if available
|
125
|
+
def publication_date
|
126
|
+
return nil if @year == Enums::DEFAULT_INTEGER
|
127
|
+
|
128
|
+
year = @year
|
129
|
+
month = @month == Enums::DEFAULT_INTEGER ? 1 : @month
|
130
|
+
day = @day == Enums::DEFAULT_INTEGER ? 1 : @day
|
131
|
+
|
132
|
+
begin
|
133
|
+
Date.new(year, month, day)
|
134
|
+
rescue ArgumentError
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Plural methods that return arrays
|
140
|
+
def genres
|
141
|
+
split_comma_separated(@genre)
|
142
|
+
end
|
143
|
+
|
144
|
+
def characters
|
145
|
+
split_comma_separated(@character)
|
146
|
+
end
|
147
|
+
|
148
|
+
def teams
|
149
|
+
split_comma_separated(@team)
|
150
|
+
end
|
151
|
+
|
152
|
+
def locations
|
153
|
+
split_comma_separated(@location)
|
154
|
+
end
|
155
|
+
|
156
|
+
def story_arcs
|
157
|
+
split_comma_separated(@story_arc)
|
158
|
+
end
|
159
|
+
|
160
|
+
def story_arc_numbers
|
161
|
+
split_comma_separated(@story_arc_number)
|
162
|
+
end
|
163
|
+
|
164
|
+
def web_urls
|
165
|
+
return [] if @web.empty?
|
166
|
+
|
167
|
+
@web.split(/\s+/)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Convert to JSON representation
|
171
|
+
def to_json(*)
|
172
|
+
to_h.to_json(*)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Convert to YAML representation
|
176
|
+
def to_yaml(*)
|
177
|
+
to_h.to_yaml(*)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Convert to hash representation for JSON serialization
|
181
|
+
def to_h
|
182
|
+
{
|
183
|
+
title: @title,
|
184
|
+
series: @series,
|
185
|
+
number: @number,
|
186
|
+
count: @count,
|
187
|
+
volume: @volume,
|
188
|
+
alternate_series: @alternate_series,
|
189
|
+
alternate_number: @alternate_number,
|
190
|
+
alternate_count: @alternate_count,
|
191
|
+
summary: @summary,
|
192
|
+
notes: @notes,
|
193
|
+
year: @year,
|
194
|
+
month: @month,
|
195
|
+
day: @day,
|
196
|
+
writer: @writer,
|
197
|
+
penciller: @penciller,
|
198
|
+
inker: @inker,
|
199
|
+
colorist: @colorist,
|
200
|
+
letterer: @letterer,
|
201
|
+
cover_artist: @cover_artist,
|
202
|
+
editor: @editor,
|
203
|
+
translator: @translator,
|
204
|
+
publisher: @publisher,
|
205
|
+
imprint: @imprint,
|
206
|
+
genre: @genre,
|
207
|
+
genres: genres,
|
208
|
+
web: @web,
|
209
|
+
web_urls: web_urls,
|
210
|
+
page_count: @page_count,
|
211
|
+
language_iso: @language_iso,
|
212
|
+
format: @format,
|
213
|
+
black_and_white: @black_and_white,
|
214
|
+
manga: @manga,
|
215
|
+
character: @character,
|
216
|
+
characters: characters,
|
217
|
+
team: @team,
|
218
|
+
teams: teams,
|
219
|
+
location: @location,
|
220
|
+
locations: locations,
|
221
|
+
scan_information: @scan_information,
|
222
|
+
story_arc: @story_arc,
|
223
|
+
story_arcs: story_arcs,
|
224
|
+
story_arc_number: @story_arc_number,
|
225
|
+
story_arc_numbers: story_arc_numbers,
|
226
|
+
series_group: @series_group,
|
227
|
+
age_rating: @age_rating,
|
228
|
+
main_character_or_team: @main_character_or_team,
|
229
|
+
community_rating: @community_rating,
|
230
|
+
review: @review,
|
231
|
+
pages: @pages.map(&:to_h)
|
232
|
+
}.compact
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def parse_fields
|
238
|
+
# String fields
|
239
|
+
@title = get_string_field('Title')
|
240
|
+
@series = get_string_field('Series')
|
241
|
+
@number = get_string_field('Number')
|
242
|
+
@alternate_series = get_string_field('AlternateSeries')
|
243
|
+
@alternate_number = get_string_field('AlternateNumber')
|
244
|
+
@summary = get_string_field('Summary')
|
245
|
+
@notes = get_string_field('Notes')
|
246
|
+
|
247
|
+
# Creator fields
|
248
|
+
@writer = get_string_field('Writer')
|
249
|
+
@penciller = get_string_field('Penciller')
|
250
|
+
@inker = get_string_field('Inker')
|
251
|
+
@colorist = get_string_field('Colorist')
|
252
|
+
@letterer = get_string_field('Letterer')
|
253
|
+
@cover_artist = get_string_field('CoverArtist')
|
254
|
+
@editor = get_string_field('Editor')
|
255
|
+
@translator = get_string_field('Translator')
|
256
|
+
|
257
|
+
# Publication fields
|
258
|
+
@publisher = get_string_field('Publisher')
|
259
|
+
@imprint = get_string_field('Imprint')
|
260
|
+
@genre = get_string_field('Genre')
|
261
|
+
@web = get_string_field('Web')
|
262
|
+
@language_iso = get_string_field('LanguageISO')
|
263
|
+
@format = get_string_field('Format')
|
264
|
+
|
265
|
+
# Multi-value string fields (singular names for string values)
|
266
|
+
@character = get_string_field('Characters')
|
267
|
+
@team = get_string_field('Teams')
|
268
|
+
@location = get_string_field('Locations')
|
269
|
+
@scan_information = get_string_field('ScanInformation')
|
270
|
+
@story_arc = get_string_field('StoryArc')
|
271
|
+
@story_arc_number = get_string_field('StoryArcNumber')
|
272
|
+
@series_group = get_string_field('SeriesGroup')
|
273
|
+
@main_character_or_team = get_string_field('MainCharacterOrTeam')
|
274
|
+
@review = get_string_field('Review')
|
275
|
+
|
276
|
+
# Integer fields
|
277
|
+
@count = get_integer_field('Count')
|
278
|
+
@volume = get_integer_field('Volume')
|
279
|
+
@alternate_count = get_integer_field('AlternateCount')
|
280
|
+
@year = Enums::Validators.validate_year(get_field_text('Year'))
|
281
|
+
@month = Enums::Validators.validate_month(get_field_text('Month'))
|
282
|
+
@day = Enums::Validators.validate_day(get_field_text('Day'))
|
283
|
+
@page_count = get_integer_field('PageCount', Enums::DEFAULT_PAGE_COUNT)
|
284
|
+
|
285
|
+
# Enum fields
|
286
|
+
@black_and_white = Enums::Validators.validate_yes_no(get_field_text('BlackAndWhite'))
|
287
|
+
@manga = Enums::Validators.validate_manga(get_field_text('Manga'))
|
288
|
+
@age_rating = Enums::Validators.validate_age_rating(get_field_text('AgeRating'))
|
289
|
+
|
290
|
+
# Decimal fields
|
291
|
+
@community_rating = Enums::Validators.validate_community_rating(get_field_text('CommunityRating'))
|
292
|
+
|
293
|
+
# Array fields
|
294
|
+
@pages = parse_pages
|
295
|
+
end
|
296
|
+
|
297
|
+
def get_string_field field_name
|
298
|
+
text = get_field_text(field_name)
|
299
|
+
text.nil? || text.empty? ? Enums::DEFAULT_STRING : text
|
300
|
+
end
|
301
|
+
|
302
|
+
def get_integer_field field_name, default = Enums::DEFAULT_INTEGER
|
303
|
+
text = get_field_text(field_name)
|
304
|
+
Enums::Validators.validate_integer(text, field_name, default)
|
305
|
+
end
|
306
|
+
|
307
|
+
def get_field_text field_name
|
308
|
+
element = @root.at_css(field_name)
|
309
|
+
element&.text
|
310
|
+
end
|
311
|
+
|
312
|
+
def parse_pages
|
313
|
+
pages_element = @root.at_css('Pages')
|
314
|
+
return [] unless pages_element
|
315
|
+
|
316
|
+
page_elements = pages_element.css('Page')
|
317
|
+
page_elements.map do |page_element|
|
318
|
+
attributes = {}
|
319
|
+
page_element.attributes.each do |name, attr|
|
320
|
+
attributes[name] = attr.value
|
321
|
+
end
|
322
|
+
Page.new(attributes)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def split_comma_separated text
|
327
|
+
return [] if text.nil? || text.empty?
|
328
|
+
|
329
|
+
text.split(/,\s*/).map(&:strip).reject(&:empty?)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require_relative 'enums'
|
2
|
+
require_relative 'errors'
|
3
|
+
|
4
|
+
module ComicInfo
|
5
|
+
# Represents a single page in a comic book with metadata
|
6
|
+
# Maps to ComicPageInfo type in the ComicInfo XSD schema
|
7
|
+
class Page
|
8
|
+
# Page attributes from XSD schema
|
9
|
+
attr_reader :image, :type, :double_page, :image_size, :key, :bookmark, :image_width, :image_height
|
10
|
+
|
11
|
+
def initialize attributes = {}
|
12
|
+
@image = parse_image(attributes['Image'] || attributes[:image])
|
13
|
+
@type = Enums::Validators.validate_comic_page_type(attributes['Type'] || attributes[:type])
|
14
|
+
@double_page = parse_boolean(attributes['DoublePage'] || attributes[:double_page])
|
15
|
+
@image_size = parse_image_size(attributes['ImageSize'] || attributes[:image_size])
|
16
|
+
@key = (attributes['Key'] || attributes[:key] || Enums::DEFAULT_STRING).to_s
|
17
|
+
@bookmark = (attributes['Bookmark'] || attributes[:bookmark] || Enums::DEFAULT_STRING).to_s
|
18
|
+
@image_width = parse_image_dimension(attributes['ImageWidth'] || attributes[:image_width], 'ImageWidth')
|
19
|
+
@image_height = parse_image_dimension(attributes['ImageHeight'] || attributes[:image_height], 'ImageHeight')
|
20
|
+
end
|
21
|
+
|
22
|
+
# Check if this page is a cover page
|
23
|
+
def cover?
|
24
|
+
@type == 'FrontCover' || @type == 'BackCover' || @type == 'InnerCover'
|
25
|
+
end
|
26
|
+
|
27
|
+
# Check if this page is a story page
|
28
|
+
def story?
|
29
|
+
@type == 'Story'
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check if this page should be deleted/hidden
|
33
|
+
def deleted?
|
34
|
+
@type == 'Deleted'
|
35
|
+
end
|
36
|
+
|
37
|
+
# Check if this is a double-page spread
|
38
|
+
def double_page?
|
39
|
+
@double_page
|
40
|
+
end
|
41
|
+
|
42
|
+
# Get page types as an array (handles space-separated values)
|
43
|
+
def types
|
44
|
+
@type.split(/\s+/)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Check if page has a specific type
|
48
|
+
def include_type? type
|
49
|
+
types.include?(type)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get image dimensions as a hash
|
53
|
+
def dimensions
|
54
|
+
{
|
55
|
+
width: @image_width == Enums::DEFAULT_INTEGER ? nil : @image_width,
|
56
|
+
height: @image_height == Enums::DEFAULT_INTEGER ? nil : @image_height
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Check if image dimensions are available
|
61
|
+
def dimensions_available?
|
62
|
+
@image_width != Enums::DEFAULT_INTEGER && @image_height != Enums::DEFAULT_INTEGER
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get aspect ratio if dimensions are available
|
66
|
+
def aspect_ratio
|
67
|
+
return nil unless dimensions_available? && @image_height != 0
|
68
|
+
|
69
|
+
@image_width.to_f / @image_height
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check if this page has a bookmark
|
73
|
+
def bookmarked?
|
74
|
+
!@bookmark.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Convert to hash representation
|
78
|
+
def to_h
|
79
|
+
{
|
80
|
+
image: @image,
|
81
|
+
type: @type,
|
82
|
+
double_page: @double_page,
|
83
|
+
image_size: @image_size,
|
84
|
+
key: @key,
|
85
|
+
bookmark: @bookmark,
|
86
|
+
image_width: @image_width,
|
87
|
+
image_height: @image_height
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
# Convert to XML attributes hash (for XML generation)
|
92
|
+
def to_xml_attributes
|
93
|
+
attrs = { 'Image' => @image.to_s }
|
94
|
+
attrs['Type'] = @type unless @type == Enums::DEFAULT_PAGE_TYPE
|
95
|
+
attrs['DoublePage'] = @double_page.to_s unless @double_page == Enums::DEFAULT_DOUBLE_PAGE
|
96
|
+
attrs['ImageSize'] = @image_size.to_s unless @image_size == Enums::DEFAULT_IMAGE_SIZE
|
97
|
+
attrs['Key'] = @key unless @key.empty?
|
98
|
+
attrs['Bookmark'] = @bookmark unless @bookmark.empty?
|
99
|
+
attrs['ImageWidth'] = @image_width.to_s unless @image_width == Enums::DEFAULT_INTEGER
|
100
|
+
attrs['ImageHeight'] = @image_height.to_s unless @image_height == Enums::DEFAULT_INTEGER
|
101
|
+
attrs
|
102
|
+
end
|
103
|
+
|
104
|
+
# String representation
|
105
|
+
def to_s
|
106
|
+
parts = ["Page #{@image}"]
|
107
|
+
parts << "Type: #{@type}" unless @type == Enums::DEFAULT_PAGE_TYPE
|
108
|
+
parts << 'Double' if @double_page
|
109
|
+
parts << "Bookmark: #{@bookmark}" unless @bookmark.empty?
|
110
|
+
parts.join(', ')
|
111
|
+
end
|
112
|
+
|
113
|
+
# Detailed inspection
|
114
|
+
def inspect
|
115
|
+
"#<ComicInfo::Page #{self}>"
|
116
|
+
end
|
117
|
+
|
118
|
+
# Equality comparison
|
119
|
+
def == other
|
120
|
+
return false unless other.is_a?(Page)
|
121
|
+
|
122
|
+
@image == other.image &&
|
123
|
+
@type == other.type &&
|
124
|
+
@double_page == other.double_page &&
|
125
|
+
@image_size == other.image_size &&
|
126
|
+
@key == other.key &&
|
127
|
+
@bookmark == other.bookmark &&
|
128
|
+
@image_width == other.image_width &&
|
129
|
+
@image_height == other.image_height
|
130
|
+
end
|
131
|
+
|
132
|
+
alias eql? ==
|
133
|
+
|
134
|
+
def hash
|
135
|
+
[@image, @type, @double_page, @image_size, @key, @bookmark, @image_width, @image_height].hash
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def parse_image value
|
141
|
+
raise Errors::SchemaError, 'Image attribute is required for Page' if value.nil? || value.to_s.empty?
|
142
|
+
|
143
|
+
begin
|
144
|
+
Integer(value)
|
145
|
+
rescue ArgumentError
|
146
|
+
raise Errors::TypeCoercionError.new('Image', value, 'Integer')
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def parse_boolean value
|
151
|
+
case value.to_s.downcase
|
152
|
+
when 'true', '1', 'yes'
|
153
|
+
true
|
154
|
+
when 'false', '0', 'no', ''
|
155
|
+
false
|
156
|
+
else
|
157
|
+
raise Errors::TypeCoercionError.new('DoublePage', value, 'Boolean')
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def parse_image_size value
|
162
|
+
return Enums::DEFAULT_IMAGE_SIZE if value.nil? || value.to_s.empty?
|
163
|
+
|
164
|
+
begin
|
165
|
+
size = Integer(value)
|
166
|
+
size.negative? ? Enums::DEFAULT_IMAGE_SIZE : size
|
167
|
+
rescue ArgumentError
|
168
|
+
raise Errors::TypeCoercionError.new('ImageSize', value, 'Integer')
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def parse_image_dimension value, field_name
|
173
|
+
return Enums::DEFAULT_INTEGER if value.nil? || value.to_s.empty?
|
174
|
+
|
175
|
+
begin
|
176
|
+
Integer(value)
|
177
|
+
rescue ArgumentError
|
178
|
+
raise Errors::TypeCoercionError.new(field_name, value, 'Integer')
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
data/lib/comicinfo.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'comicinfo/version'
|
2
|
+
|
3
|
+
module ComicInfo
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
autoload :Issue, 'comicinfo/issue'
|
7
|
+
autoload :Page, 'comicinfo/page'
|
8
|
+
autoload :Enums, 'comicinfo/enums'
|
9
|
+
autoload :Errors, 'comicinfo/errors'
|
10
|
+
|
11
|
+
# Convenience method for loading ComicInfo files
|
12
|
+
def self.load file_path_or_xml_string
|
13
|
+
Issue.load(file_path_or_xml_string)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
2
|
+
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
3
|
+
<xs:element name="ComicInfo" nillable="true" type="ComicInfo"/>
|
4
|
+
<xs:complexType name="ComicInfo">
|
5
|
+
<xs:sequence>
|
6
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Title" type="xs:string"/>
|
7
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Series" type="xs:string"/>
|
8
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Number" type="xs:string"/>
|
9
|
+
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Count" type="xs:int"/>
|
10
|
+
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Volume" type="xs:int"/>
|
11
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateSeries" type="xs:string"/>
|
12
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateNumber" type="xs:string"/>
|
13
|
+
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="AlternateCount" type="xs:int"/>
|
14
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Summary" type="xs:string"/>
|
15
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Notes" type="xs:string"/>
|
16
|
+
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Year" type="xs:int"/>
|
17
|
+
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Month" type="xs:int"/>
|
18
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Writer" type="xs:string"/>
|
19
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Penciller" type="xs:string"/>
|
20
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Inker" type="xs:string"/>
|
21
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Colorist" type="xs:string"/>
|
22
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Letterer" type="xs:string"/>
|
23
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="CoverArtist" type="xs:string"/>
|
24
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Editor" type="xs:string"/>
|
25
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Publisher" type="xs:string"/>
|
26
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Imprint" type="xs:string"/>
|
27
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Genre" type="xs:string"/>
|
28
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Web" type="xs:string"/>
|
29
|
+
<xs:element minOccurs="0" maxOccurs="1" default="0" name="PageCount" type="xs:int"/>
|
30
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="LanguageISO" type="xs:string"/>
|
31
|
+
<xs:element minOccurs="0" maxOccurs="1" default="" name="Format" type="xs:string"/>
|
32
|
+
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="BlackAndWhite" type="YesNo"/>
|
33
|
+
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="Manga" type="YesNo"/>
|
34
|
+
<xs:element minOccurs="0" maxOccurs="1" name="Pages" type="ArrayOfComicPageInfo"/>
|
35
|
+
</xs:sequence>
|
36
|
+
</xs:complexType>
|
37
|
+
<xs:simpleType name="YesNo">
|
38
|
+
<xs:restriction base="xs:string">
|
39
|
+
<xs:enumeration value="Unknown"/>
|
40
|
+
<xs:enumeration value="No"/>
|
41
|
+
<xs:enumeration value="Yes"/>
|
42
|
+
</xs:restriction>
|
43
|
+
</xs:simpleType>
|
44
|
+
<xs:complexType name="ArrayOfComicPageInfo">
|
45
|
+
<xs:sequence>
|
46
|
+
<xs:element minOccurs="0" maxOccurs="unbounded" name="Page" nillable="true" type="ComicPageInfo"/>
|
47
|
+
</xs:sequence>
|
48
|
+
</xs:complexType>
|
49
|
+
<xs:complexType name="ComicPageInfo">
|
50
|
+
<xs:attribute name="Image" type="xs:int" use="required"/>
|
51
|
+
<xs:attribute default="Story" name="Type" type="ComicPageType"/>
|
52
|
+
<xs:attribute default="false" name="DoublePage" type="xs:boolean"/>
|
53
|
+
<xs:attribute default="0" name="ImageSize" type="xs:long"/>
|
54
|
+
<xs:attribute default="" name="Key" type="xs:string"/>
|
55
|
+
<xs:attribute default="-1" name="ImageWidth" type="xs:int"/>
|
56
|
+
<xs:attribute default="-1" name="ImageHeight" type="xs:int"/>
|
57
|
+
</xs:complexType>
|
58
|
+
<xs:simpleType name="ComicPageType">
|
59
|
+
<xs:list>
|
60
|
+
<xs:simpleType>
|
61
|
+
<xs:restriction base="xs:string">
|
62
|
+
<xs:enumeration value="FrontCover"/>
|
63
|
+
<xs:enumeration value="InnerCover"/>
|
64
|
+
<xs:enumeration value="Roundup"/>
|
65
|
+
<xs:enumeration value="Story"/>
|
66
|
+
<xs:enumeration value="Advertisement"/>
|
67
|
+
<xs:enumeration value="Editorial"/>
|
68
|
+
<xs:enumeration value="Letters"/>
|
69
|
+
<xs:enumeration value="Preview"/>
|
70
|
+
<xs:enumeration value="BackCover"/>
|
71
|
+
<xs:enumeration value="Other"/>
|
72
|
+
<xs:enumeration value="Deleted"/>
|
73
|
+
</xs:restriction>
|
74
|
+
</xs:simpleType>
|
75
|
+
</xs:list>
|
76
|
+
</xs:simpleType>
|
77
|
+
</xs:schema>
|