prawn-format 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest +37 -0
- data/Rakefile +31 -0
- data/examples/basic-formatting.rb +37 -0
- data/examples/christmas-carol.txt +717 -0
- data/examples/document.rb +61 -0
- data/examples/flowing.rb +24 -0
- data/examples/style-classes.rb +12 -0
- data/examples/syntax-highlighting.rb +31 -0
- data/examples/tags.rb +24 -0
- data/lib/prawn/format.rb +211 -0
- data/lib/prawn/format/effects/link.rb +30 -0
- data/lib/prawn/format/effects/underline.rb +32 -0
- data/lib/prawn/format/instructions/base.rb +62 -0
- data/lib/prawn/format/instructions/tag_close.rb +52 -0
- data/lib/prawn/format/instructions/tag_open.rb +95 -0
- data/lib/prawn/format/instructions/text.rb +89 -0
- data/lib/prawn/format/layout_builder.rb +113 -0
- data/lib/prawn/format/lexer.rb +222 -0
- data/lib/prawn/format/line.rb +99 -0
- data/lib/prawn/format/parser.rb +181 -0
- data/lib/prawn/format/state.rb +189 -0
- data/lib/prawn/format/text_object.rb +107 -0
- data/lib/prawn/format/version.rb +11 -0
- data/manual/html.rb +187 -0
- data/manual/include/basics.rb +6 -0
- data/manual/include/breaks.rb +13 -0
- data/manual/include/custom-tags.rb +10 -0
- data/manual/include/custom-tags2.rb +2 -0
- data/manual/include/indent.rb +4 -0
- data/manual/include/options.rb +15 -0
- data/manual/include/style-classes.rb +5 -0
- data/manual/manual.txt +101 -0
- data/manual/pdf.rb +204 -0
- data/prawn-format.gemspec +45 -0
- data/spec/layout_builder_spec.rb +27 -0
- data/spec/lexer_spec.rb +91 -0
- data/spec/parser_spec.rb +103 -0
- data/spec/spec_helper.rb +24 -0
- 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
|