prawn-format 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/Manifest +37 -0
  2. data/Rakefile +31 -0
  3. data/examples/basic-formatting.rb +37 -0
  4. data/examples/christmas-carol.txt +717 -0
  5. data/examples/document.rb +61 -0
  6. data/examples/flowing.rb +24 -0
  7. data/examples/style-classes.rb +12 -0
  8. data/examples/syntax-highlighting.rb +31 -0
  9. data/examples/tags.rb +24 -0
  10. data/lib/prawn/format.rb +211 -0
  11. data/lib/prawn/format/effects/link.rb +30 -0
  12. data/lib/prawn/format/effects/underline.rb +32 -0
  13. data/lib/prawn/format/instructions/base.rb +62 -0
  14. data/lib/prawn/format/instructions/tag_close.rb +52 -0
  15. data/lib/prawn/format/instructions/tag_open.rb +95 -0
  16. data/lib/prawn/format/instructions/text.rb +89 -0
  17. data/lib/prawn/format/layout_builder.rb +113 -0
  18. data/lib/prawn/format/lexer.rb +222 -0
  19. data/lib/prawn/format/line.rb +99 -0
  20. data/lib/prawn/format/parser.rb +181 -0
  21. data/lib/prawn/format/state.rb +189 -0
  22. data/lib/prawn/format/text_object.rb +107 -0
  23. data/lib/prawn/format/version.rb +11 -0
  24. data/manual/html.rb +187 -0
  25. data/manual/include/basics.rb +6 -0
  26. data/manual/include/breaks.rb +13 -0
  27. data/manual/include/custom-tags.rb +10 -0
  28. data/manual/include/custom-tags2.rb +2 -0
  29. data/manual/include/indent.rb +4 -0
  30. data/manual/include/options.rb +15 -0
  31. data/manual/include/style-classes.rb +5 -0
  32. data/manual/manual.txt +101 -0
  33. data/manual/pdf.rb +204 -0
  34. data/prawn-format.gemspec +45 -0
  35. data/spec/layout_builder_spec.rb +27 -0
  36. data/spec/lexer_spec.rb +91 -0
  37. data/spec/parser_spec.rb +103 -0
  38. data/spec/spec_helper.rb +24 -0
  39. metadata +157 -0
