File: generator.rb

package info (click to toggle)
ruby-sdoc 0.4.1-2
  • links: PTS, VCS
  • area: main
  • in suites: buster, stretch
  • size: 896 kB
  • ctags: 523
  • sloc: ruby: 746; makefile: 3
file content (413 lines) | stat: -rw-r--r-- 11,954 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
require 'rubygems'
require 'erb'
require 'pathname'
require 'fileutils'
if Gem::Specification.respond_to?(:find_by_name) ? Gem::Specification::find_by_name("json") : Gem.available?("json")
  gem "json", ">= 1.1.3"
else
  gem "json_pure", ">= 1.1.3"
end
require 'json'

require 'sdoc/github'
require 'sdoc/templatable'
require 'sdoc/helpers'
require 'rdoc'

class RDoc::ClassModule
  def with_documentation?
    document_self_or_methods || classes_and_modules.any?{ |c| c.with_documentation? }
  end
end

class RDoc::Options
  attr_accessor :github
  attr_accessor :search_index
end

class RDoc::AnyMethod

  TITLE_AFTER = %w(def class module)

  ##
  # Turns the method's token stream into HTML.
  #
  # Prepends line numbers if +add_line_numbers+ is true.

  def sdoc_markup_code
    return '' unless @token_stream

    src = ""
    starting_title = false

    @token_stream.each do |t|
      next unless t

      style = case t
              when RDoc::RubyToken::TkFLOAT    then 'ruby-number'
              when RDoc::RubyToken::TkINTEGER  then 'ruby-number'
              when RDoc::RubyToken::TkCONSTANT then 'ruby-constant'
              when RDoc::RubyToken::TkKW       then 'ruby-keyword'
              when RDoc::RubyToken::TkIVAR     then 'ruby-ivar'
              when RDoc::RubyToken::TkOp       then 'ruby-operator'
              when RDoc::RubyToken::TkId       then 'ruby-identifier'
              when RDoc::RubyToken::TkNode     then 'ruby-node'
              when RDoc::RubyToken::TkCOMMENT  then 'ruby-comment'
              when RDoc::RubyToken::TkREGEXP   then 'ruby-regexp'
              when RDoc::RubyToken::TkSTRING   then 'ruby-string'
              when RDoc::RubyToken::TkVal      then 'ruby-value'
              end

      if RDoc::RubyToken::TkId === t && starting_title
        starting_title = false
        style = 'ruby-keyword ruby-title'
      end

      if RDoc::RubyToken::TkKW === t && TITLE_AFTER.include?(t.text)
        starting_title = true
      end

      text = CGI.escapeHTML t.text

      if style then
        src << "<span class=\"#{style}\">#{text}</span>"
      else
        src << text
      end
    end

    # dedent the source
    indent = src.length
    lines = src.lines.to_a
    lines.shift if src =~ /\A.*#\ *File/i # remove '# File' comment
    lines.each do |line|
      if line =~ /^ *(?=\S)/
        n = $&.length
        indent = n if n < indent
        break if n == 0
      end
    end
    src.gsub!(/^#{' ' * indent}/, '') if indent > 0

    add_line_numbers(src) if self.class.add_line_numbers

    src
  end

end

class RDoc::Generator::SDoc
  RDoc::RDoc.add_generator self

  DESCRIPTION = 'Searchable HTML documentation'

  include ERB::Util
  include SDoc::GitHub
  include SDoc::Templatable
  include SDoc::Helpers

  GENERATOR_DIRS = [File.join('sdoc', 'generator')]

  TREE_FILE = File.join 'panel', 'tree.js'
  SEARCH_INDEX_FILE = File.join 'js', 'search_index.js'

  FILE_DIR = 'files'
  CLASS_DIR = 'classes'

  RESOURCES_DIR = File.join('resources', '.')

  attr_reader :base_dir

  attr_reader :options

  ##
  # The RDoc::Store that is the source of the generated content

  attr_reader :store

  def self.setup_options(options)
    @github = false
    options.search_index = true

    opt = options.option_parser
    opt.separator nil
    opt.separator "SDoc generator options:"
    opt.separator nil
    opt.on("--github", "-g",
            "Generate links to github.") do |value|
      options.github = true
    end
    opt.separator nil

    opt.on("--without-search", "-s",
            "Do not generated index file for search engines.",
            "SDoc uses javascript to refrence individual documentation pages.",
            "Search engine crawlers are not smart enough to find all the",
            "referenced pages.",
            "To help them SDoc generates a static file with links to every",
            "documentation page. This file is not shown to the user."
            ) do
      options.search_index = false
    end
    opt.separator nil

  end

  def initialize(store, options)
    @store   = store
    @options = options
    if @options.respond_to?('diagram=')
      @options.diagram = false
    end
    @options.pipe = true
    @github_url_cache = {}

    @template_dir = Pathname.new(options.template_dir)
    @base_dir = Pathname.pwd.expand_path

    @json_index = RDoc::Generator::JsonIndex.new self, options
  end

  def generate
    @outputdir = Pathname.new(@options.op_dir).expand_path(@base_dir)
    @files = @store.all_files.sort
    @classes = @store.all_classes_and_modules.sort

    # Now actually write the output
    copy_resources
    generate_class_tree
    @json_index.generate
    generate_file_files
    generate_class_files
    generate_index_file
    generate_search_index if @options.search_index
  end

  def class_dir
    CLASS_DIR
  end

  def file_dir
    FILE_DIR
  end

  protected
  ### Output progress information if debugging is enabled
  def debug_msg( *msg )
    return unless $DEBUG_RDOC
    $stderr.puts( *msg )
  end

  ### Create class tree structure and write it as json
  def generate_class_tree
    debug_msg "Generating class tree"
    topclasses = @classes.select {|klass| !(RDoc::ClassModule === klass.parent) }
    tree = generate_file_tree + generate_class_tree_level(topclasses)
    debug_msg "  writing class tree to %s" % TREE_FILE
    File.open(TREE_FILE, "w", 0644) do |f|
      f.write('var tree = '); f.write(tree.to_json(:max_nesting => 0))
    end unless @options.dry_run
  end

  ### Recursivly build class tree structure
  def generate_class_tree_level(classes, visited = {})
    tree = []
    classes.select do |klass|
      !visited[klass] && klass.with_documentation?
    end.sort.each do |klass|
      visited[klass] = true
      item = [
        klass.name,
        klass.document_self_or_methods ? klass.path : '',
        klass.module? ? '' : (klass.superclass ? " < #{String === klass.superclass ? klass.superclass : klass.superclass.full_name}" : ''),
        generate_class_tree_level(klass.classes_and_modules, visited)
      ]
      tree << item
    end
    tree
  end

  ### Add files to search +index+ array
  def add_file_search_index(index)
    debug_msg "  generating file search index"

    @files.select { |file|
      file.document_self
    }.sort.each do |file|
      debug_msg "    #{file.path}"
      index[:searchIndex].push( search_string(file.name) )
      index[:longSearchIndex].push( search_string(file.path) )
      index[:info].push([
        file.name,
        file.path,
        file.path,
        '',
        snippet(file.comment),
        TYPE_FILE
      ])
    end
  end

  ### Add classes to search +index+ array
  def add_class_search_index(index)
    debug_msg "  generating class search index"
    @classes.uniq.select { |klass|
      klass.document_self_or_methods
    }.sort.each do |klass|
      modulename = klass.module? ? '' : (klass.superclass ? (String === klass.superclass ? klass.superclass : klass.superclass.full_name) : '')
      debug_msg "    #{klass.parent.full_name}::#{klass.name}"
      index[:searchIndex].push( search_string(klass.name) )
      index[:longSearchIndex].push( search_string(klass.parent.full_name) )
      files = klass.in_files.map{ |file| file.absolute_name }
      index[:info].push([
        klass.name,
        files.include?(klass.parent.full_name) ? files.first : klass.parent.full_name,
        klass.path,
        modulename ? " < #{modulename}" : '',
        snippet(klass.comment),
        TYPE_CLASS
      ])
    end
  end

  ### Add methods to search +index+ array
  def add_method_search_index(index)
    debug_msg "  generating method search index"

    list = @classes.uniq.map do |klass|
      klass.method_list
    end.flatten.sort do |a, b|
      a.name == b.name ?
        a.parent.full_name <=> b.parent.full_name :
        a.name <=> b.name
    end.select do |method|
      method.document_self
    end.find_all do |m|
      m.visibility == :public || m.visibility == :protected ||
      m.force_documentation
    end

    list.each do |method|
      debug_msg "    #{method.full_name}"
      index[:searchIndex].push( search_string(method.name) + '()' )
      index[:longSearchIndex].push( search_string(method.parent.full_name) )
      index[:info].push([
        method.name,
        method.parent.full_name,
        method.path,
        method.params,
        snippet(method.comment),
        TYPE_METHOD
      ])
    end
  end

  ### Generate a documentation file for each class
  def generate_class_files
    debug_msg "Generating class documentation in #@outputdir"
    templatefile = @template_dir + 'class.rhtml'

    @classes.each do |klass|
      debug_msg "  working on %s (%s)" % [ klass.full_name, klass.path ]
      outfile     = @outputdir + klass.path
      rel_prefix  = @outputdir.relative_path_from( outfile.dirname )

      debug_msg "  rendering #{outfile}"
      self.render_template( templatefile, binding(), outfile ) unless @options.dry_run
    end
  end

  ### Generate a documentation file for each file
  def generate_file_files
    debug_msg "Generating file documentation in #@outputdir"
    templatefile = @template_dir + 'file.rhtml'

    @files.each do |file|
      outfile     = @outputdir + file.path
      debug_msg "  working on %s (%s)" % [ file.full_name, outfile ]
      rel_prefix  = @outputdir.relative_path_from( outfile.dirname )

      debug_msg "  rendering #{outfile}"
      self.render_template( templatefile, binding(), outfile ) unless @options.dry_run
    end
  end

  ### Determines index path based on @options.main_page (or lack thereof)
  def index_path
    # Break early to avoid a big if block when no main page is specified
    default = @files.first.path
    return default unless @options.main_page

    # Transform class name to file path
    if @options.main_page.include?("::")
      slashed = @options.main_page.sub(/^::/, "").gsub("::", "/")
      "%s/%s.html" % [ class_dir, slashed ]
    elsif file = @files.find { |f| f.full_name == @options.main_page }
      file.path
    else
      default
    end
  end

  ### Create index.html with frameset
  def generate_index_file
    debug_msg "Generating index file in #@outputdir"
    templatefile = @template_dir + 'index.rhtml'
    outfile      = @outputdir + 'index.html'

    self.render_template( templatefile, binding(), outfile ) unless @options.dry_run
  end

  ### Generate file with links for the search engine
  def generate_search_index
    debug_msg "Generating search engine index in #@outputdir"
    templatefile = @template_dir + 'search_index.rhtml'
    outfile      = @outputdir + 'panel/links.html'

    self.render_template( templatefile, binding(), outfile ) unless @options.dry_run
  end

  ### Copy all the resource files to output dir
  def copy_resources
    resources_path = @template_dir + RESOURCES_DIR
    debug_msg "Copying #{resources_path}/** to #{@outputdir}/**"
    FileUtils.cp_r resources_path.to_s, @outputdir.to_s unless @options.dry_run
  end

  class FilesTree
    attr_reader :children
    def add(path, url)
      path = path.split(File::SEPARATOR) unless Array === path
      @children ||= {}
      if path.length == 1
        @children[path.first] = url
      else
        @children[path.first] ||= FilesTree.new
        @children[path.first].add(path[1, path.length], url)
      end
    end
  end

  def generate_file_tree
    if @files.length > 1
      @files_tree = FilesTree.new
      @files.each do |file|
        @files_tree.add(file.relative_name, file.path)
      end
      [['', '', 'files', generate_file_tree_level(@files_tree)]]
    else
      []
    end
  end

  def generate_file_tree_level(tree)
    tree.children.keys.sort.map do |name|
      child = tree.children[name]
      if String === child
        [name, child, '', []]
      else
        ['', '', name, generate_file_tree_level(child)]
      end
    end
  end
end