docubot 0.3.3 → 0.4

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.
Files changed (80) hide show
  1. data/bin/docubot +5 -1
  2. data/lib/docubot.rb +4 -2
  3. data/lib/docubot/bundle.rb +109 -61
  4. data/lib/docubot/converter.rb +4 -1
  5. data/lib/docubot/converters/haml.rb +1 -1
  6. data/lib/docubot/glossary.rb +3 -0
  7. data/lib/docubot/link_tree.rb +109 -0
  8. data/lib/docubot/metasection.rb +61 -0
  9. data/lib/docubot/page.rb +40 -118
  10. data/lib/docubot/shells/docubot-help/3_Advanced_Topics/Controlling the Table of Contents.md +2 -4
  11. data/lib/docubot/shells/nvphysx/_templates/_root/common.css +10 -5
  12. data/lib/docubot/shells/nvphysx/_templates/_root/glossary.css +1 -0
  13. data/lib/docubot/shells/nvphysx/_templates/_root/glossary.js +11 -1
  14. data/lib/docubot/shells/nvphysx/_templates/section.haml +9 -0
  15. data/lib/docubot/templates/index.haml +13 -20
  16. data/lib/docubot/templates/section.haml +1 -4
  17. data/lib/docubot/templates/toc.haml +2 -11
  18. data/lib/docubot/templates/top.haml +7 -4
  19. data/lib/docubot/writers/chm.rb +6 -5
  20. data/lib/docubot/writers/chm/hhc.erb +35 -12
  21. data/lib/docubot/writers/chm/hhk.erb +1 -1
  22. data/lib/docubot/writers/chm/hhp.erb +2 -2
  23. data/lib/docubot/writers/html.rb +33 -23
  24. data/spec/_all.rb +1 -0
  25. data/spec/_helper.rb +10 -0
  26. data/spec/bundle.rb +193 -2
  27. data/spec/global.rb +28 -0
  28. data/spec/glossary.rb +13 -11
  29. data/spec/page.rb +35 -9
  30. data/spec/samples/attributes/defaults.haml +34 -0
  31. data/spec/samples/attributes/explicit1.haml +43 -0
  32. data/spec/samples/attributes/explicit2.haml +42 -0
  33. data/spec/samples/attributes/hidden.haml +40 -0
  34. data/spec/samples/attributes/index.md +8 -0
  35. data/spec/samples/collisions/page1.md +3 -0
  36. data/spec/samples/collisions/page1.textile +3 -0
  37. data/spec/samples/collisions/page2.haml +4 -0
  38. data/spec/samples/collisions/page2.html +3 -0
  39. data/spec/samples/collisions/page2.txt +3 -0
  40. data/spec/samples/collisions/page3.bin +1 -0
  41. data/spec/samples/collisions/page3.md +3 -0
  42. data/spec/samples/{link_test/sub2/bozo.bin → files/BUILDING.txt} +0 -0
  43. data/spec/samples/{titletest/1 First One.txt → files/_static/Thumbs.db} +0 -0
  44. data/spec/samples/{titletest/2_Second_One.txt → files/_static/foo.ai} +0 -0
  45. data/spec/samples/{titletest/4 Fourth_One.txt → files/_static/foo.png} +0 -0
  46. data/spec/samples/{titletest/5_Fifth One.txt → files/_static/foo.psd} +0 -0
  47. data/spec/samples/files/another.md +0 -0
  48. data/spec/samples/files/common.css +0 -0
  49. data/spec/samples/files/first.textile +0 -0
  50. data/spec/samples/files/index.md +2 -0
  51. data/spec/samples/files/section/foo.jpg +0 -0
  52. data/spec/samples/files/section/page.haml +0 -0
  53. data/spec/samples/files/section/sub section/Thumbs.db b/data/spec/samples/files/section/sub → section/Thumbs.db +0 -0
  54. data/spec/samples/files/section/sub section/foo.gif b/data/spec/samples/files/section/sub → section/foo.gif +0 -0
  55. data/spec/samples/files/section/sub section/page.txt b/data/spec/samples/files/section/sub → section/page.txt +0 -0
  56. data/spec/samples/{link_test → links}/index.txt +0 -0
  57. data/spec/samples/{link_test → links}/root.md +0 -0
  58. data/spec/samples/{link_test → links}/sub1/inner1.md +0 -0
  59. data/spec/samples/{link_test → links}/sub2.md +0 -0
  60. data/spec/samples/links/sub2/bozo.bin +0 -0
  61. data/spec/samples/{link_test → links}/sub2/inner2.md +0 -0
  62. data/spec/samples/templates/_templates/404.haml +1 -0
  63. data/spec/samples/templates/_templates/doubler.haml +7 -0
  64. data/spec/samples/templates/_templates/page.haml +2 -0
  65. data/spec/samples/templates/goaway.txt +3 -0
  66. data/spec/samples/templates/onepara_html.html +3 -0
  67. data/spec/samples/templates/onepara_md.md +5 -0
  68. data/spec/samples/templates/twopara_haml.haml +7 -0
  69. data/spec/samples/templates/twopara_textile.textile +6 -0
  70. data/spec/samples/titles/1 First One.txt b/data/spec/samples/titles/1 First → One.txt +0 -0
  71. data/spec/samples/titles/2_Second_One.txt +0 -0
  72. data/spec/samples/{titletest → titles}/3_renamed.txt +0 -0
  73. data/spec/samples/titles/4 Fourth_One.txt b/data/spec/samples/titles/4 → Fourth_One.txt +0 -0
  74. data/spec/samples/titles/5_Fifth One.txt b/data/spec/samples/titles/5_Fifth → One.txt +0 -0
  75. data/spec/samples/titles/911.txt +0 -0
  76. data/spec/samples/titles/index.txt +2 -0
  77. data/spec/templates.rb +42 -0
  78. data/spec/toc.rb +64 -30
  79. metadata +53 -14
  80. data/spec/samples/titletest/index.txt +0 -2
