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.
@@ -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