Class | PDF::TechBook |
In: |
lib/pdf/techbook.rb
|
Parent: | PDF::Writer |
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 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 begin with the hash-mark (’#’) at the beginning of the line. Comment lines are ignored.
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 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.
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
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
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
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
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
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
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.
Sets values in the PDF information object. The arguments — to the end of the line — are used to populate the values.
Stops the processing of the document at this point.
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>
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.
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.
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).
H1_STYLE | = | { :background => Color::RGB::Black, :foreground => Color::RGB::White, :justification => :center, :font_size => 26, :bar => true |
H2_STYLE | = | { :background => Color::RGB::Grey80, :foreground => Color::RGB::Black, :justification => :left, :font_size => 18, :bar => true |
H3_STYLE | = | { :background => Color::RGB::White, :foreground => Color::RGB::Black, :justification => :left, :font_size => 18, :bar => false |
H4_STYLE | = | { :background => Color::RGB::White, :foreground => Color::RGB::Black, :justification => :left, :font_size => 14, :bar => false |
H5_STYLE | = | { :background => Color::RGB::White, :foreground => Color::RGB::Black, :justification => :left, :font_size => 12, :bar => false |
LIST_ITEM_STYLES | = | %w(bullet disc) |
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] |
# File lib/pdf/techbook.rb, line 793 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: pdf = File.open(files[:cache], "rb") { |cf| Marshal.load(cf.read) } 858: pdf.save_as(files[:pdf]) 859: File.open(files[:pdf], "wb") { |pf| pf.write pdf.render } 860: exit(0) 861: else 862: $stderr.puts PDF::Writer::Lang[:techbook_regenerating] 863: end 864: end 865: else 866: $stderr.puts PDF::Writer::Lang[:techbook_ignoring_cache] if File.exist?(files[:cache]) 867: end 868: 869: # Create the manual object. 870: pdf = PDF::TechBook.new 871: pdf.compressed = config.compressed 872: pdf.techbook_source_dir = File.expand_path(dirn) 873: 874: document = open(files[:document]) { |io| io.read.split($/) } 875: progress = ProgressBar.new(base.capitalize, document.size) 876: pdf.techbook_parse(document, progress) 877: progress.finish 878: 879: if pdf.generate_table_of_contents? 880: progress = ProgressBar.new("TOC", pdf.table_of_contents.size) 881: pdf.techbook_toc(progress) 882: progress.finish 883: end 884: 885: if config.cache 886: File.open(files[:cache], "wb") { |f| f.write Marshal.dump(pdf) } 887: end 888: 889: pdf.save_as(files[:pdf]) 890: end
# File lib/pdf/techbook.rb, line 433 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, line 447 447: def __heading5(heading) 448: "<c:uline>#{heading}</c:uline>" 449: end
# File lib/pdf/techbook.rb, line 746 746: def techbook_directive_author(args) 747: info.author = args 748: end
# File lib/pdf/techbook.rb, line 764 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, line 661 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, line 719 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, line 709 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, line 782 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, line 670 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, line 684 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, line 656 656: def techbook_directive_endpre(args) 657: @techbook_mode = :normal 658: end
Eval: .eval
# File lib/pdf/techbook.rb, line 678 678: def techbook_directive_eval(args) 679: __render_paragraph 680: @techbook_lastmode, @techbook_mode = @techbook_mode, :eval 681: end
# File lib/pdf/techbook.rb, line 758 758: def techbook_directive_keywords(args) 759: info.keywords = args 760: end
Start a new page: .newpage
# File lib/pdf/techbook.rb, line 639 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, line 650 650: def techbook_directive_pre(args) 651: __render_paragraph 652: @techbook_mode = :preserved 653: end
# File lib/pdf/techbook.rb, line 754 754: def techbook_directive_subject(args) 755: info.subject = args 756: end
# File lib/pdf/techbook.rb, line 750 750: def techbook_directive_title(args) 751: info.title = args 752: end
# File lib/pdf/techbook.rb, line 741 741: def techbook_directive_toc(args) 742: @toc_title = args unless args.empty? 743: @gen_toc = true 744: end
# File lib/pdf/techbook.rb, line 517 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 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, line 892 892: def techbook_text(line) 893: opt = @techbook_textopt.dup 894: opt[:font_size] = @techbook_fontsize 895: text(line, opt) 896: end
# File lib/pdf/techbook.rb, line 589 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
# File lib/pdf/techbook.rb, line 355 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, line 376 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, line 386 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, line 453 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