@@ -99,8 +99,12 @@ if ARGS[:create]
99
99
  end
100
100
  end
101
101
  else
102
+ # require 'perftools'
102
103
  start = Time.now
103
- bundle = DocuBot::Bundle.new( ARGS[:directory] )
104
+ # bundle = nil
105
+ # PerfTools::CpuProfiler.start("/tmp/docubot") do
106
+ bundle = DocuBot::Bundle.new( ARGS[:directory] )
107
+ # end
104
108
  lap = Time.now
105
109
  puts "%.2fs to prepare the bundle..." % (lap-start)
106
110
  bundle.write( ARGS[:writer], ARGS[:output] )
@@ -20,7 +20,7 @@ module FileUtils
20
20
  end
21
21
 
22
22
  module DocuBot
23
- VERSION = '0.3.3'
23
+ VERSION = '0.4'
24
24
  DIR = File.expand_path( File.dirname( __FILE__ ) )
25
25
 
26
26
  TEMPLATE_DIR = DIR / 'docubot/templates'
@@ -28,10 +28,12 @@ module DocuBot
28
28
  Dir.chdir( SHELL_DIR ){ SHELLS = Dir['*'] }
29
29
 
30
30
  def self.id_from_text( text )
31
- text.strip.gsub(/[^\w.:-]+/,'-').gsub(/^-|-$/,'')
31
+ "#" << text.strip.gsub(/[^\w.:-]+/,'-').gsub(/^[^a-z]+|-+$/i,'')
32
32
  end
33
33
  end
34
34
 
35
+ require 'docubot/link_tree'
36
+ require 'docubot/metasection'
35
37
  require 'docubot/snippet'
36
38
  require 'docubot/converter'
37
39
  require 'docubot/writer'
@@ -1,64 +1,49 @@
1
1
  # encoding: UTF-8
2
2
  require 'pathname'
3
3
  class DocuBot::Bundle
4
- attr_reader :toc, :extras, :glossary, :index, :source
4
+ attr_reader :toc, :extras, :glossary, :index, :source, :global
5
5
  attr_reader :internal_links, :external_links, :file_links, :broken_links
6
+ attr_reader :pages, :pages_by_title, :page_by_file_path, :page_by_html_path
6
7
  def initialize( source_directory )
7
8
  @source = File.expand_path( source_directory )