@@ -0,0 +1,99 @@
1
+ # encoding: utf-8
2
+
3
+ module Prawn
4
+ module Format
5
+
6
+ class Line
7
+ attr_reader :source
8
+ attr_reader :instructions
9
+
10
+ def initialize(instructions, hard_break)
11
+ # need to remember the "source" instructions, because lines can
12
+ # pushed back onto the stack en masse when flowing into boxes,
13
+ # if a line is discovered to not fit. Thus, a line must preserve
14
+ # all instructions it was originally given.
15
+
16
+ @source = instructions
17
+ @hard_break = hard_break
18
+ end
19
+
20
+ def instructions
21
+ @instructions ||= begin
22
+ instructions = source.dup
23
+
24
+ # ignore discardable items at the end of lines
25
+ instructions.pop while instructions.any? && instructions.last.discardable?
26
+
27
+ consolidate(instructions)
28
+ end
29
+ end
30
+
31
+ def spaces
32
+ @spaces ||= begin
33
+ spaces = instructions.inject(0) { |sum, instruction| sum + instruction.spaces }
34
+ [1, spaces].max
35
+ end
36
+ end
37
+
38
+ def hard_break?
39
+ @hard_break
40
+ end
41
+
42
+ def width
43
+ instructions.inject(0) { |sum, instruction| sum + instruction.width }
44
+ end
45
+
46
+ # distance from top of line to baseline
47
+ def ascent
48
+ instructions.map { |instruction| instruction.ascent }.max || 0
49
+ end
50
+
51
+ # distance from bottom of line to baseline
52
+ def descent
53
+ instructions.map { |instruction| instruction.descent }.min || 0
54
+ end
55
+
56
+ def height(include_blank=false)
57
+ instructions.map { |instruction| instruction.height(include_blank) }.max
58
+ end
59
+
60
+ def draw_on(document, state, options={})
61
+ return if instructions.empty?
62
+
63
+ format_state = instructions.first.state
64
+
65
+ case(options[:align])
66
+ when :left
67
+ state[:dx] = 0
68
+ when :center
69
+ state[:dx] = (state[:width] - width) / 2.0
70
+ when :right
71
+ state[:dx] = state[:width] - width
72
+ when :justify
73
+ state[:dx] = 0
74
+ state[:padding] = hard_break? ? 0 : (state[:width] - width) / spaces
75
+ state[:text].word_space(state[:padding])
76
+ end
77
+
78
+ state[:dy] -= ascent
79
+
80
+ state[:text].move_to(state[:dx], state[:dy])
81
+ state[:line] = self
82
+
83
+ document.save_font do
84
+ instructions.each { |instruction| instruction.draw(document, state, options) }
85
+ state[:pending_effects].each { |effect| effect.wrap(document, state) }
86
+ end
87
+
88
+ state[:dy] -= (options[:spacing] || 0) + (height - ascent)
89
+ end
90
+
91
+ private
92
+
93
+ def consolidate(list)
94
+ list.inject([]) { |l,i| i.accumulate(l) }
95
+ end
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,181 @@
1
+ # encoding: utf-8
2
+
3
+ require 'prawn/format/instructions/text'
4
+ require 'prawn/format/instructions/tag_open'
5
+ require 'prawn/format/instructions/tag_close'
6
+ require 'prawn/format/lexer'
7
+ require 'prawn/format/line'
8
+ require 'prawn/format/state'
9
+
10
+ module Prawn
11
+ module Format
12
+
13
+ # The Parser class is used by the formatting subsystem to take
14
+ # the raw tokens from the Lexer class and wrap them in
15
+ # "instructions", which are then used by the LayoutBuilder to
16
+ # determine how each token should be rendered.
17
+ #
18
+ # The parser also ensures that tags are opened and closed
19
+ # consistently. It is not forgiving at all--if you forget to
20
+ # close a tag, the parser will raise an exception (TagError).
21
+ #
22
+ # It will also raise an exception if a tag is encountered with
23
+ # no style definition for it.
24
+ class Parser
25
+ # This is the exception that gets raised when the parser cannot
26
+ # process a particular tag.
27
+ class TagError < RuntimeError; end
28
+
29
+ attr_reader :document
30
+ attr_reader :tags
31
+ attr_reader :state
32
+
33
+ # Creates a new parser associated with the given +document+, and which
34
+ # will parse the given +text+. The +options+ may include either of two
35
+ # optional keys:
36
+ #
37
+ # * :tags is used to specify the hash of tags and their associated
38
+ # styles. Any tag not specified here will not be recognized by the
39
+ # parser, and will cause an error if it is encountered in +text+.
40
+ # * :styles is used to specify the mapping of style classes to their
41
+ # definitions. The keys should be symbols, and the values should be
42
+ # hashes. The values have the same format as for the :tags map.
43
+ # * :style is the default style for any text not otherwise wrapped by
44
+ # tags.
45
+ #
46
+ # Example:
47
+ #
48
+ # parser = Parser.new(@pdf, "<b class='ruby'>hello</b>",
49
+ # :tags => { :b => { :font_weight => :bold } },
50
+ # :styles => { :ruby => { :color => "red" } },
51
+ # :style => { :font_family => "Times-Roman" })
52
+ #
53
+ # See Format::State for a description of the supported style options.
54
+ def initialize(document, text, options={})
55
+ @document = document
56
+ @lexer = Lexer.new(text)
57
+ @tags = options[:tags] || {}
58
+ @styles = options[:styles] || {}
59
+
60
+ @state = State.new(document, :style => options[:style])
61
+ @lexer.verbatim = (@state.white_space == :pre)
62
+
63
+ @action = :start
64
+
65
+ @saved = []
66
+ @tag_stack = []
67
+ end
68
+
69
+ def verbatim?
70
+ @lexer.verbatim
71
+ end
72
+
73
+ # Returns the next instruction from the stream. If there are no more
74
+ # instructions in the stream (e.g., the end has been encountered), this
75
+ # returns +nil+.
76
+ def next
77
+ return @saved.pop if @saved.any?
78
+
79
+ case @action
80
+ when :start then start_parse
81
+ when :text then text_parse
82
+ else raise "BUG: unknown parser action: #{@action.inspect}"
83
+ end
84
+ end
85
+
86
+ # "Ungets" the given +instruction+. This makes it so the next call to
87
+ # +next+ will return +instruction+. This is useful for backtracking.
88
+ def push(instruction)
89
+ @saved.push(instruction)
90
+ end
91
+
92
+ # This is identical to +next+, except it does not consume the
93
+ # instruction. This means that +peek+ returns the instruction that will
94
+ # be returned by the next call to +next+. It is useful for testing
95
+ # the next instruction in the stream without advancing the stream.
96
+ def peek
97
+ save = self.next
98
+ push(save) if save
99
+ return save
100
+ end
101
+
102
+ # Returns +true+ if the end of the stream has been reached. Subsequent
103
+ # calls to +peek+ or +next+ will return +nil+.
104
+ def eos?
105
+ peek.nil?
106
+ end
107
+
108
+ private
109
+
110
+ def start_parse
111
+ instruction = nil
112
+ while (@token = @lexer.next)
113
+ case @token[:type]
114
+ when :text
115
+ @position = 0
116
+ instruction = text_parse
117
+ when :open
118
+ instruction = process_open_tag
119
+ when :close
120
+ raise TagError, "closing #{@token[:tag]}, but no tags are open" if @tag_stack.empty?
121
+ raise TagError, "closing #{@tag_stack.last[:tag]} with #{@token[:tag]}" if @tag_stack.last[:tag] != @token[:tag]
122
+
123
+ instruction = Instructions::TagClose.new(@state, @tag_stack.pop)
124
+ @state = @state.previous
125
+ else
126
+ raise ArgumentError, "[BUG] unknown token type #{@token[:type].inspect} (#{@token.inspect})"
127
+ end
128
+
129
+ if instruction
130
+ if instruction.start_verbatim?
131
+ @lexer.verbatim = true
132
+ elsif instruction.end_verbatim?
133
+ @lexer.verbatim = false
134
+ end
135
+
136
+ return instruction
137
+ end
138
+ end
139
+
140
+ return nil
141
+ end
142
+
143
+ def text_parse
144
+ if @token[:text][@position]
145
+ @action = :text
146
+ @position += 1
147
+
148
+ text = @token[:text][@position - 1]
149
+ if @state.white_space == :pre && text =~ /(?:\r\n|\r|\n)/
150
+ Instructions::TagClose.new(@state, { :style => { :display => :break }, :options => {} })
151
+ else
152
+ Instructions::Text.new(@state, text)
153
+ end
154
+ else
155
+ @action = :start
156
+ start_parse
157
+ end
158
+ end
159
+
160
+ def process_open_tag
161
+ @tag_stack << @token
162
+ raise TagError, "undefined tag #{@token[:tag]}" unless @tags[@token[:tag]]
163
+ @token[:style] = @tags[@token[:tag]].dup
164
+
165
+ (@token[:options][:class] || "").split(/\s/).each do |name|
166
+ @token[:style].update(@styles[name.to_sym] || {})
167
+ end
168
+
169
+ if @token[:style][:meta]
170
+ @token[:style][:meta].each do |key, value|
171
+ @token[:style][value] = @token[:options][key]
172
+ end
173
+ end
174
+
175
+ @state = @state.with_style(@token[:style])
176
+ Instructions::TagOpen.new(@state, @token)
177
+ end
178
+ end
179
+
180
+ end
181
+ end
@@ -0,0 +1,189 @@
1
+ # encoding: utf-8
2
+
3
+ module Prawn
4
+ module Format
5
+ class State
6
+ attr_reader :document
7
+ attr_reader :original_style, :style
8
+
9
+ def initialize(document, options={})
10
+ @document = document
11
+ @previous = options[:previous]
12
+
13
+ @original_style = (@previous && @previous.inheritable_style || {}).
14
+ merge(options[:style] || {})
15
+
16
+ compute_styles!
17
+
18
+ @style[:kerning] = font.has_kerning_data? unless @style.key?(:kerning)
19
+ end
20
+
21
+ def inheritable_style
22
+ @inheritable_style ||= begin
23
+ subset = original_style.dup
24
+ subset.delete(:meta)
25
+ subset.delete(:display)
26
+ subset.delete(:width)
27
+
28
+ # explicitly set font-size so that relative font-sizes don't get
29
+ # recomputed upon each nesting.
30
+ subset[:font_size] = font_size
31
+
32
+ subset
33
+ end
34
+ end
35
+
36
+ def kerning?
37
+ @style[:kerning]
38
+ end
39
+
40
+ def display
41
+ @style[:display] || :inline
42
+ end
43
+
44
+ def font_size
45
+ @style[:font_size] || 12
46
+ end
47
+
48
+ def font_family
49
+ @style[:font_family] || "Helvetica"
50
+ end
51
+
52
+ def font_style
53
+ @style[:font_style] || :normal
54
+ end
55
+
56
+ def font_weight
57
+ @style[:font_weight] || :normal
58
+ end
59
+
60
+ def color
61
+ @style[:color] || "000000"
62
+ end
63
+
64
+ def vertical_align
65
+ @style[:vertical_align] || 0
66
+ end
67
+
68
+ def text_decoration
69
+ @style[:text_decoration] || :none
70
+ end
71
+
72
+ def white_space
73
+ @style[:white_space] || :normal
74
+ end
75
+
76
+ def width
77
+ @style[:width] || 0
78
+ end
79
+
80
+ def font
81
+ @font ||= document.find_font(font_family, :style => pdf_font_style)
82
+ end
83
+
84
+ def pdf_font_style
85
+ if bold? && italic?
86
+ :bold_italic
87
+ elsif bold?
88
+ :bold
89
+ elsif italic?
90
+ :italic
91
+ else
92
+ :normal
93
+ end
94
+ end
95
+
96
+ def with_style(style)
97
+ self.class.new(document, :previous => self, :style => style)
98
+ end
99
+
100
+ def apply!(text_object, cookies)
101
+ if cookies[:color] != color
102
+ cookies[:color] = color
103
+ text_object.fill_color(color)
104
+ end
105
+
106
+ if cookies[:vertical_align] != vertical_align
107
+ cookies[:vertical_align] = vertical_align
108
+ text_object.rise(vertical_align)
109
+ end
110
+ end
111
+
112
+ def apply_font!(text_object, cookies, subset)
113
+ if cookies[:font] != [font_family, pdf_font_style, font_size, subset]
114
+ cookies[:font] = [font_family, pdf_font_style, font_size, subset]
115
+ font = document.font(font_family, :style => pdf_font_style)
116
+ font.add_to_current_page(subset)
117
+ text_object.font(font.identifier_for(subset), font_size)
118
+ end
119
+ end
120
+
121
+ def italic?
122
+ font_style == :italic
123
+ end
124
+
125
+ def bold?
126
+ font_weight == :bold
127
+ end
128
+
129
+ def previous(attr=nil, default=nil)
130
+ return @previous unless attr
131
+ return default unless @previous
132
+ return @previous.send(attr) || default
133
+ end
134
+
135
+ private
136
+
137
+ def compute_styles!
138
+ @style = @original_style.dup
139
+
140
+ evaluate_style(:font_size, 12, :current)
141
+ evaluate_style(:vertical_align, 0, font_size, :super => "+40%", :sub => "-30%")
142
+ evaluate_style(:width, 0, document.bounds.width)
143
+
144
+ @style[:color] = evaluate_color(@style[:color])
145
+ end
146
+
147
+ def evaluate_style(which, default, relative_to, mappings={})
148
+ current = previous(which, default)
149
+ relative_to = current if relative_to == :current
150
+ @style[which] = document.evaluate_measure(@style[which],
151
+ :em => @previous && @previous.font_size || 12,
152
+ :current => current, :relative => relative_to, :mappings => mappings) || default
153
+ end
154
+
155
+ HTML_COLORS = {
156
+ "aqua" => "00FFFF",
157
+ "black" => "000000",
158
+ "blue" => "0000FF",
159
+ "fuchsia" => "FF00FF",
160
+ "gray" => "808080",
161
+ "green" => "008000",
162
+ "lime" => "00FF00",
163
+ "maroon" => "800000",
164
+ "navy" => "000080",
165
+ "olive" => "808000",
166
+ "purple" => "800080",
167
+ "red" => "FF0000",
168
+ "silver" => "C0C0C0",
169
+ "teal" => "008080",
170
+ "white" => "FFFFFF",
171
+ "yellow" => "FFFF00"
172
+ }
173
+
174
+ def evaluate_color(color)
175
+ case color
176
+ when nil then nil
177
+ when /^\s*#?([a-f0-9]{3})\s*$/i then
178
+ return $1.gsub(/./, '\&0')
179
+ when /^\s*#?([a-f0-9]+)$\s*/i then
180
+ return $1
181
+ when /^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/
182
+ return "%02x%02x%02x" % [$1.to_i, $2.to_i, $3.to_i]
183
+ else
184
+ return HTML_COLORS[color.strip.downcase]
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end