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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module ComicInfo
2
+ VERSION = '1.0.0'.freeze
3
+ 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>