8
9
  raise "DocuBot cannot find directory #{@source}. Exiting." unless File.exists?( @source )
10
+ @pages = []
9
11
  @extras = []
12
+ @pages_by_title = Hash.new{ |h,k| h[k]=[] }
13
+ @page_by_file_path = {}
14
+ @page_by_html_path = {}
15
+
10
16
  @glossary = DocuBot::Glossary.new( self, @source/'_glossary' )
11
17
  @index = DocuBot::Index.new( self )
18
+ @toc = DocuBot::LinkTree::Root.new( self )
19
+
12
20
  Dir.chdir( @source ) do
13
- @toc = DocuBot::Page.new( self, ".", "Table of Contents" )
14
- @toc.meta['glossary'] = @glossary
15
- @toc.meta['index'] = @index
16
- pages_by_path = { '.'=>@toc }
17
-
21
+ # This might be nil; MetaSection.new is OK with that.
22
+ index_file = Dir[ *DocuBot::Converter.types.map{|t| "index.#{t}"} ][ 0 ]
23
+ @global = DocuBot::MetaSection.new( {}, index_file )
24
+ @global.glossary = @glossary
25
+ @global.index = @index
26
+ @global.toc = @toc
27
+
18
28
  files_and_folders = Dir[ '**/*' ]
19
- files_and_folders.reject!{ |f| File.basename(f) =~ /^index\.[^.]+$/ || File.basename(f) == '_static' || File.basename(f) == '_glossary' }
20
- files_and_folders.reject!{ |f| f =~ /\b_templates\b/ }
21
- files_and_folders.each do |item|
22
- extension = File.extname( item )[ 1..-1 ]
23
- item_is_page = File.directory?(item) || DocuBot::Converter.by_type[extension]
24
- if item_is_page
25
- parent = pages_by_path[ File.dirname( item ) ]
26
- page = DocuBot::Page.new( self, item )
27
- pages_by_path[ item ] = page
28
- parent << page if parent
29
- if item =~ /\b_glossary\b/
30
- @glossary << page
31
- end
32
- @index.process_page( page )
33
-
34
- # TODO: Move this bloat elsewhere.
35
- if page.toc?
36
- ndoc = page.nokodoc
37
- toc = page.toc
38
- ids = if toc[','] # Comma-delimited toc interpreted as generated ids
39
- toc.split(/,\s*/).map{ |title| DocuBot.id_from_text(title) }
40
- else
41
- toc.scan /[a-z][\w.:-]*/i
42
- end
43
- ids.each do |id|
44
- if ele = ndoc.at_css("##{id}")
45
- page << DocuBot::SubLink.new( page, ele.inner_text, id )
46
- else
47
- warn "Could not find requested toc anchor '##{id}' on #{page.html_path}"
48
- end
49
- end
50
- end
51
-
52
- else
53
- # TODO: Anything better needed?
54
- @extras << item
55
- end
29
+
30
+ # index files are handled by Page.new for a directory; no sections for special folders (but process contents)
31
+ files_and_folders.reject!{ |path| name = File.basename( path ); name =~ /^(?:index\.[^.]+|_static|_glossary)$/ }
32
+
33
+ # All files in the _templates directory should be ignored
34
+ files_and_folders.reject!{ |f| f =~ /^_templates\b/ }
35
+
36
+ @global.ignore.as_list.each do |glob|
37
+ files_and_folders = files_and_folders - Dir[glob]
56
38
  end
39
+
40
+ create_pages( files_and_folders )
57
41
  end
42
+ # puts @toc.to_txt
58
43
 
59
44
  # Regenerate pages whose templates require full scaning to have completed
60
45
  # TODO: make this based off of a metasection attribute.
61
- @toc.every_page.select do |page|
46
+ @pages.select do |page|
62
47
  %w[ glossary ].include?( page.template )
63
48
  end.each do |page|
64
49
  page.dirty_template
@@ -66,20 +51,48 @@ class DocuBot::Bundle
66
51
 
67
52
  # TODO: make this optional via global variable
68
53
  validate_links
69
- @broken_links.each do |page,links|
70
- links.each do |link|
71
- warn "Broken link on #{page.file}: '#{link}'"
72
- end
73
- end
54
+ warn_for_broken_links
74
55
 
