docbook_files 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- FileData.new(fname,parent_dir,parent_fd)
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 = FileData.new(fname, parent_dir, parent_fd)
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
- # Data about a member file of a DocBook project
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
- attr_accessor :name, :path, :exists, :includes, :refs
25
- attr_accessor :status, :error_string
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
- def FileData.init_vars()
28
- x = {:full_name => "file name + path",
29
- :ts => "last modified timestamp",
30
- :size => "file size",
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
- def initialize(name,parent_dir=".",parent_file=nil)
48
- @path = name
49
- @full_name = get_full_name(name, parent_dir)
50
- @name = File.basename(name)
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
- @parent = (parent_file.nil? ? nil : parent_file.name)
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
- # Return the names and parent files of non-existing files
82
- def find_non_existing_files
83
- files = traverse([:name, :status, :parent])
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
- # Return a tree-like array with all names
88
- def names
89
- self.traverse([:name])
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
- # Return a hash with the values for the passed symbols.
93
- # The type is added.
94
- #
95
- # Example: to_hash([:name, :mime]) would return
96
- # {:name => "name", :mime => "application/xml"}.
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
- def to_hash(props,type)
99
- me_hash = {:type => type}
100
- props.each {|p| me_hash[p] = self.send(p)}
101
- me_hash
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
- # Return a tree-like array of maps with the
105
- # requested properties (symbols)
106
- def traverse(props=[],type=TYPE_MAIN)
107
- me = self.to_hash(props,type)
108
- me2 = [me]
109
- unless @refs.empty?()
110
- me2 += @refs.map {|r| r.to_hash(props,TYPE_REFERENCE)}
111
- end
112
- if @includes.empty?()
113
- me2
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
- me2 + @includes.map {|i| i.traverse(props,TYPE_INCLUDE)}
157
+ md.post_match
116
158
  end
117
159
  end
118
160
 
119
- # Return a table-like array of maps with the
120
- # requested properties (symbols). Each entry gets a level
121
- # indicator (:level) to show the tree-level.
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 traverse_as_table(props,level=0,type=TYPE_MAIN)
124
- me = self.to_hash(props,type)
125
- me[:level] = level
126
- me2 = [me]
127
- unless @refs.empty?()
128
- me2 += @refs.map {|r| x = r.to_hash(props,TYPE_REFERENCE); x[:level] = level+1; x}
129
- end
130
- unless @includes.empty?()
131
- me2 += @includes.map {|i| i.traverse_as_table(props,level+1,TYPE_INCLUDE)}
132
- end
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(@full_name, "rb") {|io| io.read }
187
+ contents = open(full_name, "rb") {|io| io.read }
146
188
  else
147
- contents = IO.binread(@full_name)
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
- main_n = '/dir1/dir2/dir3/book.xml'
11
- main_d = '/dir1/dir2/dir3/'
12
- a.format_name(0,main_n,main_n).should == 'book.xml'
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',main_n).should == ' 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',main_n).should == expected
32
- a.format_name(2,main_d2+'/chapter.xml',main_n).length.should == 61
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