docbook_files 0.4.0 → 0.5.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.
- data/Gemfile +12 -4
- data/Gemfile.lock +9 -4
- data/History.txt +10 -0
- data/LICENSE +22 -0
- data/README.md +30 -18
- data/Rakefile +15 -1
- data/lib/docbook_files/app.rb +112 -79
- data/lib/docbook_files/docbook.rb +2 -2
- data/lib/docbook_files/file_data.rb +125 -83
- data/lib/docbook_files/file_ref.rb +98 -0
- data/lib/docbook_files/file_ref_types.rb +20 -0
- data/spec/docbook_files/app_spec.rb +69 -15
- data/spec/docbook_files/docbook_spec.rb +1 -1
- data/spec/docbook_files/file_data_spec.rb +59 -49
- data/spec/docbook_files/file_ref_spec.rb +97 -0
- data/version.txt +1 -1
- metadata +49 -16
@@ -108,7 +108,7 @@ private
|
|
108
108
|
refs = doc.find('//db:*[@fileref!=""]',:db => DOCBOOK_NS)
|
109
109
|
refs.map {|r|
|
110
110
|
fname = r.attributes['fileref']
|
111
|
-
|
111
|
+
FileRef.new(fname,parent_dir,parent_fd)
|
112
112
|
}
|
113
113
|
end
|
114
114
|
|
@@ -116,7 +116,7 @@ private
|
|
116
116
|
# Returns a FileData object with its include-tree
|
117
117
|
#
|
118
118
|
def analyze_file(fname, parent_dir, parent_fd=nil)
|
119
|
-
fl =
|
119
|
+
fl = FileRef.new(fname, parent_dir, parent_fd)
|
120
120
|
if fl.exists?
|
121
121
|
begin
|
122
122
|
doc = XML::Document.file(fl.full_name)
|
@@ -4,61 +4,78 @@ module DocbookFiles
|
|
4
4
|
require 'digest/sha1'
|
5
5
|
require 'wand'
|
6
6
|
|
7
|
-
#
|
7
|
+
# Represents the actual file that is included or referenced in a DocBook project.
|
8
|
+
# For every file there should only one FileData instance, so we use a factory method
|
9
|
+
# #FileData.for to create new instances.
|
10
|
+
#
|
8
11
|
class FileData
|
9
12
|
|
10
|
-
# Type for the main/master file
|
11
|
-
TYPE_MAIN = :main
|
12
|
-
# Type for referenced files
|
13
|
-
TYPE_REFERENCE = :ref
|
14
|
-
# Type for included files
|
15
|
-
TYPE_INCLUDE = :inc
|
16
|
-
|
17
13
|
# File exists and no error happened
|
18
14
|
STATUS_OK = 0
|
19
15
|
# File does not exist
|
20
16
|
STATUS_NOT_FOUND = 1
|
21
17
|
# Error while processing the file, see #error_string
|
22
18
|
STATUS_ERR = 2
|
19
|
+
|
20
|
+
# A storage for all FileData instances.
|
21
|
+
@@Files = {}
|
22
|
+
|
23
|
+
# The directory of the main file. All #path names are relative to that
|
24
|
+
@@MainDir = ""
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
+
# Return the FileData storage -- for testing only
|
27
|
+
def self.storage; @@Files; end
|
28
|
+
|
29
|
+
# Return all existing FileData instances
|
30
|
+
def self.files; @@Files.values; end
|
26
31
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
:checksum => "file checksum",
|
32
|
-
:mime => "the file's MIME type",
|
33
|
-
:namespace => "XML namespace, if applicable",
|
34
|
-
:docbook => "DocBook type flag",
|
35
|
-
:version => "DocBook version number, if applicable",
|
36
|
-
:tag => "XML start tag, if applicable",
|
37
|
-
:parent => "parent file, if included or referenced"}
|
38
|
-
x.each { |s,ex|
|
39
|
-
attr_accessor s
|
40
|
-
}
|
41
|
-
x
|
32
|
+
# Reset the FileData storage -- must be done before every run!
|
33
|
+
def self.reset
|
34
|
+
@@Files={}
|
35
|
+
@@MainDir = ""
|
42
36
|
end
|
43
|
-
|
44
|
-
@@vars = init_vars()
|
45
37
|
|
38
|
+
# Factory method for FileData instances. Checks if there is already
|
39
|
+
# an instance.
|
40
|
+
def self.for(name,parent_dir=".")
|
41
|
+
full_name = get_full_name(name, parent_dir)
|
42
|
+
# Initialize the main dir name for path construction
|
43
|
+
@@MainDir = File.dirname(full_name) if @@Files.size == 0
|
44
|
+
key = full_name
|
45
|
+
if (@@Files[key].nil?)
|
46
|
+
@@Files[key] = FileData.new(name, full_name, key, parent_dir)
|
47
|
+
end
|
48
|
+
@@Files[key]
|
49
|
+
end
|
50
|
+
|
46
51
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
52
|
+
attr_accessor :name, :path, :exists
|
53
|
+
attr_accessor :status, :error_string
|
54
|
+
attr_accessor :full_name
|
55
|
+
# Unique key (path+checksum)
|
56
|
+
attr_reader :key
|
57
|
+
# SHA1 checksum, file size in bytes, mime type, last modified timestamp
|
58
|
+
attr_reader :checksum, :size, :mime, :ts
|
59
|
+
# XML data: namespace, docbook flag, namespace version, start tag
|
60
|
+
attr_accessor :namespace, :docbook, :version, :tag
|
61
|
+
|
62
|
+
|
63
|
+
def initialize(name, full_name, key, parent_dir=".")
|
64
|
+
@path = relative2main(full_name)
|
65
|
+
@full_name = full_name
|
66
|
+
@name = File.basename(name)
|
67
|
+
@key = key
|
51
68
|
@namespace = ""
|
52
69
|
@docbook = false
|
53
70
|
@version = ""
|
54
71
|
@tag = ""
|
55
72
|
@error_string = nil
|
56
|
-
@
|
73
|
+
@rels = []
|
57
74
|
if (File.exists?(@full_name))
|
58
75
|
@status = STATUS_OK
|
59
76
|
@ts = File.mtime(full_name)
|
60
77
|
@size = File.size(full_name)
|
61
|
-
@checksum = calc_checksum()
|
78
|
+
@checksum = calc_checksum(full_name)
|
62
79
|
@mime = get_mime_type()
|
63
80
|
else
|
64
81
|
@status = STATUS_NOT_FOUND
|
@@ -67,9 +84,7 @@ module DocbookFiles
|
|
67
84
|
@checksum = ""
|
68
85
|
@mime = ""
|
69
86
|
@error_string = "file not found"
|
70
|
-
end
|
71
|
-
@includes = []
|
72
|
-
@refs = []
|
87
|
+
end
|
73
88
|
end
|
74
89
|
|
75
90
|
|
@@ -78,59 +93,86 @@ module DocbookFiles
|
|
78
93
|
@status != STATUS_NOT_FOUND
|
79
94
|
end
|
80
95
|
|
81
|
-
#
|
82
|
-
def
|
83
|
-
|
84
|
-
files.flatten.reject{|f| f[:status] != STATUS_NOT_FOUND}.map{|f| f.delete(:status); f}
|
96
|
+
# Add a one-way relationship, type and target, to self
|
97
|
+
def add_rel(type, target)
|
98
|
+
@rels << {:type => type, :target => target}
|
85
99
|
end
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
100
|
+
# Add a two-way relationship between self and a number of targets.
|
101
|
+
def add_rels(type, invtype, targets)
|
102
|
+
targets.each {|t|
|
103
|
+
self.add_rel(type, t)
|
104
|
+
t.add_rel(invtype, self)
|
105
|
+
}
|
90
106
|
end
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
#
|
107
|
+
# Get all targets for a relation
|
108
|
+
def get_rel_targets(type)
|
109
|
+
@rels.find_all{|rel| rel[:type] == type}.map{|r| r[:target]}
|
110
|
+
end
|
111
|
+
|
112
|
+
# Add included FileDatas. Establishes a two way relationship between self
|
113
|
+
# and the included files:
|
97
114
|
#
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
115
|
+
# self -> TYPE_INCLUDE -> target
|
116
|
+
# self <- TYPE_INCLUDED_BY <- target
|
117
|
+
#
|
118
|
+
# TODO: should be 'add_includes'
|
119
|
+
def add_includes(incs)
|
120
|
+
add_rels(FileRefTypes::TYPE_INCLUDE, FileRefTypes::TYPE_INCLUDED_BY, incs)
|
121
|
+
end
|
122
|
+
# Retrieves all included FileDatas
|
123
|
+
def includes
|
124
|
+
get_rel_targets(FileRefTypes::TYPE_INCLUDE)
|
125
|
+
end
|
126
|
+
# Retrieves all FileDatas that include self
|
127
|
+
def included_by
|
128
|
+
get_rel_targets(FileRefTypes::TYPE_INCLUDED_BY)
|
102
129
|
end
|
103
130
|
|
104
|
-
#
|
105
|
-
#
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
131
|
+
# Add referenced FileDatas. Establishes a two way relationship between self
|
132
|
+
# and the referenced files:
|
133
|
+
#
|
134
|
+
# self -> TYPE_REFERENCE -> target
|
135
|
+
# self <- TYPE_REFERENCED_BY <- target
|
136
|
+
#
|
137
|
+
def add_references(incs)
|
138
|
+
add_rels(FileRefTypes::TYPE_REFERENCE, FileRefTypes::TYPE_REFERENCED_BY, incs)
|
139
|
+
end
|
140
|
+
# Retrieves all referenced files
|
141
|
+
def references
|
142
|
+
get_rel_targets(FileRefTypes::TYPE_REFERENCE)
|
143
|
+
end
|
144
|
+
# Retrieves all FileDatas that reference self
|
145
|
+
def referenced_by
|
146
|
+
get_rel_targets(FileRefTypes::TYPE_REFERENCED_BY)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Try to find the path of _file_name_ that is relative to directory
|
150
|
+
# of the _main file_.
|
151
|
+
# If there is no common part return the _file_name_.
|
152
|
+
def relative2main(file_name)
|
153
|
+
md = file_name.match("^#{@@MainDir}/")
|
154
|
+
if md.nil?
|
155
|
+
file_name
|
114
156
|
else
|
115
|
-
|
157
|
+
md.post_match
|
116
158
|
end
|
117
159
|
end
|
118
160
|
|
119
|
-
# Return a
|
120
|
-
#
|
121
|
-
#
|
161
|
+
# Return a hash with the values for the passed symbols.
|
162
|
+
#
|
163
|
+
# Example: to_hash([:name, :mime]) would return
|
164
|
+
# {:name => "name", :mime => "application/xml"}.
|
122
165
|
#
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
me2.flatten
|
166
|
+
def to_hash(props)
|
167
|
+
me_hash = {}
|
168
|
+
props.each {|p|
|
169
|
+
if ([:includes, :included_by, :references, :referenced_by].member?(p))
|
170
|
+
me_hash[p] = self.send(p).map{|p2| p2.path}
|
171
|
+
else
|
172
|
+
me_hash[p] = self.send(p)
|
173
|
+
end
|
174
|
+
}
|
175
|
+
me_hash
|
134
176
|
end
|
135
177
|
|
136
178
|
private
|
@@ -140,17 +182,17 @@ private
|
|
140
182
|
#--
|
141
183
|
# Includes hack for Ruby 1.8
|
142
184
|
#++
|
143
|
-
def calc_checksum
|
185
|
+
def calc_checksum(full_name)
|
144
186
|
if RUBY_VERSION=~ /^1.8/
|
145
|
-
contents = open(
|
187
|
+
contents = open(full_name, "rb") {|io| io.read }
|
146
188
|
else
|
147
|
-
contents = IO.binread(
|
189
|
+
contents = IO.binread(full_name)
|
148
190
|
end
|
149
191
|
Digest::SHA1.hexdigest(contents)
|
150
192
|
end
|
151
193
|
|
152
194
|
# Produce the full path for a filename
|
153
|
-
def get_full_name(fname, parent_dir)
|
195
|
+
def self.get_full_name(fname, parent_dir)
|
154
196
|
dir = File.dirname(fname)
|
155
197
|
file = File.basename(fname)
|
156
198
|
full_name = File.expand_path(file,dir)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# -*- encoding:utf-8 -*-
|
2
|
+
module DocbookFiles
|
3
|
+
|
4
|
+
# A FileRef represents a file inclusion (<xi:include href='...') or
|
5
|
+
# reference (<imagedata filref=='...') in DocBook. It points to a FileData
|
6
|
+
# instance, that represents the actual file. Multiple FileRefs can point to
|
7
|
+
# the same FileData.
|
8
|
+
#
|
9
|
+
# A FileRef contains
|
10
|
+
# * the parent FileRef
|
11
|
+
# * the kind of relation (included or referenced)
|
12
|
+
# * the FileData instance
|
13
|
+
#
|
14
|
+
class FileRef
|
15
|
+
|
16
|
+
# TODO file and line?
|
17
|
+
attr_accessor :rel_type, :file_data, :parent
|
18
|
+
attr_reader :includes, :refs
|
19
|
+
|
20
|
+
def initialize(name,parent_dir=".",parent_file=nil)
|
21
|
+
@file_data = FileData.for(name,parent_dir)
|
22
|
+
@parent = (parent_file.nil? ? nil : parent_file.name)
|
23
|
+
@includes = []
|
24
|
+
@refs = []
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_missing(name, *args, &block)
|
28
|
+
@file_data.send(name,*args, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def includes=(incs)
|
32
|
+
@includes = incs
|
33
|
+
@file_data.add_includes(incs.map{|inc| inc.file_data})
|
34
|
+
end
|
35
|
+
|
36
|
+
def refs=(refs)
|
37
|
+
@refs = refs
|
38
|
+
@file_data.add_references(refs.map{|ref| ref.file_data})
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return a hash with the values for the passed symbols.
|
42
|
+
# The type is added.
|
43
|
+
#
|
44
|
+
# Example: to_hash([:name, :mime]) would return
|
45
|
+
# {:type => "main", :name => "name", :mime => "application/xml"}.
|
46
|
+
#
|
47
|
+
def to_hash(props,type)
|
48
|
+
me_hash = {:type => type}
|
49
|
+
props.each {|p| me_hash[p] = self.send(p)}
|
50
|
+
me_hash
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return a tree-like array of maps with the
|
54
|
+
# requested properties (symbols)
|
55
|
+
def traverse(props=[],type=FileRefTypes::TYPE_MAIN)
|
56
|
+
me = self.to_hash(props,type)
|
57
|
+
me2 = [me]
|
58
|
+
unless self.refs.empty?()
|
59
|
+
me2 += self.refs.map {|r| r.to_hash(props,FileRefTypes::TYPE_REFERENCE)}
|
60
|
+
end
|
61
|
+
if self.includes.empty?()
|
62
|
+
me2
|
63
|
+
else
|
64
|
+
me2 + self.includes.map {|i| i.traverse(props,FileRefTypes::TYPE_INCLUDE)}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return the names and parent files of non-existing files
|
69
|
+
def find_non_existing_files
|
70
|
+
files = traverse([:name, :status, :parent])
|
71
|
+
files.flatten.reject{|f| f[:status] != FileData::STATUS_NOT_FOUND}.map{|f| f.delete(:status); f}
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return a tree-like array with all names
|
75
|
+
def names
|
76
|
+
self.traverse([:name])
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Return a table-like array of maps with the
|
81
|
+
# requested properties (symbols). Each entry gets a level
|
82
|
+
# indicator (:level) to show the tree-level.
|
83
|
+
#
|
84
|
+
def traverse_as_table(props,level=0,type=FileRefTypes::TYPE_MAIN)
|
85
|
+
me = self.to_hash(props,type)
|
86
|
+
me[:level] = level
|
87
|
+
me2 = [me]
|
88
|
+
unless self.refs.empty?()
|
89
|
+
me2 += self.refs.map {|r| x = r.to_hash(props,FileRefTypes::TYPE_REFERENCE); x[:level] = level+1; x}
|
90
|
+
end
|
91
|
+
unless self.includes.empty?()
|
92
|
+
me2 += self.includes.map {|i| i.traverse_as_table(props,level+1,FileRefTypes::TYPE_INCLUDE)}
|
93
|
+
end
|
94
|
+
me2.flatten
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module DocbookFiles
|
4
|
+
|
5
|
+
# Contains thre reference type for relations between files in DocBook.
|
6
|
+
class FileRefTypes
|
7
|
+
|
8
|
+
# Type for the main/master file
|
9
|
+
TYPE_MAIN = :main
|
10
|
+
|
11
|
+
# Type for referenced files (via fileref attribute)
|
12
|
+
TYPE_REFERENCE = :ref
|
13
|
+
TYPE_REFERENCED_BY = :refdby
|
14
|
+
|
15
|
+
# Type for included files (XInclude)
|
16
|
+
TYPE_INCLUDE = :inc
|
17
|
+
TYPE_INCLUDED_BY = :incdby
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -1,24 +1,17 @@
|
|
1
1
|
# -*- encoding:utf-8 -*-
|
2
2
|
require_relative '../spec_helper'
|
3
|
+
require 'stringio'
|
3
4
|
|
4
5
|
module DocbookFiles
|
5
6
|
describe App do
|
6
7
|
|
7
8
|
describe "displays file names" do
|
9
|
+
|
8
10
|
it "according to level" do
|
9
11
|
a = App.new
|
10
|
-
|
11
|
-
|
12
|
-
a.format_name(
|
13
|
-
a.format_name(2,main_d+'chapter.xml',main_n).should == ' chapter.xml'
|
14
|
-
a.format_name(3,main_d+'dir4/chapter.xml',main_n).should == ' dir4/chapter.xml'
|
15
|
-
end
|
16
|
-
|
17
|
-
it "in full when not below main file" do
|
18
|
-
a = App.new
|
19
|
-
main_n = '/dir1/dir2/dir3/book.xml'
|
20
|
-
main_d = '/dir1/dir2/dir3/'
|
21
|
-
a.format_name(2,'/dir1/dir2/chapter4.xml',main_n).should == ' /dir1/dir2/chapter4.xml'
|
12
|
+
a.format_name(0,'book.xml').should == 'book.xml'
|
13
|
+
a.format_name(2,'chapter.xml').should == ' chapter.xml'
|
14
|
+
a.format_name(3,'chapter.xml').should == ' chapter.xml'
|
22
15
|
end
|
23
16
|
|
24
17
|
it "shortened when too long" do
|
@@ -26,10 +19,11 @@ module DocbookFiles
|
|
26
19
|
main_d = '/dir1/dir2'*5
|
27
20
|
main_d2 = '/dir0/dir1/dir2'*5
|
28
21
|
main_n = main_d+'/book.xml'
|
29
|
-
a.format_name(2,main_d+'/chapter.xml'
|
22
|
+
a.format_name(2,main_d+'/chapter.xml').should ==
|
23
|
+
' ...r2/dir1/dir2/dir1/dir2/dir1/dir2/dir1/dir2/chapter.xml'
|
30
24
|
expected = " ...r0/dir1/dir2/dir0/dir1/dir2/dir0/dir1/dir2/chapter.xml"
|
31
|
-
a.format_name(2,main_d2+'/chapter.xml'
|
32
|
-
a.format_name(2,main_d2+'/chapter.xml'
|
25
|
+
a.format_name(2,main_d2+'/chapter.xml').should == expected
|
26
|
+
a.format_name(2,main_d2+'/chapter.xml').length.should == 61
|
33
27
|
end
|
34
28
|
|
35
29
|
end
|
@@ -47,5 +41,65 @@ module DocbookFiles
|
|
47
41
|
a.format_size(1024 + App::TB).should == "1TB"
|
48
42
|
a.format_size(1024 + App::PB).should == "XXL"
|
49
43
|
end
|
44
|
+
|
45
|
+
it "can use YAML as output format" do
|
46
|
+
stdout1 = StringIO.new
|
47
|
+
stderr1 = StringIO.new
|
48
|
+
a = App.new({:stdout => stdout1, :stderr => stderr1})
|
49
|
+
a.run(['--outputformat=yaml','spec/fixtures/bookxi.xml'])
|
50
|
+
stderr1.string.should == ""
|
51
|
+
stdout1.string.should_not == ""
|
52
|
+
yml = YAML.load(stdout1.string)
|
53
|
+
yml.size.should == 2
|
54
|
+
hier = yml[:hierarchy]
|
55
|
+
det = yml[:details]
|
56
|
+
hier.size.should == 5
|
57
|
+
hier[0][:type].should == :main
|
58
|
+
hier[0][:path].should == "bookxi.xml"
|
59
|
+
hier[0][:level].should == 0
|
60
|
+
hier[4][:type].should == :inc
|
61
|
+
hier[4][:path].should == "c4/chapter4xi.xml"
|
62
|
+
hier[4][:level].should == 1
|
63
|
+
det.size.should == 5
|
64
|
+
det[0][:path].should == "bookxi.xml"
|
65
|
+
det[0][:error_string].should be_nil
|
66
|
+
det[0][:includes].should == ["chapter2xi.xml", "chapter3xi.xml", "c4/chapter4xi.xml"]
|
67
|
+
det[0][:included_by].should be_empty
|
68
|
+
det[4][:path].should == "c4/chapter4xi.xml"
|
69
|
+
det[4][:error_string].should be_nil
|
70
|
+
det[4][:includes].should be_empty
|
71
|
+
det[4][:included_by].should == ["bookxi.xml"]
|
72
|
+
end
|
73
|
+
|
74
|
+
it "can use JSON as output format" do
|
75
|
+
stdout1 = StringIO.new
|
76
|
+
stderr1 = StringIO.new
|
77
|
+
require 'json'
|
78
|
+
a = App.new({:stdout => stdout1, :stderr => stderr1, :json_available => true})
|
79
|
+
a.run(['--outputformat=json','spec/fixtures/bookxi.xml'])
|
80
|
+
stderr1.string.should == ""
|
81
|
+
stdout1.string.should_not == ""
|
82
|
+
yml = JSON.parse(stdout1.string)
|
83
|
+
yml.size.should == 2
|
84
|
+
hier = yml["hierarchy"]
|
85
|
+
det = yml["details"]
|
86
|
+
hier.size.should == 5
|
87
|
+
hier[0]["type"].should == "main"
|
88
|
+
hier[0]["path"].should == "bookxi.xml"
|
89
|
+
hier[0]["level"].should == 0
|
90
|
+
hier[4]["type"].should == "inc"
|
91
|
+
hier[4]["path"].should == "c4/chapter4xi.xml"
|
92
|
+
hier[4]["level"].should == 1
|
93
|
+
det.size.should == 5
|
94
|
+
det[0]["path"].should == "bookxi.xml"
|
95
|
+
det[0]["error_string"].should be_nil
|
96
|
+
det[0]["includes"].should == ["chapter2xi.xml", "chapter3xi.xml", "c4/chapter4xi.xml"]
|
97
|
+
det[0]["included_by"].should be_empty
|
98
|
+
det[4]["path"].should == "c4/chapter4xi.xml"
|
99
|
+
det[4]["error_string"].should be_nil
|
100
|
+
det[4]["includes"].should be_empty
|
101
|
+
det[4]["included_by"].should == ["bookxi.xml"]
|
102
|
+
end
|
103
|
+
|
50
104
|
end
|
51
105
|
end
|