prawn-format 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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