class PDF::TechBook

PDF::TechBook

The TechBook class is a markup language interpreter. This will read a file containing the “TechBook” markukp, described below, and create a PDF document from it. This is intended as a complete document language, but it does have a number of limitations.

The TechBook markup language and class are used to format the PDF::Writer manual, represented in the distrubtion by the file “manual.pwd”.

The TechBook markup language is primarily stream-oriented with awareness of lines. That is to say that the document will be read and generated from beginning to end in the order of the markup stream.

TechBook Markup

TechBook markup is relatively simple. The simplest markup is no markup at all (flowed paragraphs). This means that two lines separated by a single line separator will be treaed as part of the same paragraph and formatted appropriately by PDF::Writer. Paragaphs are terminated by empty lines, valid line markup directives, or valid headings.

Certain XML entitites will need to be escaped as they would in normal XML usage, that is, < must be written as <; > must be written as >; and & must be written as &.

Comments, headings, and directives are line-oriented where the first mandatory character is in the first column of the document and take up the whole line. Styling and callback tags may appear anywhere in the text.

Comments

Comments begin with the hash-mark (‘#’) at the beginning of the line. Comment lines are ignored.

Styling and Callback Tags

Within normal, preserved, or code text, or in headings, HTML-like markup may be used for bold (&lt;b&gt;) and italic (&lt;i&gt;) text. TechBook supports standard PDF::Writer callback tags (<c:alink>, <c:ilink>, <C:bullet/>, and <C:disc/>) and adds two new ones (<r:xref/>, <C:tocdots/>).

&lt;r:xref/>

Creates an internal document link to the named cross-reference destination. Works with the heading format (see below). See tag_xref_replace for more information.

&lt;C:tocdots/>

This is used internally to create and display a row of dots between a table of contents entry and the page number to which it refers. This is used internally by TechBook.

Directives

Directives begin with a period (‘.’) and are followed by a letter (‘a’..‘z’) and then any combination of word characters (‘a’..‘z’, ‘0’..‘9’, and ‘_’). Directives are case-insensitive. A directive may have arguments; if there are arguments, they must follow the directive name after whitespace. After the arguments for a directive, if any, all other text is ignored and may be considered a comment.

.newpage [force]

The .newpage directive starts a new page. If multicolumn mode is on, a new column will be started if the current column is not the last column. If the optional argument force follows the .newpage directive, a new page will be started even if multicolumn mode is on.

.newpage
.newpage force

.pre, .endpre

The .pre and .endpre directives enclose a block of text with preserved newlines. This is similar to normal text, but the lines in the .pre block are not flowed together. This is useful for poetic forms or other text that must end when each line ends. .pre blocks may not be nested in any other formatting block. When an .endpre directive is encountered, the text format will be returned to normal (flowed text) mode.

.pre
The Way that can be told of is not the eternal Way;
The name that can be named is not the eternal name.
The Nameless is the origin of Heaven and Earth;
The Named is the mother of all things.
Therefore let there always be non-being,
  so we may see their subtlety,
And let there always be being,
  so we may see their outcome.
The two are the same,
But after they are produced,
  they have different names.
.endpre

.code, .endcode

The .code and .endcode directives enclose a block of text with preserved newlines. In addition, the font is changed from the normal techbook_textfont to techbook_codefont. The techbook_codefont is normally a fixed pitched font and defaults to Courier. At the end of the code block, the text state is restored to its prior state, which will either be .pre or normal.

.code
require 'pdf/writer'
PDF::Writer.prepress # US Letter, portrait, 1.3, prepress
.endcode

.blist, .endblist

These directives enclose a bulleted list block. Lists may be nested within other text states. If lists are nested, each list will be appropriately indented. Each line in the list block will be treated as a single list item with a bullet inserted in front using either the <C:bullet/> or <C:disc/> callbacks. Nested lists are successively indented. .blist directives accept one optional argument, the name of the type of bullet callback desired (e.g., ‘bullet’ for <C:bullet/> and ‘disc’ for <C:disc/>).

.blist
Item 1
.blist disc
Item 1.1
.endblist
.endblist

.eval, .endeval

With these directives, the block enclosed will collected and passed to Ruby’s Kernel#eval. .eval blocks may be present within normal text, .pre, .code, and .blist blocks. No other block may be embedded within an .eval block.

.eval
puts "Hello"
.endeval

.columns

Multi-column output is controlled with this directive, which accepts one or two parameters. The first parameter is mandatory and is either the number of columns (2 or more) or the word ‘off’ (turning off multi-column output). When starting multi-column output, a second parameter with the gutter size may be specified.

.columns 3
Column 1
.newpage
Column 2
.newpage
Column 3
.columns off

.toc

This directive is used to tell TechBook to generate a table of contents after the first page (assumed to be a title page). If this is not present, then a table of contents will not be generated.

.author, .title, .subject, .keywords

Sets values in the PDF information object. The arguments – to the end of the line – are used to populate the values.

.done

Stops the processing of the document at this point.

Headings

Headings begin with a number followed by the rest of the heading format. This format is “#<heading-text>” or “#<heading-text>xref_name”. TechBook supports five levels of headings. Headings may include markup, but should not exceed a single line in size; those headings which have boxes as part of their layout are not currently configured to work with multiple lines of heading output. If an xref_name is specified, then the &lt;r:xref> tag can use this name to find the target for the heading. If xref_name is not specified, then the “name” associated with the heading is the index of the order of insertion. The xref_name is case sensitive.

1<Chapter>xChapter
2<Section>Section23
3<Subsection>
4<Subsection>
5<Subsection>

Heading Level 1

First level headings are generally chapters. As such, the standard implementation of the heading level 1 method (#__heading1), will be rendered as “chapter#. heading-text” in centered white on a black background, at 26 point (H1_STYLE). First level headings are added to the table of contents.

Heading Level 2

Second level headings are major sections in chapters. The headings are rendered by default as black on 80% grey, left-justified at 18 point (H2_STYLE). The text is unchanged (#__heading2). Second level headings are added to the table of contents.

Heading Level 3, 4, and 5

The next three heading levels are used for varying sections within second level chapter sections. They are rendered by default in black on the background (there is no bar) at 18, 14, and 12 points, respectively (H3_STYLE, H4_STYLE, and H5_STYLE). Third level headings are bold-faced (#__heading3); fourth level headings are italicised (#__heading4), and fifth level headings are underlined (#__heading5).

Constants

H1_STYLE
H2_STYLE
H3_STYLE
H4_STYLE
H5_STYLE
LIST_ITEM_STYLES

Attributes

chapter_number[RW]
table_of_contents[RW]
techbook_codefont[RW]
techbook_encoding[RW]
techbook_fontsize[RW]
techbook_source_dir[RW]
techbook_textfont[RW]
xref_table[R]

Public Class Methods

run(args) click to toggle source
    # File lib/pdf/techbook.rb
793 def self.run(args)
794   config = OpenStruct.new
795   config.regen      = false
796   config.cache      = true
797   config.compressed = false
798 
799   opts = OptionParser.new do |opt|
800     opt.banner    = PDF::Writer::Lang[:techbook_usage_banner] % [ File.basename($0) ]
801     PDF::Writer::Lang[:techbook_usage_banner_1].each do |ll|
802       opt.separator "  #{ll}"
803     end
804     opt.on('-f', '--force-regen', *PDF::Writer::Lang[:techbook_help_force_regen]) { config.regen = true }
805     opt.on('-n', '--no-cache', *PDF::Writer::Lang[:techbook_help_no_cache]) { config.cache = false }
806     opt.on('-z', '--compress', *PDF::Writer::Lang[:techbook_help_compress]) { config.compressed = true }
807     opt.on_tail ""
808     opt.on_tail("--help", *PDF::Writer::Lang[:techbook_help_help]) { $stderr << opt; exit(0) }
809   end
810   opts.parse!(args)
811 
812   config.document = args[0]
813 
814   unless config.document
815     config.document = "manual.pwd"
816     unless File.exist?(config.document)
817       dirn = File.dirname(__FILE__)
818       config.document = File.join(dirn, File.basename(config.document))
819       unless File.exist?(config.document)
820         dirn = File.join(dirn, "..")
821         config.document = File.join(dirn, File.basename(config.document))
822         unless File.exist?(config.document)
823           dirn = File.join(dirn, "..")
824           config.document = File.join(dirn,
825                                       File.basename(config.document))
826           unless File.exist?(config.document)
827             $stderr.puts PDF::Writer::Lang[:techbook_cannot_find_document]
828             exit(1)
829           end
830         end
831       end
832     end
833 
834     $stderr.puts PDF::Writer::Lang[:techbook_using_default_doc] % config.document
835   end
836 
837   dirn = File.dirname(config.document)
838   extn = File.extname(config.document)
839   base = File.basename(config.document, extn)
840 
841   files = {
842     :document => config.document,
843     :cache    => "#{base}._mc",
844     :pdf      => "#{base}.pdf"
845   }
846 
847   unless config.regen
848     if File.exist?(files[:cache])
849       _tm_doc = File.mtime(config.document)
850       _tm_prg = File.mtime(__FILE__)
851       _tm_cch = File.mtime(files[:cache])
852       
853         # If the cached file is newer than either the document or the
854         # class program, then regenerate.
855       if (_tm_doc < _tm_cch) and (_tm_prg < _tm_cch)
856         $stderr.puts PDF::Writer::Lang[:techbook_using_cached_doc] % File.basename(files[:cache])
857         if RUBY_VERSION >= '1.9'
858           pdf = File.open(files[:cache], "rb:binary") { |cf| Marshal.load(cf.read) }
859         else
860           pdf = File.open(files[:cache], "rb") { |cf| Marshal.load(cf.read) }
861         end
862         pdf.save_as(files[:pdf])
863         File.open(files[:pdf], "wb") { |pf| pf.write pdf.render }
864         exit(0)
865       else
866         $stderr.puts PDF::Writer::Lang[:techbook_regenerating]
867       end
868     end
869   else
870     $stderr.puts PDF::Writer::Lang[:techbook_ignoring_cache] if File.exist?(files[:cache])
871   end
872 
873     # Create the manual object.
874   pdf = PDF::TechBook.new
875   pdf.compressed = config.compressed
876   pdf.techbook_source_dir = File.expand_path(dirn)
877 
878   document = open(files[:document]) { |io| io.read.split($/) }
879   progress = ProgressBar.new(base.capitalize, document.size)
880   pdf.techbook_parse(document, progress)
881   progress.finish
882 
883   if pdf.generate_table_of_contents?
884     progress = ProgressBar.new("TOC", pdf.table_of_contents.size)
885     pdf.techbook_toc(progress)
886     progress.finish
887   end
888 
889   if config.cache
890     File.open(files[:cache], "wb") { |f| f.write Marshal.dump(pdf) }
891   end
892 
893   pdf.save_as(files[:pdf])
894 end

Public Instance Methods

__heading1(heading) click to toggle source
    # File lib/pdf/techbook.rb
433 def __heading1(heading)
434   @chapter_number ||= 0
435   @chapter_number = @chapter_number.succ
436   "#{chapter_number}. #{heading}"
437 end
__heading2(heading) click to toggle source
    # File lib/pdf/techbook.rb
438 def __heading2(heading)
439   heading
440 end
__heading3(heading) click to toggle source
    # File lib/pdf/techbook.rb
441 def __heading3(heading)
442   "<b>#{heading}</b>"
443 end
__heading4(heading) click to toggle source
    # File lib/pdf/techbook.rb
444 def __heading4(heading)
445   "<i>#{heading}</i>"
446 end
__heading5(heading) click to toggle source
    # File lib/pdf/techbook.rb
447 def __heading5(heading)
448   "<c:uline>#{heading}</c:uline>"
449 end
generate_table_of_contents?() click to toggle source
    # File lib/pdf/techbook.rb
787 def generate_table_of_contents?
788   @gen_toc
789 end
techbook_directive_author(args) click to toggle source
    # File lib/pdf/techbook.rb
746 def techbook_directive_author(args)
747   info.author = args
748 end
techbook_directive_blist(args) click to toggle source
    # File lib/pdf/techbook.rb
764 def techbook_directive_blist(args)
765   __render_paragraph
766   sm = /^(\w+).*$/o.match(args)
767   style = sm.captures[0] if sm
768   style = "bullet" unless LIST_ITEM_STYLES.include?(style)
769 
770   @blist_factor = @left_margin * 0.10 if @blist_info.empty?
771 
772   info = {
773     :left_margin  => @left_margin,
774     :style        => style
775   }
776   @blist_info << info
777   @left_margin += @blist_factor
778 
779   @techbook_lastmode, @techbook_mode = @techbook_mode, :blist if :blist != @techbook_mode
780 end
techbook_directive_code(args) click to toggle source

Code: .code

    # File lib/pdf/techbook.rb
661 def techbook_directive_code(args)
662   __render_paragraph
663   select_font @techbook_codefont, @techbook_encoding
664   @techbook_lastmode, @techbook_mode = @techbook_mode, :code
665   @techbook_textopt  = { :justification => :left, :left => 20, :right => 20 }
666   @techbook_fontsize = 10
667 end
techbook_directive_columns(args) click to toggle source

Columns. .columns <number-of-columns>|off

    # File lib/pdf/techbook.rb
719 def techbook_directive_columns(args)
720   av = /^(\d+|off)(?: (\d+))?(?: .*)?$/o.match(args)
721   unless av
722     $stderr.puts PDF::Writer::Lang[:techbook_bad_columns_directive] % args
723     raise ArgumentError
724   end
725   cols = av.captures[0]
726 
727     # Flush the paragraph cache.
728   __render_paragraph
729 
730   if cols == "off" or cols.to_i < 2
731     stop_columns
732   else
733     if av.captures[1]
734       start_columns(cols.to_i, av.captures[1].to_i)
735     else
736       start_columns(cols.to_i)
737     end
738   end
739 end
techbook_directive_done(args) click to toggle source

Done. Stop parsing: .done

    # File lib/pdf/techbook.rb
709 def techbook_directive_done(args)
710   unless @techbook_code.empty?
711     $stderr.puts PDF::Writer::Lang[:techbook_code_not_empty]
712     $stderr.puts @techbook_code
713   end
714   __render_paragraph
715   :break
716 end
techbook_directive_endblist(args) click to toggle source
    # File lib/pdf/techbook.rb
782 def techbook_directive_endblist(args)
783   self.left_margin = @blist_info.pop[:left_margin]
784   @techbook_lastmode, @techbook_mode = @techbook_mode, @techbook_lastmode if @blist_info.empty?
785 end
techbook_directive_endcode(args) click to toggle source

End Code: .endcode

    # File lib/pdf/techbook.rb
670 def techbook_directive_endcode(args)
671   select_font @techbook_textfont, @techbook_encoding
672   @techbook_lastmode, @techbook_mode = @techbook_mode, @techbook_lastmode
673   @techbook_textopt  = { :justification => :full }
674   @techbook_fontsize = 12
675 end
techbook_directive_endeval(args) click to toggle source

End Eval: .endeval

    # File lib/pdf/techbook.rb
684 def techbook_directive_endeval(args)
685   save_state
686 
687   thread = Thread.new do
688     begin
689       @techbook_code.untaint
690       pdf = self
691       eval @techbook_code
692     rescue Exception => ex
693       err = PDF::Writer::Lang[:techbook_eval_exception]
694       $stderr.puts err % [ @techbook_line__, ex, ex.backtrace.join("\n") ]
695       raise ex
696     end
697   end
698   thread.abort_on_exception = true
699   thread.join
700 
701   restore_state
702   select_font @techbook_textfont, @techbook_encoding
703 
704   @techbook_code = ""
705   @techbook_mode, @techbook_lastmode = @techbook_lastmode, @techbook_mode
706 end
techbook_directive_endpre(args) click to toggle source

End preserved newlines: .endpre

    # File lib/pdf/techbook.rb
656 def techbook_directive_endpre(args)
657   @techbook_mode = :normal
658 end
techbook_directive_eval(args) click to toggle source

Eval: .eval

    # File lib/pdf/techbook.rb
678 def techbook_directive_eval(args)
679   __render_paragraph
680   @techbook_lastmode, @techbook_mode = @techbook_mode, :eval
681 end
techbook_directive_keywords(args) click to toggle source
    # File lib/pdf/techbook.rb
758 def techbook_directive_keywords(args)
759   info.keywords = args
760 end
techbook_directive_newpage(args) click to toggle source

Start a new page: .newpage

    # File lib/pdf/techbook.rb
639 def techbook_directive_newpage(args)
640   __render_paragraph
641 
642   if args =~ /^force/
643     start_new_page true
644   else
645     start_new_page
646   end
647 end
techbook_directive_pre(args) click to toggle source

Preserved newlines: .pre

    # File lib/pdf/techbook.rb
650 def techbook_directive_pre(args)
651   __render_paragraph
652   @techbook_mode = :preserved
653 end
techbook_directive_subject(args) click to toggle source
    # File lib/pdf/techbook.rb
754 def techbook_directive_subject(args)
755   info.subject  = args
756 end
techbook_directive_title(args) click to toggle source
    # File lib/pdf/techbook.rb
750 def techbook_directive_title(args)
751   info.title  = args
752 end
techbook_directive_toc(args) click to toggle source
    # File lib/pdf/techbook.rb
741 def techbook_directive_toc(args)
742   @toc_title  = args unless args.empty?
743   @gen_toc    = true
744 end
techbook_parse(document, progress = nil) click to toggle source
    # File lib/pdf/techbook.rb
517 def techbook_parse(document, progress = nil)
518   @table_of_contents = []
519 
520   @toc_title          = "Table of Contents"
521   @gen_toc            = false
522   @techbook_code      = ""
523   @techbook_para      = ""
524   @techbook_fontsize  = 12
525   @techbook_textopt   = { :justification => :full }
526   @techbook_lastmode  = @techbook_mode = :normal
527 
528   @techbook_textfont  = "Times-Roman"
529   @techbook_codefont  = "Courier"
530 
531   @blist_info         = []
532 
533   @techbook_line__    = 0
534 
535   __build_xref_table(document)
536 
537   document.each_line do |line|
538   begin
539     progress.inc if progress
540     @techbook_line__ += 1
541 
542     next if line =~ %r{^#}o
543 
544     directive, args = techbook_find_directive(line)
545     if directive
546         # Just try to call the method/directive. It will be far more
547         # common to *find* the method than not to.
548       res = __send__("techbook_directive_#{directive}", args) rescue nil
549       break if :break == res 
550       next
551     end
552 
553     case @techbook_mode
554     when :eval
555       @techbook_code << line << "\n"
556       next
557     when :code
558       techbook_text(line)
559       next
560     when :blist
561       line = "<C:#{@blist_info[-1][:style]}/>#{line}"
562       techbook_text(line)
563       next
564     end
565 
566     next if techbook_heading(line)
567 
568     if :preserved == @techbook_mode
569       techbook_text(line)
570       next
571     end
572 
573     line.chomp!
574 
575     if line.empty?
576       __render_paragraph
577       techbook_text("\n")
578     else
579       @techbook_para << " " unless @techbook_para.empty?
580       @techbook_para << line
581     end
582   rescue Exception => ex
583     $stderr.puts PDF::Writer::Lang[:techbook_exception] % [ ex, @techbook_line ]
584     raise
585   end
586   end
587 end
techbook_text(line) click to toggle source
    # File lib/pdf/techbook.rb
896 def techbook_text(line)
897   opt = @techbook_textopt.dup
898   opt[:font_size] = @techbook_fontsize
899   text(line, opt)
900 end
techbook_toc(progress = nil) click to toggle source
    # File lib/pdf/techbook.rb
589 def techbook_toc(progress = nil)
590   insert_mode :on
591   insert_position :after
592   insert_page 1
593   start_new_page
594 
595   style = H1_STYLE
596   save_state
597 
598   if style[:bar]
599     fill_color    style[:background]
600     fh = font_height(style[:font_size]) * 1.01
601     fd = font_descender(style[:font_size]) * 1.01
602     x = absolute_left_margin
603     w = absolute_right_margin - absolute_left_margin
604     rectangle(x, y - fh + fd, w, fh).fill
605   end
606 
607   fill_color  style[:foreground]
608   text(@toc_title, :font_size => style[:font_size],
609        :justification => style[:justification])
610 
611   restore_state
612 
613   self.y += font_descender(style[:font_size])#* 0.5
614 
615   right = absolute_right_margin
616 
617     # TODO -- implement tocdots as a replace tag and a single drawing tag.
618   @table_of_contents.each do |entry|
619     progress.inc if progress
620 
621     info =  "<c:ilink dest='#{entry[:xref]}'>#{entry[:title]}</c:ilink>"
622     info << "<C:tocdots level='#{entry[:level]}' page='#{entry[:page]}' xref='#{entry[:xref]}'/>"
623 
624     case entry[:level]
625     when 1
626       text info, :font_size => 16, :absolute_right => right
627     when 2
628       text info, :font_size => 12, :left => 50, :absolute_right => right
629     end
630   end
631 end

Private Instance Methods

__build_xref_table(data) click to toggle source
    # File lib/pdf/techbook.rb
355 def __build_xref_table(data)
356   headings = data.grep(HEADING_FORMAT_RE)
357 
358   @xref_table = {}
359 
360   headings.each_with_index do |text, idx|
361     level, label, name = HEADING_FORMAT_RE.match(text).captures
362 
363     xref = "xref#{idx}"
364 
365     name ||= idx.to_s
366     @xref_table[name] = {
367       :title  => __send__("__heading#{level}", label),
368       :page   => nil,
369       :level  => level.to_i,
370       :xref   => xref
371     }
372   end
373 end
__render_paragraph() click to toggle source
    # File lib/pdf/techbook.rb
376 def __render_paragraph
377   unless @techbook_para.empty?
378     techbook_text(@techbook_para.squeeze(" "))
379     @techbook_para.replace ""
380   end
381 end
techbook_find_directive(line) click to toggle source
    # File lib/pdf/techbook.rb
386 def techbook_find_directive(line)
387   directive = nil
388   arguments = nil
389   dmatch = LINE_DIRECTIVE_RE.match(line)
390   if dmatch
391     directive = dmatch.captures[0].downcase.chomp
392     arguments = dmatch.captures[1]
393   end
394   [directive, arguments]
395 end
techbook_heading(line) click to toggle source
    # File lib/pdf/techbook.rb
453 def techbook_heading(line)
454   head = HEADING_FORMAT_RE.match(line)
455   if head
456     __render_paragraph
457 
458     @heading_num ||= -1
459     @heading_num += 1
460 
461     level, heading, name = head.captures
462     level = level.to_i
463 
464     name ||= @heading_num.to_s
465     heading = @xref_table[name]
466 
467     style   = self.class.const_get("H#{level}_STYLE")
468 
469     start_transaction(:heading_level)
470     ok = false
471 
472     loop do # while not ok
473       break if ok
474       this_page = pageset.size
475 
476       save_state
477 
478       if style[:bar]
479         fill_color style[:background]
480         fh = font_height(style[:font_size]) * 1.01
481         fd = font_descender(style[:font_size]) * 1.01
482         x = absolute_left_margin
483         w = absolute_right_margin - absolute_left_margin
484         rectangle(x, y - fh + fd, w, fh).fill
485       end
486 
487       fill_color style[:foreground]
488       text(heading[:title], :font_size => style[:font_size],
489            :justification => style[:justification])
490 
491       restore_state
492 
493       if (pageset.size == this_page)
494         commit_transaction(:heading_level)
495         ok = true
496       else
497           # We have moved onto a new page. This is bad, as the background
498           # colour will be on the old one.
499         rewind_transaction(:heading_level)
500         start_new_page
501       end
502     end
503 
504     heading[:page] = which_page_number(current_page_number)
505 
506     case level
507     when 1, 2
508       @table_of_contents << heading
509     end
510 
511     add_destination(heading[:xref], 'FitH', @y + font_height(style[:font_size]))
512   end
513   head
514 end