papyrus 0.0.1

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.
@@ -0,0 +1,382 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of Papyrus.
4
+ #
5
+ # Papyrus is a RDoc plugin for generating PDF files.
6
+ # Copyright © 2011 Pegasus Alpha
7
+ #
8
+ # Papyrus is free software; you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation; either version 2 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # Papyrus is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with Papyrus; if not, write to the Free Software
20
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
+
22
+ require "fileutils"
23
+ require "pathname"
24
+ require "erb"
25
+ require "open3"
26
+ gem "rdoc"
27
+ require "rdoc/rdoc"
28
+ require "rdoc/generator"
29
+ require_relative "papyrus/options" #Rest required in #initialize
30
+
31
+ #This is the main class for the Papyrus PDF generator for RDoc. It takes
32
+ #RDoc’s raw parsed data and transforms it into a single PDF file
33
+ #backed by pdfLaTeX. If you’re interested in how this process works,
34
+ #feel free to dig into this class’ code, but ensure you also have
35
+ #a look on the Markup::ToLaTeX class which does the heavy work of
36
+ #translating the markup tokens into LaTeX code. The below section
37
+ #also has a brief overview of how the generation process works.
38
+ #
39
+ #== The generation process
40
+ #
41
+ #1. During startup, RDoc calls the ::setup_options method that adds
42
+ # some commandline options to RDoc (these are described in the
43
+ # RDoc::Generator::Papyrus::Options module).
44
+ #2. RDoc parses the source files.
45
+ #3. RDoc calls the #generate method and passes it all encountered
46
+ # files as an array of RDoc::TopLevel objects.
47
+ #4. The #generate method examines all encountered classes, modules
48
+ # and methods and then starts to transform the raw data handed by
49
+ # RDoc into LaTeX markup by means of the formatter class
50
+ # RDoc::Markup::ToLaTeX_Crossref. The generated markup is written
51
+ # into a temporary directory "tmp" below the output directory
52
+ # RDoc has chosen or has been instructed to choose (via commandline),
53
+ # generating one LaTeX file for each class and module plus one main
54
+ # file "main.tex" that includes the other files as needed.
55
+ #5. pdfLaTeX is invoked on that final main file, generating "main.pdf"
56
+ # in the temporary directory.
57
+ #6. The generated PDF file is copied over to the real output directory
58
+ # and renamed to "Documentation.pdf".
59
+ #7. The temporary directory is recursively deleted.
60
+ #8. A file <tt>index.html</tt> is created inside the output directory
61
+ # that contains a link to the PDF file. This allows navigating to the
62
+ # documentation via RubyGems’ server (if somebody really set his
63
+ # default generator to +pdf_latex+...).
64
+ class RDoc::Generator::Papyrus
65
+
66
+ #Generic exception class for this library.
67
+ class PapyrusError < StandardError
68
+ end
69
+
70
+ RDoc::RDoc.add_generator(self) #Tell RDoc about the new generator
71
+
72
+ #Description displayed in RDoc’s help.
73
+ DESCRIPTION = "PDF generator based on LaTeX"
74
+
75
+ #The version number.
76
+ VERSION = Pathname.new(__FILE__).dirname.parent.parent.parent.join("VERSION.txt").read.chomp.freeze
77
+
78
+ #Directory where the LaTeX template files are stored.
79
+ TEMPLATE_DIR = Pathname.new(__FILE__).dirname.expand_path.join("..", "..", "..", "data")
80
+ #The main file’s ERB template.
81
+ MAIN_TEMPLATE = ERB.new(TEMPLATE_DIR.join("main.tex.erb").read)
82
+ #The ERB template for a single file.
83
+ RDOC_FILE_TEMPLATE = ERB.new(TEMPLATE_DIR.join("rdoc_file.tex.erb").read)
84
+ #The ERB template for a single class or module.
85
+ MODULE_TEMPLATE = ERB.new(TEMPLATE_DIR.join("module.tex.erb").read)
86
+
87
+ #Basename of the main resulting LaTeX file. The path is prepended
88
+ #later as it’s a temporary directory.
89
+ MAIN_FILE_BASENAME = "main.tex"
90
+ #Basename of the resulting documentation file inside the
91
+ #temporary directory.
92
+ MAIN_FILE_RESULT_BASENAME = "main.pdf"
93
+
94
+ #Creates a new instance of this class. Automatically called by RDoc.
95
+ #There shouldn’t be any need for you to call this.
96
+ #==Parameter
97
+ #[options] RDoc passes the current RDoc::Options instance here.
98
+ #==Return value
99
+ #The newly created instance.
100
+ def initialize(options)
101
+ #The requiring of the rest of the library *must* be placed here,
102
+ #because otherwise it’s loaded during RDoc’s discovering process,
103
+ #effectively eliminating the possibility to generate anything
104
+ #other than LaTeX output due to the overwrites the
105
+ #RDoc::Generator::LaTeX_Markup module does.
106
+ require_relative "../markup/to_latex_crossref"
107
+ require_relative "latex_markup"
108
+
109
+ @options = options
110
+ @output_dir = Pathname.pwd.expand_path + @options.op_dir
111
+ #The following variable is used to generate unique filenames.
112
+ #During processing the ERB templates, many files are created and
113
+ #accidentally creating two files with the same name, effectively
114
+ #overwriting the previous one, should be avoided. Hence, this
115
+ #little number is prepended to generated filenames (except the
116
+ #main file).
117
+ @counter = 0
118
+ end
119
+
120
+ class << self
121
+
122
+ #Called by RDoc during option processing. Adds commandline
123
+ #switches specific to this generator.
124
+ #==Parameter
125
+ #[options] The yet unparsed RDoc::Options.
126
+ def setup_options(options)
127
+ debug("Teaching new options to RDoc")
128
+ #Define the methods to get and set the options
129
+ options.extend(RDoc::Generator::Papyrus::Options)
130
+
131
+ #Define the options themselves
132
+ options.option_parser.on("--[no-]show-pages", "(pdf_latex) Enables or disables page", "numbers following hyperlinks (default true).") do |val|
133
+ debug("Found --show-pages: #{val}")
134
+ options.show_pages = val
135
+ end
136
+ options.option_parser.on("--latex-command=VALUE", "(pdf_latex) Sets the command to run", "LaTeX (defaults to '#{RDoc::Generator::Papyrus::Options::DEFAULT_LATEX_COMMAND}')") do |val|
137
+ debug("Found --latex-command: #{val}")
138
+ options.latex_command = val
139
+ end
140
+ options.option_parser.on("--babel-lang=VALUE", "(pdf_latex) Sets the language option", "for babel (defaults to '#{RDoc::Generator::Papyrus::Options::DEFAULT_BABEL_LANG}')") do |val|
141
+ debug("Found --babel-lang: #{val}")
142
+ options.babel_lang = val
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ #If RDoc is invoked in debug mode, writes out +str+ using
149
+ #+puts+ (prepending "[pdf_latex] ") and calls it’s block
150
+ #if one was given. If RDoc isn’t invoked in debug mode,
151
+ #does nothing.
152
+ def debug(str = nil)
153
+ if $DEBUG_RDOC
154
+ puts "[papyrus] #{str}" if str
155
+ yield if block_given?
156
+ end
157
+ end
158
+
159
+ end
160
+
161
+ #Called by RDoc after parsing has happened in order to generate the output.
162
+ #This method takes the input of RDoc::TopLevel objects and tranforms
163
+ #them by means of the RDoc::Markup::ToLaTeX_Crossref class into LaTeX
164
+ #markup.
165
+ def generate(top_levels)
166
+ #Prepare all the data needed by all the templates
167
+ doc_title = @options.title
168
+ babel_lang = @options.babel_lang
169
+
170
+ #Get the rdoc file list and move the "main page file" to the beginning.
171
+ debug("Examining toplevel files")
172
+ @rdoc_files = top_levels.select{|t| t.name =~ /\.rdoc$/i}
173
+ debug("Found #{@rdoc_files.count} toplevels ending in .rdoc that will be processed")
174
+ if @options.main_page #nil if not set, no main page
175
+ main_index = @rdoc_files.index{|t| t.full_name == @options.main_page}
176
+ @rdoc_files.unshift(@rdoc_files.slice!(main_index))
177
+ debug("Main page is #{@rdoc_files.first.name}")
178
+ end
179
+
180
+ #Get the class, module and methods lists, sorted alphabetically by their full names
181
+ debug("Sorting classes, modules and methods")
182
+ @classes = RDoc::TopLevel.all_classes.sort_by{|klass| klass.full_name}
183
+ @modules = RDoc::TopLevel.all_modules.sort_by{|mod| mod.full_name}
184
+ @classes_and_modules = @classes.concat(@modules).sort_by{|mod| mod.full_name}
185
+ @methods = @classes_and_modules.map{|mod| mod.method_list}.flatten.sort
186
+
187
+ #Start the template filling process
188
+ if @options.dry_run
189
+ temp_dir = Pathname.pwd #No output directory in dryrun mode!
190
+ else
191
+ temp_dir = @output_dir + "tmp"
192
+ temp_dir.mkpath
193
+ end
194
+ debug("Temporary directory is at '#{temp_dir.expand_path}'")
195
+ Dir.chdir(temp_dir) do #We want LaTeX to output it’s files into our temporary directory
196
+ #Evaluate the main page which includes all
197
+ #subpages as necessary.
198
+ debug("Evaluating main ERB temlpate")
199
+ main_file = temp_dir + MAIN_FILE_BASENAME
200
+ main_file.open("w"){|f| f.write(MAIN_TEMPLATE.result(binding))} unless @options.dry_run
201
+
202
+ #Let LaTeX process the whole thing -- 3 times, to ensure
203
+ #any kinds of references are correct.
204
+ debug("Invoking LaTeX")
205
+ 3.times{latex(main_file)}
206
+
207
+ #Oh, and don’t forget to copy the result file into our documentation
208
+ #directory :-)
209
+ debug("Copying resulting Documentation.pdf file")
210
+ unless @options.dry_run
211
+ FileUtils.rm(@output_dir + "Documentation.pdf") if File.file?(@output_dir + "Documentation.pdf")
212
+ FileUtils.cp(temp_dir + MAIN_FILE_RESULT_BASENAME, @output_dir + "Documentation.pdf")
213
+ end
214
+
215
+ #To allow browsing the documentation with the RubyGems server, put an index.html
216
+ #file there that points to the PDF file.
217
+ debug("Creating index.html")
218
+ unless @options.dry_run
219
+ File.open(@output_dir + "index.html", "w") do |f|
220
+ f.puts("<html>")
221
+ f.puts("<!-- This file exists to allow browsing docs with the Gem server -->")
222
+ f.puts("<head><title>#{doc_title}</title></head>")
223
+ f.puts('<body><p>Documentation available as a <a href="Documentation.pdf">PDF file</a>.</p></body>')
224
+ f.puts("</html>")
225
+ end
226
+ end
227
+ end
228
+
229
+ #Remove the temporary directory (this is *not* done if invoking LaTeX
230
+ #failed, as the #latex method throws an exception. This is useful for
231
+ #debugging the generated LaTeX files)
232
+ debug("Removing temporary directory")
233
+ temp_dir.rmtree unless @options.dry_run
234
+ end
235
+
236
+ private
237
+
238
+ #Invokes the class method ::debug.
239
+ def debug(str = nil, &block)
240
+ self.class.send(:debug, str, &block) #Private class method
241
+ end
242
+
243
+ #Runs the LaTeX command with the specified +opts+, which will be quoted
244
+ #by this method. Raises PapyrusError if the +pdflatex+ command (or
245
+ #the command named on the commandline) wasn't found. This exception
246
+ #is also raised if something goes wrong when calling LaTeX.
247
+ def latex(*opts)
248
+ cmd = "\"#{@options.latex_command}\""
249
+ opts.each{|o| cmd << " \"#{o}\""}
250
+ puts cmd if $DEBUG_RDOC
251
+ unless @options.dry_run
252
+ Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
253
+ stdin.close #We’re always running noninteractive
254
+ if $DEBUG_RDOC
255
+ print stdout.read
256
+ print stderr.read
257
+ end
258
+
259
+ e = thread.value.exitstatus
260
+ unless e == 0
261
+ raise(PapyrusError, "Invoking #{@options.latex_command} failed with exitstatus #{e}!")
262
+ end
263
+ end
264
+ end
265
+ rescue Errno::ENOENT => e
266
+ raise(PapyrusError, "LaTeX not found -- original error was: #{e.class} -- #{e.message}")
267
+ end
268
+
269
+ #Renders the given RDoc::ClassModule into a TeX file and returns the
270
+ #relative path (relative to the temporary directory) to the file.
271
+ #Suitable for use with \input in the main template.
272
+ def render_module(mod)
273
+ debug("Rendering module ERB template for #{mod.name}")
274
+ filename = "#{@counter}_#{mod.name}.tex"; @counter += 1
275
+ File.open(filename, "w"){|f| f.write(MODULE_TEMPLATE.result(binding))}
276
+ filename
277
+ end
278
+
279
+ #Renders the given RDoc::TopLevel into a TeX file and returns the
280
+ #relative path (relative to the temporary directory) to the file.
281
+ #Suitable for use with \input in the main template.
282
+ def render_rdoc_file(rdoc_file)
283
+ debug("Rendering file ERB template for #{rdoc_file.name}")
284
+ filename = "#{@counter}_#{rdoc_file.name}.tex"; @counter += 1
285
+ File.open(filename, "w"){|f| f.write(RDOC_FILE_TEMPLATE.result(binding))}
286
+ filename
287
+ end
288
+
289
+ #Generates a \hyperref with the given arguments. +show_page+ may be
290
+ #one of three values:
291
+ #[true] Force page numbers in brackets following the hyperlink.
292
+ #[false] Suppress page numbers in any case.
293
+ #[nil] Use the value of the commandline options --show-pages, which
294
+ # defaults to true if not given.
295
+ #The generated hyperlink will be of the following form:
296
+ # \hyperref[<label>]{<name>} [p. page if requested]
297
+ def hyperref(label, name, show_page = nil)
298
+ if show_page.nil? and @options.show_pages
299
+ "\\hyperref[#{label}]{#{name}} \\nolinebreak[2][p.~\\pageref{#{label}}]"
300
+ elsif show_page.nil? and !@options.show_pages
301
+ "\\hyperref[#{label}]{#{name}}"
302
+ elsif show_page
303
+ "\\hyperref[#{label}]{#{name}} \\nolinebreak[2][p.~\\pageref{#{label}}]"
304
+ else
305
+ "\\hyperref[#{label}]{#{name}}"
306
+ end
307
+ end
308
+
309
+ #Generates a \pageref with the given +label+.
310
+ def pageref(label)
311
+ "\\pageref{#{label}}"
312
+ end
313
+
314
+ #Shortcut for calling #hyperref with <tt>meth.latex_label</tt>,
315
+ #<tt>meth.latexized(:pretty_name)</tt> and +show_page+.
316
+ def hyperref_method(meth, show_page = false)
317
+ hyperref(meth.latex_label, meth.latexized(:pretty_name), show_page)
318
+ end
319
+
320
+ #Takes either a string or a RDoc::CodeObject and returns
321
+ #a \hyperref to it if possible. Otherwise just returns +obj+.
322
+ def superclass_string(obj)
323
+ if obj.kind_of?(String)
324
+ obj
325
+ else
326
+ hyperref(obj.latex_label, obj.latexized(:full_name))
327
+ end
328
+ end
329
+
330
+ #Takes a list of RDoc::MethodAttr objects and turns them into a sorted
331
+ #LaTeX table with hyperlinks and page references.
332
+ def generate_method_table(methods)
333
+ table_str = ""
334
+ table_str << "\\small"
335
+ table_str << "\\begin{longtable}{l|l|l|l|l|l}\n"
336
+ table_str << " \\bfseries Name & \\bfseries p & \\bfseries Name & \\bfseries p & \\bfseries Name & \\bfseries p \\\\\n"
337
+ table_str << " \\hline\n"
338
+ table_str << "\\endhead\n"
339
+ methods.sort.each_slice(3) do |meth1, meth2, meth3|
340
+ table_str << hyperref_method(meth1, false) << " & " << pageref(meth1.latex_label) << " &\n"
341
+
342
+ if meth2
343
+ table_str << hyperref_method(meth2, false) << " & " << pageref(meth2.latex_label) << " &\n"
344
+ else
345
+ table_str << "&&\n"
346
+ end
347
+
348
+ if meth3
349
+ table_str << hyperref_method(meth3, false) << " & " << pageref(meth3.latex_label) << " \\\\\n"
350
+ else
351
+ table_str << "&\\\\\n"
352
+ end
353
+ end
354
+
355
+ table_str << "\n\\end{longtable}\n"
356
+ table_str << "\\normalsize\n"
357
+
358
+ table_str
359
+ end
360
+
361
+ #Generates the method overview table after the TOC for +methods+, which should
362
+ #be all methods of all classes and modules.
363
+ def generate_method_toc_table
364
+ table_str = ""
365
+ table_str << "\\small"
366
+ table_str << "\\begin{longtable}{l|l}\n"
367
+ table_str << " \\bfseries Name & \\bfseries p \\\\\n"
368
+ table_str << " \\hline\n"
369
+ table_str << "\\endhead\n"
370
+ @methods.each do |meth|
371
+ table_str << hyperref_method(meth, false) << " ("
372
+ table_str << hyperref(meth.parent.latex_label, meth.parent.latexized(:full_name), false) << ")"
373
+ table_str << " & " << pageref(meth.latex_label) << " \\\\\n"
374
+ end
375
+
376
+ table_str << "\n\\end{longtable}\n"
377
+ table_str << "\\normalsize\n"
378
+
379
+ table_str
380
+ end
381
+
382
+ end
@@ -0,0 +1,87 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of Papyrus.
4
+ #
5
+ # Papyrus is a RDoc plugin for generating PDF files.
6
+ # Copyright © 2011 Pegasus Alpha
7
+ #
8
+ # Papyrus is free software; you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation; either version 2 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # Papyrus is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with Papyrus; if not, write to the Free Software
20
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
+
22
+ #
23
+ module RDoc
24
+
25
+ module Generator
26
+
27
+ class Papyrus
28
+
29
+ #Mixin module that is used to extend the RDoc::Options
30
+ #instance given to RDoc::Generator::Papyrus.setup_options
31
+ #to arrange for some new options specific to the PDF
32
+ #generator.
33
+ module Options
34
+
35
+ #The default command invoked when running LaTeX.
36
+ #Overriden by the <tt>--latex-command</tt> commandline
37
+ #option.
38
+ DEFAULT_LATEX_COMMAND = "pdflatex"
39
+
40
+ #The default language option passed to the LaTeX +babel+
41
+ #package.
42
+ DEFAULT_BABEL_LANG = "english"
43
+
44
+ #Wheather or not to show page numbers in square
45
+ #brackets behind any cross-reference made. Useful
46
+ #if one is sure that the documentation won’t be printed
47
+ #and therefore doesn’t need the annoying page numbers.
48
+ def show_pages
49
+ @show_pages ||= true
50
+ end
51
+
52
+ #Setter for #show_pages value.
53
+ def show_pages=(val)
54
+ @show_pages = !!val
55
+ end
56
+
57
+ #The command to run LaTeX, defaults to the
58
+ #value of DEFAULT_LATEX_COMMAND, which in turn
59
+ #is "pdflatex".
60
+ def latex_command
61
+ @latex_command ||= DEFAULT_LATEX_COMMAND
62
+ end
63
+
64
+ #Setter for the #latex_command value.
65
+ def latex_command=(val)
66
+ @latex_command = val
67
+ end
68
+
69
+ #The language option to be passed to the +babel+ package.
70
+ #Note that changing this value doesn’t translate hard-coded
71
+ #strings like "Public class methods" yet.
72
+ def babel_lang
73
+ @babel_lang ||= DEFAULT_BABEL_LANG
74
+ end
75
+
76
+ #Setter for the #babel_lang value.
77
+ def babel_lang=(val)
78
+ @babel_lang = val
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+
87
+ end