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,204 @@
1
+ $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
2
+ require 'prawn'
3
+ require 'prawn/format'
4
+ require 'prawn/format/version'
5
+ require 'coderay'
6
+
7
+ def process_style(document, style, content, line_number)
8
+ content = process_substitutions(content)
9
+
10
+ case style
11
+ when "h1" then h1(document, content)
12
+ when "h2" then h2(document, content)
13
+ when "p" then paragraph(document, content)
14
+ when "fp" then paragraph(document, content, false)
15
+ when "ul" then start_list(document)
16
+ when "li" then list_item(document, content)
17
+ when "/ul" then end_list(document)
18
+ when "page" then new_page(document)
19
+ when "highlight" then highlight(document, content)
20
+ when "hr" then horiz_rule(document)
21
+ when "center" then center(document, content)
22
+ else warn "unknown style #{style.inspect}"
23
+ end
24
+
25
+ rescue Exception => err
26
+ puts "[error occurred while processing line ##{line_number}]"
27
+ raise
28
+ end
29
+
30
+ def process_substitutions(content)
31
+ content.
32
+ gsub(/%FORMAT:VERSION%/, Prawn::Format::Version::STRING).
33
+ gsub(/%NOW%/, Time.now.utc.strftime("%e %B %Y at %H:%M UTC")).
34
+ gsub(/%PDF\{(.*?)\}HTML\{(.*?)\}END%/, '\\1')
35
+ end
36
+
37
+ def center(document, content)
38
+ padding(document, :clear => true)
39
+ document.text(content, :plain => false, :align => :center)
40
+ padding(document)
41
+ end
42
+
43
+ def horiz_rule(document)
44
+ padding(document, :clear => true)
45
+ document.stroke_color "000000"
46
+ document.stroke_horizontal_rule
47
+ padding(document)
48
+ end
49
+
50
+ def h1(document, content)
51
+ clear_padding!
52
+ document.text "<h1>#{content}</h1>"
53
+ document.stroke_color "000080"
54
+ document.stroke_horizontal_rule
55
+ padding(document, :size => document.font_size * 2)
56
+ end
57
+
58
+ def h2(document, content)
59
+ clear_padding!
60
+ document.text "<h2>#{content}</h2>"
61
+ document.stroke_color "000080"
62
+ document.stroke_horizontal_rule
63
+ padding(document, :size => document.font_size * 2)
64
+ end
65
+
66
+ def paragraph(document, content, indent=true)
67
+ return unless content.strip.length > 0
68
+ clear_padding!
69
+ document.text "#{content}", :align => :justify
70
+ padding(document, :size => document.font_size / 2)
71
+ end
72
+
73
+ def start_list(document)
74
+ padding(document)
75
+ end
76
+
77
+ def list_item(document, content)
78
+ clear_padding!
79
+
80
+ indent_b = document.font_size * 3
81
+ indent = document.font_size * 4
82
+
83
+ document.start_new_page if document.y < document.font_size
84
+ y = document.y - document.bounds.absolute_bottom
85
+ document.text "&bull;", :at => [indent_b, y - document.font.ascender]
86
+ document.layout(content, :align => :justify) do |helper|
87
+ while !helper.done?
88
+ y = helper.fill(indent, y, document.bounds.width-indent, :height => document.y)
89
+ if helper.done?
90
+ document.y = y + document.bounds.absolute_bottom
91
+ else
92
+ document.start_new_page
93
+ y = document.y - document.bounds.absolute_bottom
94
+ end
95
+ end
96
+ end
97
+
98
+ padding(document, :size => document.font_size / 4)
99
+ end
100
+
101
+ def end_list(document)
102
+ padding(document)
103
+ end
104
+
105
+ def new_page(document)
106
+ document.start_new_page
107
+ clear_padding!
108
+ end
109
+
110
+ def highlight(document, content)
111
+ file, syntax = content.split(/,/)
112
+ analyzed = CodeRay.scan(File.read(File.join(File.dirname(__FILE__), file)), syntax.to_sym)
113
+ html = "<pre>" + analyzed.html + "</pre>"
114
+
115
+ padding(document, :size => document.font_size * 2, :clear => true)
116
+ y = document.y - document.bounds.absolute_bottom
117
+ start_y = y + document.font_size
118
+
119
+ indent = document.font_size * 2
120
+
121
+ document.layout(html) do |helper|
122
+ while !helper.done?
123
+ y = helper.fill(indent, y, document.bounds.width-indent*2, :height => document.y)
124
+ if helper.done?
125
+ document.y = y + document.bounds.absolute_bottom
126
+ else
127
+ document.start_new_page
128
+ y = document.y - document.bounds.absolute_bottom
129
+ end
130
+ end
131
+ end
132
+
133
+ document.stroke_color "a0a0a0"
134
+ document.rectangle [document.font_size, start_y], bounds.width - document.font_size*2, (start_y - y)
135
+ document.stroke
136
+
137
+ padding(document, :size => document.font_size)
138
+ end
139
+
140
+ def clear_padding!
141
+ @last_padding = nil
142
+ end
143
+
144
+ # This padding stuff is mostly here just so that adjacent vertical spaces
145
+ # will collapse.
146
+ def padding(document, options={})
147
+ size = options[:size] || document.font_size
148
+
149
+ if @last_padding
150
+ full_size = [@last_padding, size].max
151
+ size = full_size - @last_padding
152
+ @last_padding = full_size
153
+ else
154
+ @last_padding = size
155
+ end
156
+
157
+ document.y -= size
158
+ clear_padding! if options[:clear]
159
+ end
160
+
161
+ SERIF_FONT = "/Library/Fonts/Baskerville.dfont"
162
+
163
+ Prawn::Document.generate("prawn-format.pdf", :compress => true) do
164
+ if File.exists?(SERIF_FONT)
165
+ font_families["Baskerville"] = {
166
+ :normal => { :file => SERIF_FONT, :font => 1 },
167
+ :italic => { :file => SERIF_FONT, :font => 2 },
168
+ :bold => { :file => SERIF_FONT, :font => 4 }, # semi-bold, not bold
169
+ :bold_italic => { :file => SERIF_FONT, :font => 3 }
170
+ }
171
+ font "Baskerville", :size => 14
172
+ else
173
+ warn "Baskerville font is preferred for the manual, but could not be found. Using Times-Roman."
174
+ font "Times-Roman", :size => 14
175
+ end
176
+
177
+ tags :h1 => { :font_size => "2em", :font_weight => :bold, :color => "navy" },
178
+ :h2 => { :font_size => "1.5em", :font_weight => :bold, :color => "navy" },
179
+ :about => { :font_size => "80%", :color => "808080", :font_style => :italic }
180
+
181
+ styles :no => { :color => "gray" },
182
+ :c => { :color => "#666" },
183
+ :s => { :color => "#d20" },
184
+ :dl => { :color => "black" },
185
+ :co => { :color => "#036", :font_weight => :bold },
186
+ :pc => { :color => "#038", :font_weight => :bold },
187
+ :sy => { :color => "#A60" },
188
+ :r => { :color => "#080" },
189
+ :i => { :color => "#00D", :font_weight => :bold },
190
+ :idl => { :color => "#888", :font_weight => :bold },
191
+ :dl => { :color => "#840", :font_weight => :bold }
192
+
193
+ File.open("#{File.dirname(__FILE__)}/manual.txt") do |source|
194
+ number = 0
195
+ source.each_line do |line|
196
+ number += 1
197
+ line.chomp!
198
+ next if line.length == 0
199
+
200
+ style, content = line.match(/^(\S+)\.\s*(.*)/)[1,2]
201
+ process_style(self, style, content, number)
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,45 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{prawn-format}
3
+ s.version = "0.1.0"
4
+
5
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
6
+ s.authors = ["Jamis Buck"]
7
+ s.date = %q{2009-01-26}
8
+ s.description = %q{an extension of Prawn that allows inline formatting}
9
+ s.email = %q{jamis@jamisbuck.org}
10
+ s.extra_rdoc_files = ["lib/prawn/format/effects/link.rb", "lib/prawn/format/effects/underline.rb", "lib/prawn/format/instructions/base.rb", "lib/prawn/format/instructions/tag_close.rb", "lib/prawn/format/instructions/tag_open.rb", "lib/prawn/format/instructions/text.rb", "lib/prawn/format/layout_builder.rb", "lib/prawn/format/lexer.rb", "lib/prawn/format/line.rb", "lib/prawn/format/parser.rb", "lib/prawn/format/state.rb", "lib/prawn/format/text_object.rb", "lib/prawn/format/version.rb", "lib/prawn/format.rb"]
11
+ s.files = ["examples/basic-formatting.rb", "examples/christmas-carol.txt", "examples/document.rb", "examples/flowing.rb", "examples/style-classes.rb", "examples/syntax-highlighting.rb", "examples/tags.rb", "lib/prawn/format/effects/link.rb", "lib/prawn/format/effects/underline.rb", "lib/prawn/format/instructions/base.rb", "lib/prawn/format/instructions/tag_close.rb", "lib/prawn/format/instructions/tag_open.rb", "lib/prawn/format/instructions/text.rb", "lib/prawn/format/layout_builder.rb", "lib/prawn/format/lexer.rb", "lib/prawn/format/line.rb", "lib/prawn/format/parser.rb", "lib/prawn/format/state.rb", "lib/prawn/format/text_object.rb", "lib/prawn/format/version.rb", "lib/prawn/format.rb", "manual/html.rb", "manual/include/basics.rb", "manual/include/breaks.rb", "manual/include/custom-tags.rb", "manual/include/custom-tags2.rb", "manual/include/indent.rb", "manual/include/options.rb", "manual/include/style-classes.rb", "manual/manual.txt", "manual/pdf.rb", "Rakefile", "spec/layout_builder_spec.rb", "spec/lexer_spec.rb", "spec/parser_spec.rb", "spec/spec_helper.rb", "Manifest", "prawn-format.gemspec"]
12
+ s.has_rdoc = true
13
+ s.homepage = %q{http://rubyforge.org/projects/prawn}
14
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Prawn-format"]
15
+ s.require_paths = ["lib"]
16
+ s.rubyforge_project = %q{prawn}
17
+ s.rubygems_version = %q{1.2.0}
18
+ s.summary = %q{an extension of Prawn that allows inline formatting}
19
+ s.test_files = ["spec/layout_builder_spec.rb", "spec/lexer_spec.rb", "spec/parser_spec.rb"]
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 2
24
+
25
+ if current_version >= 3 then
26
+ s.add_runtime_dependency(%q<prawn>, [">= 0.4"])
27
+ s.add_runtime_dependency(%q<prawn>, ["< 0.5"])
28
+ s.add_development_dependency(%q<echoe>, [">= 0"])
29
+ s.add_development_dependency(%q<rake>, [">= 0"])
30
+ s.add_development_dependency(%q<coderay>, [">= 0"])
31
+ else
32
+ s.add_dependency(%q<prawn>, [">= 0.4"])
33
+ s.add_dependency(%q<prawn>, ["< 0.5"])
34
+ s.add_dependency(%q<echoe>, [">= 0"])
35
+ s.add_dependency(%q<rake>, [">= 0"])
36
+ s.add_dependency(%q<coderay>, [">= 0"])
37
+ end
38
+ else
39
+ s.add_dependency(%q<prawn>, [">= 0.4"])
40
+ s.add_dependency(%q<prawn>, ["< 0.5"])
41
+ s.add_dependency(%q<echoe>, [">= 0"])
42
+ s.add_dependency(%q<rake>, [">= 0"])
43
+ s.add_dependency(%q<coderay>, [">= 0"])
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
4
+
5
+ describe "when building a text layout" do
6
+
7
+ before(:each) { create_pdf }
8
+
9
+ it "should understand common tags by default" do
10
+ layout = new_layout("<b>hi</b> <i>there</i>")
11
+ assert_nothing_raised { lines(layout) }
12
+ end
13
+
14
+ private
15
+
16
+ def new_layout(text, opts={})
17
+ Prawn::Format::LayoutBuilder.new(@pdf, text, opts)
18
+ end
19
+
20
+ def lines(layout)
21
+ lines = []
22
+ while (line = layout.next)
23
+ lines << line
24
+ end
25
+ return lines
26
+ end
27
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
4
+
5
+ def lexer_for(text)
6
+ Prawn::Format::Lexer.new(text)
7
+ end
8
+
9
+ describe "when scanning text" do
10
+
11
+ it "should scan a single text word as a chunk" do
12
+ lexer = lexer_for("christmas")
13
+ assert_equal({ :type => :text, :text => ["christmas"] }, lexer.next)
14
+ end
15
+
16
+ it "should delimit multiple words by spaces" do
17
+ lexer = lexer_for("a christmas carol by charles dickens")
18
+ assert_equal({ :type => :text, :text => ["a", " ", "christmas", " ", "carol", " ", "by", " ", "charles", " ", "dickens"] }, lexer.next)
19
+ end
20
+
21
+ it "should delimit multiple words by hyphens" do
22
+ lexer = lexer_for("christmas-carol-thingy")
23
+ assert_equal({ :type => :text, :text => ["christmas", "-", "carol", "-", "thingy"] }, lexer.next)
24
+ end
25
+
26
+ it "should delimit multiple words by em-dashes" do
27
+ lexer = lexer_for("christmas—carol—thingy")
28
+ assert_equal({ :type => :text, :text => ["christmas", "—", "carol", "—", "thingy"] }, lexer.next)
29
+ end
30
+
31
+ it "should report nil as end-of-stream after scanning" do
32
+ lexer = lexer_for("a christmas carol by charles dickens")
33
+ assert_not_nil lexer.next
34
+ assert_nil lexer.next
35
+ assert_nil lexer.next
36
+ end
37
+
38
+ end
39
+
40
+ describe "when scanning XML entities" do
41
+
42
+ Prawn::Format::Lexer::ENTITY_MAP.each do |key, value|
43
+ it "should map #{key} to #{value}" do
44
+ lexer = lexer_for("&#{key};")
45
+ assert_equal({ :type => :text, :text => [value] }, lexer.next)
46
+ end
47
+ end
48
+
49
+ it "should convert decimal entities to utf-8" do
50
+ lexer = lexer_for('&#8212;')
51
+ assert_equal({ :type => :text, :text => ["\xe2\x80\x94"] }, lexer.next)
52
+ end
53
+
54
+ it "should convert hexadecimal entities to utf-8" do
55
+ lexer = lexer_for('&#x2014;')
56
+ assert_equal({ :type => :text, :text => ["\xe2\x80\x94"] }, lexer.next)
57
+ end
58
+
59
+ it "should raise InvalidFormat on unrecognized entities" do
60
+ lexer = lexer_for('&bogus;')
61
+ assert_raises(Prawn::Format::Lexer::InvalidFormat) do
62
+ lexer.next
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ describe "when scanning tags" do
69
+
70
+ it "should return tag with empty options for a tag with no options" do
71
+ lexer = lexer_for("<test>")
72
+ assert_equal({ :type => :open, :tag => :test, :options => {} }, lexer.next)
73
+ end
74
+
75
+ it "should scan tags with different delimiters consistently" do
76
+ lexer = lexer_for("<test first=january second=\"february\" third='march'>")
77
+ assert_equal({ :type => :open, :tag => :test, :options => { :first => "january", :second => "february", :third => "march" } }, lexer.next)
78
+ end
79
+
80
+ it "should scan closing tag" do
81
+ lexer = lexer_for("</test>")
82
+ assert_equal({ :type => :close, :tag => :test }, lexer.next)
83
+ end
84
+
85
+ it "should scan self-closing tag as two tokens" do
86
+ lexer = lexer_for("<test/>")
87
+ assert_equal({ :type => :open, :tag => :test, :options => {} }, lexer.next)
88
+ assert_equal({ :type => :close, :tag => :test }, lexer.next)
89
+ end
90
+
91
+ end
@@ -0,0 +1,103 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
4
+
5
+ describe "when parsing formatted text" do
6
+
7
+ before(:each) { create_pdf }
8
+
9
+ it "should raise TagError when it does not recognize a tag" do
10
+ parser = parser_for("<hello>")
11
+ assert_raises(Prawn::Format::Parser::TagError) { parser.next }
12
+ end
13
+
14
+ it "should raise TagError when it encounters an unmatched closing tag" do
15
+ parser = parser_for("</b>")
16
+ assert_raises(Prawn::Format::Parser::TagError) { parser.next }
17
+ end
18
+
19
+ it "should raise TagError when it a tag is closed with the wrong type" do
20
+ parser = parser_for("<b></i>")
21
+ assert_raises(Prawn::Format::Parser::TagError) { parse(parser) }
22
+ end
23
+
24
+ it "should apply styles defined for tag" do
25
+ parser = parser_for("a<b>c</b>d")
26
+ weights = [:normal, :bold, :bold, :bold, :normal]
27
+ assert_equal weights, parse(parser).map { |i| i.state.font_weight }
28
+ end
29
+
30
+ it "should honor custom tag styles" do
31
+ parser = parser_for("a<k>c</k>d", :tags => { :k => { :text_decoration => :underline } })
32
+ decorations = [:none, :underline, :underline, :underline, :none]
33
+ assert_equal decorations, parse(parser).map { |i| i.state.text_decoration }
34
+ end
35
+
36
+ it "should honor custom style classes" do
37
+ parser = parser_for("a<j class='test'>c</j>d", :tags => { :j => {} }, :styles => { :test => { :text_decoration => :underline } })
38
+ decorations = [:none, :underline, :underline, :underline, :none]
39
+ assert_equal decorations, parse(parser).map { |i| i.state.text_decoration }
40
+ end
41
+
42
+ it "should parse delimited text as separate instructions" do
43
+ parser = parser_for("a b-cd")
44
+ bits = ["a", " ", "b", "-", "cd"]
45
+ assert_equal bits, parse(parser).map { |i| i.text }
46
+ end
47
+
48
+ it "should return nil after last token is parsed" do
49
+ parser = parser_for("a")
50
+ assert_not_nil parser.next
51
+ assert_nil parser.next
52
+ assert_nil parser.next
53
+ end
54
+
55
+ it "should report eos? at end of stream" do
56
+ parser = parser_for("a")
57
+ assert !parser.eos?
58
+ parser.next
59
+ assert parser.eos?
60
+ end
61
+
62
+ it "should return next instruction without consuming it when peek is called" do
63
+ parser = parser_for("a")
64
+ assert_equal "a", parser.peek.text
65
+ assert !parser.eos?
66
+ assert_equal "a", parser.next.text
67
+ assert parser.eos?
68
+ end
69
+
70
+ it "should save instruction for next call when push is called" do
71
+ parser = parser_for("a")
72
+ k = parser.next
73
+ assert parser.eos?
74
+ parser.push(k)
75
+ assert !parser.eos?
76
+ assert_equal k, parser.next
77
+ assert parser.eos?
78
+ end
79
+
80
+ it "should map meta styles to styles on the tag" do
81
+ parser = parser_for("<k name='bob'>a</k>", :tags => { :k => { :meta => { :name => :__name__ } } })
82
+ i = parser.next
83
+ assert_equal "bob", i.tag[:style][:__name__]
84
+ end
85
+
86
+ private
87
+
88
+ def parser_for(text, opts={})
89
+ tags = @pdf.tags.merge(opts[:tags] || {})
90
+ styles = @pdf.styles.merge(opts[:styles] || {})
91
+ @parser = Prawn::Format::Parser.new(@pdf, text, opts.merge(:styles => styles, :tags => tags))
92
+ end
93
+
94
+ def parse(parser)
95
+ instructions = []
96
+
97
+ while instr = parser.next
98
+ instructions << instr
99
+ end
100
+
101
+ return instructions
102
+ end
103
+ end