75
56
  # TODO: make this optional via global variable
76
- @glossary.missing_terms.each do |term,referrers|
77
- warn "Glossary term '#{term}' never defined."
78
- referrers.each do |referring_page|
79
- warn "...seen on #{referring_page.file}."
80
- end
81
- end
57
+ warn_for_missing_glossary_terms
82
58
 
59
+ find_page_collisions
60
+ end
61
+
62
+ def create_pages( files_and_folders )
63
+ files_and_folders.each do |path|
64
+ extension = File.extname( path )[ 1..-1 ]
65
+ item_is_page = File.directory?(path) || DocuBot::Converter.by_type[extension]
66
+ if !item_is_page
67
+ @extras << path
68
+ else
69
+ page = DocuBot::Page.new( self, path )
70
+
71
+ if path =~ %r{^_glossary/}
72
+ @glossary << page
73
+ else
74
+ @pages << page
75
+ @page_by_file_path[path] = page
76
+ @page_by_html_path[page.html_path] = page
77
+ @pages_by_title[page.title] << page
78
+ @index.process_page( page )
79
+
80
+ # Add the page (and any sub-links) to the toc
81
+ unless page.hide
82
+ @toc.add_to_link_hierarchy( page.title, page.html_path, page )
83
+ page.toc.as_list.each do |id_or_text|
84
+ id = id_or_text[0..0] == '#' ? id_or_text : DocuBot.id_from_text(id_or_text)
85
+ if ele = page.nokodoc.at_css(id)
86
+ @toc.add_to_link_hierarchy( ele.inner_text, page.html_path + id, page )
87
+ else
88
+ warn "Could not find requested toc anchor #{id.inspect} based on #{id_or_text.inspect} on #{page.html_path}"
89
+ end
90
+ end
91
+ end
92
+
93
+ end
94
+ end
95
+ end
83
96
  end
84
97
 
85
98
  def validate_links
@@ -90,26 +103,26 @@ class DocuBot::Bundle
90
103
 
91
104
  page_by_html_path = {}
92
105
  page_by_orig_path = {}
93
- @toc.every_page.each do |page|
106
+ @pages.each do |page|
94
107
  page_by_html_path[page.html_path] = page
95
108
  page_by_orig_path[page.file] = page if page.file
96
109
  end
97
110
 
98
111
  Dir.chdir( @source ) do
99
- @toc.every_page.each do |page|
100
- next unless page.nokodoc # Sub-links don't have documents
101
- page.nokodoc.xpath('//a/@href').each do |href|
112
+ @pages.each do |page|
113
+ page.nokodoc.xpath('.//a/@href').each do |href|
102
114
  href=href.content
