docubot 0.3.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
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