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 &lt;; > must be written as &gt;; and & must be written as &amp;.
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 (<b>) and italic (<i>) 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/>).
<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.
<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 <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
Public Class Methods
# 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
# 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
# File lib/pdf/techbook.rb 438 def __heading2(heading) 439 heading 440 end
# File lib/pdf/techbook.rb 441 def __heading3(heading) 442 "<b>#{heading}</b>" 443 end
# File lib/pdf/techbook.rb 444 def __heading4(heading) 445 "<i>#{heading}</i>" 446 end
# File lib/pdf/techbook.rb 447 def __heading5(heading) 448 "<c:uline>#{heading}</c:uline>" 449 end
# File lib/pdf/techbook.rb 787 def generate_table_of_contents? 788 @gen_toc 789 end
# 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
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
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
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
# 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
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
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
End preserved newlines: .endpre
# File lib/pdf/techbook.rb 656 def techbook_directive_endpre(args) 657 @techbook_mode = :normal 658 end
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
# File lib/pdf/techbook.rb 758 def techbook_directive_keywords(args) 759 info.keywords = args 760 end
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
Preserved newlines: .pre
# File lib/pdf/techbook.rb 650 def techbook_directive_pre(args) 651 __render_paragraph 652 @techbook_mode = :preserved 653 end
# File lib/pdf/techbook.rb 754 def techbook_directive_subject(args) 755 info.subject = args 756 end
# File lib/pdf/techbook.rb 750 def techbook_directive_title(args) 751 info.title = args 752 end
# File lib/pdf/techbook.rb 741 def techbook_directive_toc(args) 742 @toc_title = args unless args.empty? 743 @gen_toc = true 744 end
# 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
# 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
# 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
# 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
# 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
# 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
# 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