103
115
  if href=~%r{^[a-z]+://}i
104
116
  @external_links[page] << href
105
117
  else
106
- id = href[/(#[a-z][\w.:-]*)/i,1]
118
+ id = href[/#[a-z][\w.:-]*/i]
107
119
  file = href.sub(/#.+/,'')
108
120
  path = file.empty? ? page.html_path : Pathname.new( File.dirname(page.html_path) / file ).cleanpath.to_s
109
121
  if target=page_by_html_path[path]
110
122
  if !id || target.nokodoc.at_css(id)
111
123
  @internal_links[page] << href
112
124
  else
125
+ warn "Could not find internal link for #{id.inspect} on #{page.html_path.inspect}" if id
113
126
  @broken_links[page] << href
114
127
  end
115
128
  else
@@ -124,6 +137,38 @@ class DocuBot::Bundle
124
137
  end
125
138
  end
126
139
  end
140
+
141
+ def warn_for_broken_links
142
+ @broken_links.each do |page,links|
143
+ links.each do |link|
144
+ warn "Broken link on #{page.file}: '#{link}'"
145
+ end
146
+ end
147
+ end
148
+
149
+ def warn_for_missing_glossary_terms
150
+ @glossary.missing_terms.each do |term,referrers|
151
+ warn "Glossary term '#{term}' never defined."
152
+ referrers.each do |referring_page|
153
+ warn "...seen on #{referring_page.file}."
154
+ end
155
+ end
156
+ end
157
+
158
+ def find_page_collisions
159
+ # Find any and all pages that would collide
160
+ pages_by_html_path = Hash.new{ |h,k| h[k] = [] }
161
+ @pages.each do |page|
162
+ pages_by_html_path[page.html_path] << page
163
+ end
164
+ collisions = pages_by_html_path.select{ |path,pages| pages.length>1 }
165
+ unless collisions.empty?
166
+ message = collisions.map do |path,pages|
167
+ "#{path}: #{pages.map{ |page| "'#{page.title}' (#{page.file})" }.join(', ')}"
168
+ end.join("\n")
169
+ raise PageCollision.new, message
170
+ end
171
+ end
127
172
 
128
173
  def write( writer_type, destination=nil )
129
174
  writer = DocuBot::Writer.by_type[ writer_type.to_s.downcase ]
@@ -134,4 +179,7 @@ class DocuBot::Bundle
134
179
  end
135
180
  end
136
181
 
137
- end
182
+ end
183
+
184
+ class DocuBot::Bundle::PageCollision < RuntimeError; end
185
+
@@ -8,6 +8,9 @@ module DocuBot
8
8
  def self.by_type
9
9
  @by_type
10
10
  end
11
+ def self.types
12
+ @by_type.keys
13
+ end
11
14
  end
12
15
 
13
16
  def self.convert_to_html( page, source, type )
@@ -22,4 +25,4 @@ end
22
25
 
23
26
  Dir[ DocuBot::DIR/'docubot/converters/*.rb' ].each do |converter|
24
27
  require converter
25
- end
28
+ end
@@ -5,5 +5,5 @@ options = { :format=>:html4, :ugly=>true }
5
5
  options.merge!( :encoding=>'utf-8' ) if Object.const_defined? "Encoding"
6
6
 
7
7
  DocuBot::Converter.to_convert :haml do |page, source|
8
- Haml::Engine.new( source, options ).render( page, :page=>page, :global=>page.bundle.toc, :root=>page.root )
8
+ Haml::Engine.new( source, options ).render( page, :page=>page, :global=>page.bundle.global, :root=>page.root )
9
9
  end
@@ -46,4 +46,7 @@ class DocuBot::Glossary
46
46
  end.join(",\n")
47
47
  }};"
48
48
  end
49
+ def inspect
50
+ "<#{self.class} terms='#{@entries.keys.join ', '}'>"
51
+ end
49
52
  end
@@ -0,0 +1,109 @@
1
+ # encoding: UTF-8
2
+ module DocuBot::LinkTree; end
3
+
4
+ class DocuBot::LinkTree::Node
5
+ attr_accessor :title, :link, :page, :parent
6
+
7
+ def initialize( title=nil, link=nil, page=nil )
8
+ @title,@link,@page = title,link,page
9
+ @children = []
10
+ end
11
+
12
+ def anchor
13
+ @link[/#(.+)/,1]
14
+ end
15
+
16
+ def file
17
+ @link.sub(/#.+/,'')
18
+ end
19
+
20
+ def leaf?
21
+ !@children.any?{ |node| node.page != @page }
22
+ end
23
+
24
+ # Add a new link underneath a link to its logical parent
25
+ def add_to_link_hierarchy( title, link, page=nil )
26
+ node = DocuBot::LinkTree::Node.new( title, link, page )
27
+ parent_link = if node.anchor
28
+ node.file
29
+ elsif File.basename(link)=='index.html'
30
+ File.dirname(File.dirname(link))/'index.html'
31
+ else
32
+ (File.dirname(link) / 'index.html')
33
+ end
34
+ #puts "Adding #{title.inspect} (#{link}) to hierarchy under #{parent_link}"
35
+ parent = descendants.find{ |node| node.link==parent_link } || self
36
+ parent << node
37
+ end
38
+
39
+ def <<( node )
40
+ node.parent = self
41
+ @children << node
42
+ end
43
+
44
+ def children( parent_link=nil, &block )
45
+ if parent_link
46
+ root = find( parent_link )
47
+ root ? root.children( &block ) : []
48
+ else
49
+ @children
50
+ end
51
+ end
52
+
53
+ def descendants
54
+ ( @children + @children.map{ |child| child.descendants } ).flatten
55
+ end
56
+
57
+ def find( link )
58
+ # TODO: this is eminently cachable
59
+ descendants.find{ |node| node.link==link }
60
+ end
61
+
62
+ def depth
63
+ # Assuming no one is going to shuffle the nodes after placement
64
+ @depth ||= ancestors.length
65
+ end
66
+
67
+ def root
68
+ @root ||= "../" * (depth + ( leaf? ? 0 : 1 ))
69
+ end
70
+
71
+ def ancestors
72
+ ancestors = []
73
+ node = self
74
+ ancestors << node while node = node.parent
75
+ ancestors.reverse!
76
+ end
77
+
78
+ def to_s
79
+ "#{@title} (#{@link}) - #{@page && @page.title}"
80
+ end
81
+
82
+ def to_txt( depth=0 )
83
+ indent = " "*depth
84
+ [
85
+ indent+to_s,
86
+ children.map{|kid|kid.to_txt(depth+1)}
87
+ ].flatten.join("\n")
88
+ end
89
+ end
90
+
91
+ class DocuBot::LinkTree::Root < DocuBot::LinkTree::Node
92
+ undef_method :title
93
+ undef_method :link
94
+ undef_method :page
95
+ attr_reader :bundle
96
+ def initialize( bundle )
97
+ @bundle = bundle
98
+ @children = []
99
+ end
100
+
101
+ def <<( node )
102
+ node.parent = nil
103
+ @children << node
104
+ end
105
+
106
+ def to_s
107
+ "(Table of Contents)"
108
+ end
109
+ end
@@ -0,0 +1,61 @@
1
+ # encoding: UTF-8
2
+ class DocuBot::MetaSection; end
3
+ module DocuBot::MetaSection::Castable
4
+ def as_boolean
5
+ self == 'true'
6
+ end
7
+ def as_list
8
+ return [] if self.nil?
9
+ # Replace commas inside quoted strings with unlikely-to-be-used Unicode
10
+ # FIXME: This doesnt work for the sadistic case of "hello """, "world"
11
+ csv = self.gsub( /("(?:[^",]|"")+),/, '\\1⥸' )
12
+ csv.split(/\s*,\s*/).map do |str|
13
+ # Put real commas back, unquote, undouble internal quotes
14
+ str[/^".*"$/] ? str[1..-2].gsub('⥸',',').gsub('""','"') : str
15
+ end
16
+ end
17
+ end
18
+
19
+ class DocuBot::MetaSection
20
+ META_SEPARATOR = /^\+\+\+\s*$/ # Sort of like +++ATH0
21
+ NIL_CASTABLE = nil.extend( Castable )
22
+ attr_reader :__contents__
23
+
24
+ def initialize( attrs={}, file_path=nil )
25
+ @attrs = {}
26
+ attrs.each{ |key,value| self[key]=value }
27
+ if file_path && File.exists?( file_path )
28
+ parts = IO.read( file_path ).split( META_SEPARATOR, 2 )
29
+ if parts.length > 1
30
+ parts.first.scan(/.+/) do |line|
31
+ next if line =~ /^\s*#/
32
+ next unless line.include?(':')
33
+ attribute, value = line.split(':',2).map{ |str| str.strip }
34
+ self[attribute] = value
35
+ end
36
+ end
37
+ @__contents__ = parts.last && parts.last.strip
38
+ end
39
+ end
40
+
41
+ def has_key?( key )
42
+ @attrs.has_key?( key )
43
+ end
44
+
45
+ def []( attribute )
46
+ @attrs.has_key?( attribute ) ? @attrs[attribute] : NIL_CASTABLE
47
+ end
48
+
49
+ def []=( attribute, value )
50
+ @attrs[attribute.to_s] = value.extend(Castable)
51
+ end
52
+
53
+ def method_missing( method, *args )
54
+ key=method.to_s
55
+ case key[-1..-1] # the last character of the method name
56
+ when '=' then self[key[0..-2]] = args.first
57
+ else self[key]
58
+ end
59
+ end
60
+
61
+ end