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,52 @@
1
+ # encoding: utf-8
2
+
3
+ require 'prawn/format/instructions/base'
4
+
5
+ module Prawn
6
+ module Format
7
+ module Instructions
8
+
9
+ class TagClose < Base
10
+ def self.close(state, tag, draw_state)
11
+ closer = new(state, tag)
12
+ closer.draw(state.document, draw_state)
13
+ end
14
+
15
+ attr_reader :tag
16
+
17
+ def initialize(state, tag)
18
+ super(state)
19
+ @tag = tag
20
+ end
21
+
22
+ def [](property)
23
+ @tag[:style][property]
24
+ end
25
+
26
+ def draw(document, draw_state, options={})
27
+ (@tag[:effects] || []).each do |effect|
28
+ effect.finish(document, draw_state)
29
+ draw_state[:pending_effects].delete(effect)
30
+ end
31
+ end
32
+
33
+ def break?
34
+ force_break?
35
+ end
36
+
37
+ def style
38
+ @tag[:style]
39
+ end
40
+
41
+ def force_break?
42
+ @tag[:style][:display] == :break
43
+ end
44
+
45
+ def end_verbatim?
46
+ @tag[:style][:white_space] == :pre
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,95 @@
1
+ # encoding: utf-8
2
+
3
+ require 'prawn/format/instructions/base'
4
+ require 'prawn/format/effects/link'
5
+ require 'prawn/format/effects/underline'
6
+
7
+ module Prawn
8
+ module Format
9
+ module Instructions
10
+
11
+ class TagOpen < Base
12
+ attr_reader :tag
13
+
14
+ def initialize(state, tag)
15
+ super(state)
16
+ @tag = tag
17
+ end
18
+
19
+ def draw(document, draw_state, options={})
20
+ draw_width(document, draw_state)
21
+ draw_destination(document, draw_state)
22
+ draw_link(document, draw_state)
23
+ draw_underline(document, draw_state)
24
+ end
25
+
26
+ def start_verbatim?
27
+ @tag[:style][:white_space] == :pre
28
+ end
29
+
30
+ def style
31
+ @tag[:style]
32
+ end
33
+
34
+ def width
35
+ @state.width
36
+ end
37
+
38
+ private
39
+
40
+ def draw_width(document, draw_state)
41
+ if width > 0
42
+ draw_state[:dx] += width
43
+ draw_state[:text].move_to(draw_state[:dx], draw_state[:dy])
44
+ end
45
+ end
46
+
47
+ def draw_destination(document, draw_state)
48
+ return unless tag[:style][:anchor]
49
+
50
+ x = draw_state[:real_x]
51
+ y = draw_state[:real_y] + draw_state[:dy] + ascent
52
+
53
+ label, destination = case tag[:style][:anchor]
54
+ when /^zoom=([\d\.]+):(.*)$/
55
+ [$2, document.dest_xyz(x, y, $1.to_f)]
56
+ when /^fit:(.*)$/
57
+ [$1, document.dest_fit]
58
+ when /^fith:(.*)$/
59
+ [$1, document.dest_fit_horizontally(y)]
60
+ when /^fitv:(.*)$/
61
+ [$1, document.dest_fit_vertically(x)]
62
+ when /^fitb:(.*)$/
63
+ [$1, document.dest_fit_bounds]
64
+ when /^fitbh:(.*)$/
65
+ [$1, document.dest_fit_bounds_horizontally(y)]
66
+ when /^fitbv:(.*)$/
67
+ [$1, document.dest_fit_bounds_vertically(x)]
68
+ else
69
+ [tag[:style][:anchor], document.dest_fit_bounds]
70
+ end
71
+
72
+ document.add_dest(label, destination)
73
+ end
74
+
75
+ def draw_link(document, draw_state)
76
+ return unless tag[:style][:target]
77
+ add_effect(Effects::Link.new(tag[:style][:target], draw_state[:dx]), draw_state)
78
+ end
79
+
80
+ def draw_underline(document, draw_state)
81
+ return unless tag[:style][:text_decoration] == :underline
82
+ add_effect(Effects::Underline.new(draw_state[:dx], @state), draw_state)
83
+ end
84
+
85
+ def add_effect(effect, draw_state)
86
+ tag[:effects] ||= []
87
+ tag[:effects].push(effect)
88
+
89
+ draw_state[:pending_effects].push(effect)
90
+ end
91
+ end
92
+
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,89 @@
1
+ # encoding: utf-8
2
+
3
+ require 'prawn/format/instructions/base'
4
+
5
+ module Prawn
6
+ module Format
7
+ module Instructions
8
+
9
+ class Text < Base
10
+ attr_reader :text
11
+
12
+ def initialize(state, text, options={})
13
+ super(state)
14
+ @text = text
15
+ @break = options.key?(:break) ? options[:break] : text.index(/[-\xE2\x80\x94\s]/)
16
+ @discardable = options.key?(:discardable) ? options[:discardable] : text.index(/\s/)
17
+ state.font.normalize_encoding(@text) if options.fetch(:normalize, true)
18
+ end
19
+
20
+ def dup
21
+ self.class.new(state, @text.dup, :normalize => false,
22
+ :break => @break, :discardable => @discardable)
23
+ end
24
+
25
+ def accumulate(list)
26
+ if list.last.is_a?(Text) && list.last.state == state
27
+ list.last.text << @text
28
+ else
29
+ list.push(dup)
30
+ end
31
+
32
+ return list
33
+ end
34
+
35
+ def spaces
36
+ @spaces ||= @text.scan(/ /).length
37
+ end
38
+
39
+ def height(ignore_discardable=false)
40
+ if ignore_discardable && discardable?
41
+ 0
42
+ else
43
+ @height
44
+ end
45
+ end
46
+
47
+ def break?
48
+ @break
49
+ end
50
+
51
+ def discardable?
52
+ @discardable
53
+ end
54
+
55
+ def compatible?(with)
56
+ with.is_a?(self.class) && with.state == state
57
+ end
58
+
59
+ def width(type=:all)
60
+ @width ||= @state.font.width_of(@text, :size => @state.font_size, :kerning => @state.kerning?)
61
+
62
+ case type
63
+ when :discardable then discardable? ? @width : 0
64
+ when :nondiscardable then discardable? ? 0 : @width
65
+ else @width
66
+ end
67
+ end
68
+
69
+ def to_s
70
+ @text
71
+ end
72
+
73
+ def draw(document, draw_state, options={})
74
+ @state.apply!(draw_state[:text], draw_state[:cookies])
75
+
76
+ encoded_text = @state.font.encode_text(@text, :kerning => @state.kerning?)
77
+ encoded_text.each do |subset, chunk|
78
+ @state.apply_font!(draw_state[:text], draw_state[:cookies], subset)
79
+ draw_state[:text].show(chunk)
80
+ end
81
+ draw_state[:dx] += width
82
+
83
+ draw_state[:dx] += draw_state[:padding] * spaces if draw_state[:padding]
84
+ end
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,113 @@
1
+ # encoding: utf-8
2
+
3
+ require 'prawn/format/line'
4
+ require 'prawn/format/parser'
5
+
6
+ module Prawn
7
+ module Format
8
+ class LayoutBuilder
9
+ attr_reader :document, :options
10
+
11
+ def initialize(document, text, options={})
12
+ @document = document
13
+ @options = options
14
+ @tags = document.tags.merge(options[:tags] || {})
15
+ @styles = document.styles.merge(options[:styles] || {})
16
+ style = document.default_style.merge(options[:default_style] || {})
17
+
18
+ translate_prawn_options(style, options)
19
+
20
+ @parser = Parser.new(@document, text,
21
+ :tags => @tags, :styles => @styles, :style => style)
22
+
23
+ @state = {}
24
+ end
25
+
26
+ def done?
27
+ @parser.eos?
28
+ end
29
+
30
+ def word_wrap(width, options={}, &block)
31
+ if options[:height] && block
32
+ raise ArgumentError, "cannot specify both height and a block"
33
+ elsif options[:height]
34
+ block = Proc.new { |l, h| h > options[:height] }
35
+ elsif block.nil?
36
+ block = Proc.new { |l, h| false }
37
+ end
38
+
39
+ lines = []
40
+ total_height = 0
41
+
42
+ while (line = self.next(width))
43
+ if block[line, total_height + line.height]
44
+ unget(line)
45
+ break
46
+ end
47
+
48
+ total_height += line.height
49
+ lines.push(line)
50
+ end
51
+
52
+ return lines
53
+ end
54
+
55
+ def fill(x, y, width, fill_options={}, &block)
56
+ lines = word_wrap(width, fill_options, &block)
57
+ draw_options = options.merge(fill_options).merge(:state => @state)
58
+ @state = document.draw_lines(x, y, width, lines, draw_options)
59
+ @state.delete(:cookies)
60
+ return @state[:dy] + y
61
+ end
62
+
63
+ def next(line_width=nil)
64
+ line = []
65
+ width = 0
66
+ break_at = nil
67
+
68
+ while (instruction = @parser.next)
69
+ next if !@parser.verbatim? && line.empty? && instruction.discardable? # ignore discardables at line start
70
+ line.push(instruction)
71
+
72
+ if instruction.break?
73
+ width += instruction.width(:nondiscardable)
74
+ break_at = line.length if line_width && width <= line_width
75
+ width += instruction.width(:discardable)
76
+ else
77
+ width += instruction.width
78
+ end
79
+
80
+ if instruction.force_break? || line_width && width >= line_width
81
+ break_at ||= line.length
82
+
83
+ @parser.push(line.pop) while line.length > break_at
84
+ hard_break = instruction.force_break? || @parser.eos?
85
+
86
+ return Line.new(line, hard_break)
87
+ end
88
+ end
89
+
90
+ Line.new(line, true) if line.any?
91
+ end
92
+
93
+ def unget(line)
94
+ line.source.reverse_each { |instruction| @parser.push(instruction) }
95
+ end
96
+
97
+ def translate_prawn_options(style, options)
98
+ style[:kerning] = options[:kerning] if options.key?(:kerning)
99
+ style[:font_size] = options[:size] if options.key?(:size)
100
+
101
+ case options[:style]
102
+ when :bold then
103
+ style[:font_weight] = :bold
104
+ when :italic then
105
+ style[:font_style] = :italic
106
+ when :bold_italic then
107
+ style[:font_weight] = :bold
108
+ style[:font_style] = :italic
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,222 @@
1
+ # encoding: utf-8
2
+
3
+ require 'strscan'
4
+
5
+ module Prawn
6
+ module Format
7
+
8
+ # The Lexer class is used by the formatting subsystem to scan a string
9
+ # and extract tokens from it. The tokens it looks for are either text,
10
+ # XML entities, or XML tags.
11
+ #
12
+ # Note that the lexer only scans for a subset of XML--it is not a true
13
+ # XML scanner, and understands just enough to provide a basic markup
14
+ # language for use in formatting documents.
15
+ #
16
+ # The subset includes only XML entities and tags--instructions, comments,
17
+ # and the like are not supported.
18
+ class Lexer
19
+ # When the scanner encounters a state or entity it is not able to
20
+ # handle, this exception will be raised.
21
+ class InvalidFormat < RuntimeError; end
22
+
23
+ # Controls whether whitespace is lexed verbatim or not. If not,
24
+ # adjacent whitespace is compressed into a single space character
25
+ # (this includes newlines).
26
+ attr_accessor :verbatim
27
+
28
+ # Create a new lexer that will scan the given text. The text must be
29
+ # UTF-8 encoded, and must consist of well-formed XML in the subset
30
+ # understand by the lexer.
31
+ def initialize(text)
32
+ @scanner = StringScanner.new(text)
33
+ @state = :start
34
+ @verbatim = false
35
+ end
36
+
37
+ # Returns the next token from the scanner. If the end of the string
38
+ # has been reached, this will return nil. Otherwise, the token itself
39
+ # is returned as a hash. The hash will always include a :type key,
40
+ # identifying the type of the token. It will be one of :text, :open,
41
+ # or :close.
42
+ #
43
+ # For :text tokens, the hash will also contain a :text key, which will
44
+ # point to an array of strings. Each element of the array contains
45
+ # either word, whitespace, or some other character at which the line
46
+ # may be broken.
47
+ #
48
+ # For :open tokens, the hash will contain a :tag key which identifies
49
+ # the name of the tag (as a symbol), and an :options key, which
50
+ # is another hash that contains the options that were given with the
51
+ # tag.
52
+ #
53
+ # For :close tokens, the hash will contain only a :tag key.
54
+ def next
55
+ if @state == :start && @scanner.eos?
56
+ return nil
57
+ else
58
+ scan_next_token
59
+ end
60
+ end
61
+
62
+ # Iterates over each token in the string, until the end of the string
63
+ # is reached. Each token is yielded. See #next for a discussion of the
64
+ # available token types.
65
+ def each
66
+ while (token = next_token)
67
+ yield token
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def scan_next_token
74
+ case @state
75
+ when :start then scan_start_state
76
+ when :self_close then scan_self_close_state
77
+ end
78
+ end
79
+
80
+ if RUBY_VERSION >= "1.9.0"
81
+ def scan_other_text
82
+ @scanner.scan(/[^-\xE2\x80\x94\s<&]+/)
83
+ end
84
+ else
85
+ def scan_other_text
86
+ return nil if @scanner.eos?
87
+
88
+ result = @scanner.scan_until(/[-\s<&]|\xE2\x80\x94/)
89
+ if result
90
+ @scanner.pos -= @scanner.matched.length
91
+ return nil if result == "<" || result == "&"
92
+ return result[0,result.length - @scanner.matched.length]
93
+ else
94
+ result = @scanner.rest
95
+ @scanner.terminate
96
+ return result
97
+ end
98
+ end
99
+ end
100
+
101
+ def scan_text_chunk
102
+ @scanner.scan(/-/) || # hyphen
103
+ @scanner.scan(/\xe2\x80\x94/) || # mdash
104
+ scan_other_text
105
+ end
106
+
107
+ def scan_verbatim_text_chunk
108
+ @scanner.scan(/\r\n|\r|\n/) || # newline
109
+ @scanner.scan(/\t/) || # tab
110
+ @scanner.scan(/ +/) || # spaces
111
+ scan_text_chunk
112
+ end
113
+
114
+ def scan_nonverbatim_text_chunk
115
+ (@scanner.scan(/\s+/) && " ") || # whitespace
116
+ scan_text_chunk
117
+ end
118
+
119
+ def scan_next_text_chunk
120
+ if @verbatim
121
+ scan_verbatim_text_chunk
122
+ else
123
+ scan_nonverbatim_text_chunk
124
+ end
125
+ end
126
+
127
+ def scan_start_state
128
+ if @scanner.scan(/</)
129
+ if @scanner.scan(%r(/))
130
+ scan_end_tag
131
+ else
132
+ scan_open_tag
133
+ end
134
+ elsif @scanner.scan(/&/)
135
+ scan_entity
136
+ else
137
+ pieces = []
138
+ loop do
139
+ chunk = scan_next_text_chunk or break
140
+ pieces << chunk
141
+ end
142
+ { :type => :text, :text => pieces }
143
+ end
144
+ end
145
+
146
+ ENTITY_MAP = {
147
+ "lt" => "<",
148
+ "gt" => ">",
149
+ "amp" => "&",
150
+ "mdash" => "\xE2\x80\x94",
151
+ "ndash" => "\xE2\x80\x93",
152
+ "nbsp" => "\xC2\xA0",
153
+ "bull" => "\342\200\242",
154
+ "quot" => '"',
155
+ }
156
+
157
+ def scan_entity
158
+ entity = @scanner.scan(/(?:#x?)?\w+/) or error("bad format for entity")
159
+ @scanner.scan(/;/) or error("missing semicolon to terminate entity")
160
+
161
+ text = case entity
162
+ when /#(\d+)/ then [$1.to_i].pack("U*")
163
+ when /#x([0-9a-f]+)/ then [$1.to_i(16)].pack("U*")
164
+ else
165
+ result = ENTITY_MAP[entity] or error("unrecognized entity #{entity.inspect}")
166
+ result.dup
167
+ end
168
+
169
+ { :type => :text, :text => [text] }
170
+ end
171
+
172
+ def scan_open_tag
173
+ tag = @scanner.scan(/\w+/) or error("'<' without valid tag")
174
+ tag = tag.downcase.to_sym
175
+
176
+ options = {}
177
+ @scanner.skip(/\s*/)
178
+ while !@scanner.eos? && @scanner.peek(1) =~ /\w/
179
+ name = @scanner.scan(/\w+/)
180
+ @scanner.scan(/\s*=\s*/) or error("expected assigment after option #{name}")
181
+ if (delim = @scanner.scan(/['"]/))
182
+ value = @scanner.scan(/[^#{delim}]*/)
183
+ @scanner.scan(/#{delim}/) or error("expected option value to end with #{delim}")
184
+ else
185
+ value = @scanner.scan(/[^\s>]*/)
186
+ end
187
+ options[name.downcase.to_sym] = value
188
+ @scanner.skip(/\s*/)
189
+ end
190
+
191
+ if @scanner.scan(%r(/))
192
+ @self_close = true
193
+ @tag = tag
194
+ @state = :self_close
195
+ else
196
+ @self_close = false
197
+ @state = :start
198
+ end
199
+
200
+ @scanner.scan(/>/) or error("unclosed tag #{tag.inspect}")
201
+
202
+ { :type => :open, :tag => tag, :options => options }
203
+ end
204
+
205
+ def scan_end_tag
206
+ tag = @scanner.scan(/\w+/).to_sym
207
+ @scanner.skip(/\s*/)
208
+ @scanner.scan(/>/) or error("unclosed ending tag #{tag.inspect}")
209
+ { :type => :close, :tag => tag }
210
+ end
211
+
212
+ def scan_self_close_state
213
+ @state = :start
214
+ { :type => :close, :tag => @tag }
215
+ end
216
+
217
+ def error(message)
218
+ raise InvalidFormat, "#{message} at #{@scanner.pos} -> #{@scanner.rest.inspect[0,50]}..."
219
+ end
220
+ end
221
+ end
222
+ end