prawn-markup 0.1.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 17db8afa6a062ac1cac8322276b7f013e2d9ba7c
4
- data.tar.gz: 03a009820c6851a0d06ca2812487757e1a142056
2
+ SHA256:
3
+ metadata.gz: fcd47aaef564239231b4546e4122f9f15625c15de5aa3d91bf0e3933e62d46ce
4
+ data.tar.gz: 77d0fa0b0b8261d6e3c0a8b7df75f797b31914c761c32dc229ba93edf269a587
5
5
  SHA512:
6
- metadata.gz: b9d20fb86722b5ade3804c648116b84e3d423b39a3ffcdeadd2340b6b5d886e1ba80a98fa0a570bf1b448d2e0f1a88058781ccf5330cb6f9c218e7c8467da770
7
- data.tar.gz: 8fdce13e68ce84ccbace42d051ffc5d66f3888ad9094af6aa79abc62a0251e386088ccc81e627dc0a3be67ef746c5d6e82575af4455f14121cf50ec3d89a54f6
6
+ metadata.gz: 395187b2fe33613d41bb9f4ac89f9b370cff0ac2a01212c96d5c88eea57e88a255e93a44e519f723b0955b468eb4896da06b7094ba3aa046302ab6733f3b8e6a
7
+ data.tar.gz: fd2c752f5eb61d1029371a4d2cd2476168476696d464853ba288ca263b6a8c903cb367a8f29dbc104f8e2d809b1f7cca02c779e863e9bd10385849be283d51db
data/README.md CHANGED
@@ -33,6 +33,27 @@ doc = Prawn::Document.new
33
33
  doc.markup('<p>Hello World</p><hr/><p>KTHXBYE</p>')
34
34
  ```
35
35
 
36
+ ## Supported HTML
37
+
38
+ This gem parses the given HTML and layouts the following elements in a vertical order:
39
+
40
+ * Text blocks: `p`, `div`, `ol`, `ul`, `li`, `hr`, `br`
41
+ * Text semantics: `a`, `b`, `strong`, `i`, `em`, `u`, `s`, `del`, `sub`, `sup`
42
+ * Headings: `h1`, `h2`, `h3`, `h4`, `h5`, `h6`
43
+ * Tables: `table`, `tr`, `td`, `th`
44
+ * Media: `img`, `iframe`
45
+ * Inputs: `type=checkbox`, `type=radio`
46
+
47
+ All other elements are ignored, their content is added to the parent element. With a few exceptions, no CSS is processed. One exception is the `width` property of `img`, `td` and `th`, which may contain values in `cm`, `mm`, `px`, `pt`, `%` or `auto`.
48
+
49
+ If no explicit loader is given (see above), images are loaded from `http(s)` addresses or may be contained in the `src` attribute as base64 encoded data URIs. Prawn only supports `PNG` and `JPG`.
50
+
51
+ ## Example
52
+
53
+ Have a look at [showcase.html](spec/fixtures/showcase.html), which is rendered by the corresponding [spec](spec/prawn/markup/showcase_spec.rb). Uncomment the `lookatit` call there to directly open the generated PDF when running the spec with `spec spec/prawn/markup/showcase_spec.rb`.
54
+
55
+ ## Formatting Options
56
+
36
57
  To customize element formatting, do:
37
58
 
38
59
  ```ruby
@@ -46,44 +67,48 @@ doc.markup_options = {
46
67
  doc.markup('<p>Hello World</p><hr/><p>KTHXBYE</p>', text: { align: :center })
47
68
  ```
48
69
 
49
- Options may be set for `text`, `table` (`cell` and `header`) and `list` (`content` and `bullet`).
70
+ Options may be set for `text`, `heading[1-6]`, `table` (subkeys `cell` and `header`) and `list` (subkeys `content` and `bullet`).
50
71
 
51
- Text options include all keys from Prawns [#text](http://prawnpdf.org/api-docs/2.0/Prawn/Text.html#text-instance_method) method: `font`, `size`, `color`, `style`, `align`, `valign`, `leading`,`direction`, `character_spacing`, `indent_paragraphs`, `kerning`, `mode`.
72
+ Text and heading options include all keys from Prawns [#text](http://prawnpdf.org/api-docs/2.0/Prawn/Text.html#text-instance_method) method: `font`, `size`, `color`, `style`, `align`, `valign`, `leading`,`direction`, `character_spacing`, `indent_paragraphs`, `kerning`, `mode`.
52
73
 
53
74
  Tables and lists are rendered with [prawn-table](https://github.com/prawnpdf/prawn-table) and have the following additional options: `padding`, `borders`, `border_width`, `border_color`, `background_color`, `border_lines`, `rotate`, `overflow`, `min_font_size`. Options from `text` may be overridden.
54
75
 
55
76
  Beside these options handled by Prawn / prawn-table, the following values may be customized:
56
77
 
57
- * `[:text][:preprocessor]`: A proc/callable that is called each time before a chunk of text is rendered.
58
- * `[:text][:margin_bottom]`: Margin after each `<p>`, `<ol>`, `<ul>` or `<table>`. Defaults to about half a line.
59
- * `[:heading1-6][:margin_top]`: Margin before a heading. Default is 0.
60
- * `[:heading1-6][:margin_bottom]`: Margin after a heading. Default is 0.
61
- * `[:table][:placeholder][:too_large]`: If the table content does not fit into the current bounding box, this text/callable is rendered instead. Defaults to '[table content too large]'.
62
- * `[:table][:placeholder][:subtable_too_large]`: If the content of a subtable cannot be fitted into the table, this text is rendered instead. Defaults to '[nested tables with automatic width are not supported]'.
63
- * `[:list][:vertical_margin]`: Margin at the top and the bottom of a list. Default is 5.
64
- * `[:list][:bullet][:char]`: The text used as bullet in unordered lists. Default is '•'.
65
- * `[:list][:bullet][:margin]`: Margin before the bullet. Default is 10.
66
- * `[:list][:content][:margin]`: Margin between the bullet and the content. Default is 10.
67
- * `[:list][:placeholder][:too_large]`: If the list content does not fit into the current bounding box, this text/callable is rendered instead. Defaults to '[list content too large]'.
68
- * `[:image][:loader]`: A callable that accepts the `src` attribute as an argument an returns a value understood by Prawn's `image` method. Loads `http(s)` URLs and base64 encoded data URIs by default.
69
- * `[:image][:placeholder]`: If an image is not supported, this text/callable is rendered instead. Defaults to '[unsupported image]'.
70
- * `[:iframe][:placeholder]`: If the HTML contains IFrames, this text/callable is rendered instead.
71
- A callable gets the URL of the IFrame as an argument. Defaults to ignore iframes.
72
-
73
- ## Supported HTML
74
-
75
- This gem parses the given HTML and layouts the following elements in a vertical order:
76
-
77
- * Text content: `p`, `div`, `ol`, `ul`, `li`, `hr`
78
- * Text semantics: `b`, `strong`, `i`, `em`, `u`, `a`, `br`
79
- * Headings: `h1`, `h2`, `h3`, `h4`, `h5`, `h6`
80
- * Tables: `table`, `tr`, `td`, `th`
81
- * Media: `img`, `iframe`
82
-
83
- All other elements are ignored, their content is added to the parent element. With a few exceptions, no CSS is processed. One exception is the `width` property of `img`, `td` and `th`, which may contain values in `cm`, `mm`, `px`, `pt`, `%` or `auto`.
84
-
85
- If no explicit loader is given (see above), images are loaded from `http(s)` addresses are may be contained in the `src` attribute as base64 encoded data URIs. Prawn only supports `PNG` and `JPG`.
86
-
78
+ * `:text`
79
+ * `:preprocessor`: A proc/callable that is called each time before a chunk of text is rendered.
80
+ * `:margin_bottom`: Margin after each `<p>`, `<ol>`, `<ul>` or `<table>`. Defaults to about half a line.
81
+ * `:heading1-6`
82
+ * `:margin_top`: Margin before a heading. Default is 0.
83
+ * `:margin_bottom`: Margin after a heading. Default is 0.
84
+ * `:table`
85
+ * `:placeholder`
86
+ * `:too_large`: If the table content does not fit into the current bounding box, this text/callable is rendered instead. Defaults to '[table content too large]'.
87
+ * `:subtable_too_large`: If the content of a subtable cannot be fitted into the table, this text is rendered instead. Defaults to '[nested tables with automatic width are not supported]'.
88
+ * `:list`
89
+ * `:vertical_margin`: Margin at the top and the bottom of a list. Default is 5.
90
+ * `:bullet`
91
+ * `:char`: The text used as bullet in unordered lists. Default is '•'.
92
+ * `:margin`: Margin before the bullet. Default is 10.
93
+ * `:content`
94
+ * `:margin`: Margin between the bullet and the content. Default is 10.
95
+ * `:placeholder`
96
+ * `:too_large`: If the list content does not fit into the current bounding box, this text/callable is rendered instead. Defaults to '[list content too large]'.
97
+ * `:image`
98
+ * `:loader`: A callable that accepts the `src` attribute as an argument an returns a value understood by Prawn's `image` method. Loads `http(s)` URLs and base64 encoded data URIs by default.
99
+ * `:placeholder`: If an image is not supported, this text/callable is rendered instead. Defaults to '[unsupported image]'.
100
+ * `:iframe`
101
+ * `:placeholder`: If the HTML contains IFrames, this text/callable is rendered instead.
102
+ A callable gets the URL of the IFrame as an argument. Defaults to ignore iframes.
103
+ * `:input`
104
+ * `:symbol_font`: A special font to print checkboxes and radios. Prawn's standard fonts do not support special unicode characters. Do not forget to update the document's `font_families`.
105
+ * `:symbol_font_size`: The size of the special font to print checkboxes and radios.
106
+ * `:checkbox`
107
+ * `:checked`: The char to print for a checked checkbox. Default is '☑'.
108
+ * `:unchecked`: The char to print for an unchecked checkbox. Default is '☐'.
109
+ * `:radio`
110
+ * `:checked`: The char to print for a checked radio. Default is '◉'.
111
+ * `:unchecked`: The char to print for an unchecked radio. Default is '○'.
87
112
 
88
113
  ## Development
89
114
 
data/lib/prawn/markup.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'prawn'
2
4
  require 'prawn/measurement_extensions'
3
5
  require 'prawn/table'
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Builders
4
6
  class ListBuilder < NestableBuilder
5
- BULLET_CHAR = '•'.freeze
7
+ BULLET_CHAR = '•'
6
8
  BULLET_MARGIN = 10
7
9
  CONTENT_MARGIN = 10
8
10
  VERTICAL_MARGIN = 5
@@ -22,7 +24,10 @@ module Prawn
22
24
  end
23
25
 
24
26
  def draw
25
- make(true).draw
27
+ # fix https://github.com/prawnpdf/prawn-table/issues/120
28
+ pdf.font_size(column_cell_style(:content)[:size] || pdf.font_size) do
29
+ make(true).draw
30
+ end
26
31
  end
27
32
 
28
33
  private
@@ -65,7 +70,7 @@ module Prawn
65
70
  data = item.nodes.map { |n| [normalize_list_item_node(n)] }
66
71
  style = column_cell_style(:content)
67
72
  .merge(borders: [], padding: [0, 0, padding_bottom, 0])
68
- pdf.make_table(data, cell_style: style) do
73
+ pdf.make_table(data, cell_style: style, column_widths: [content_width]) do
69
74
  rows(-1).padding = [0, 0, 0, 0]
70
75
  end
71
76
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Builders
@@ -47,10 +49,12 @@ module Prawn
47
49
  def extract_text_cell_style(hash)
48
50
  TEXT_STYLE_OPTIONS
49
51
  .each_with_object({}) { |key, h| h[key] = hash[key] }
50
- .tap do |options|
51
- options[:font_style] ||= options.delete(:style)
52
- options[:text_color] ||= options.delete(:color)
53
- end
52
+ .tap { |options| convert_style_options(options) }
53
+ end
54
+
55
+ def convert_style_options(hash)
56
+ hash[:font_style] ||= hash.delete(:style)
57
+ hash[:text_color] ||= hash.delete(:color)
54
58
  end
55
59
 
56
60
  def type_key(object)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Builders
@@ -20,7 +22,10 @@ module Prawn
20
22
  end
21
23
 
22
24
  def draw
23
- make.draw
25
+ # fix https://github.com/prawnpdf/prawn-table/issues/120
26
+ pdf.font_size(table_options[:cell][:size] || pdf.font_size) do
27
+ make.draw
28
+ end
24
29
  rescue Prawn::Errors::CannotFit => e
25
30
  if failover_on_error
26
31
  draw
@@ -169,7 +174,8 @@ module Prawn
169
174
 
170
175
  def distribute_remaing_width(count)
171
176
  equal_width = (total_width - column_width_sum) / count.to_f
172
- return if equal_width < 0
177
+ return if equal_width.negative?
178
+
173
179
  column_widths.map! { |width| width || equal_width }
174
180
  end
175
181
 
@@ -230,11 +236,6 @@ module Prawn
230
236
  convert_style_options(opts[:header])
231
237
  end
232
238
 
233
- def convert_style_options(hash)
234
- hash[:font_style] ||= hash.delete(:style)
235
- hash[:text_color] ||= hash.delete(:color)
236
- end
237
-
238
239
  def default_table_options
239
240
  {
240
241
  cell: {
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Elements
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Elements
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Elements
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Interface
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  # Processes known HTML tags. Unknown tags are ignored.
@@ -21,34 +23,38 @@ module Prawn
21
23
  self.logger = defined?(Rails) ? Rails.logger : nil
22
24
 
23
25
  require 'prawn/markup/processor/text'
26
+ require 'prawn/markup/processor/blocks'
24
27
  require 'prawn/markup/processor/headings'
25
28
  require 'prawn/markup/processor/images'
29
+ require 'prawn/markup/processor/inputs'
26
30
  require 'prawn/markup/processor/tables'
27
31
  require 'prawn/markup/processor/lists'
28
32
 
29
33
  prepend Prawn::Markup::Processor::Text
34
+ prepend Prawn::Markup::Processor::Blocks
30
35
  prepend Prawn::Markup::Processor::Headings
31
36
  prepend Prawn::Markup::Processor::Images
37
+ prepend Prawn::Markup::Processor::Inputs
32
38
  prepend Prawn::Markup::Processor::Tables
33
39
  prepend Prawn::Markup::Processor::Lists
34
40
 
35
41
  def initialize(pdf, options = {})
42
+ super()
36
43
  @pdf = pdf
37
44
  @options = options
38
45
  end
39
46
 
40
47
  def parse(html)
41
48
  return if html.to_s.strip.empty?
49
+
42
50
  reset
43
51
  html = Prawn::Markup::Normalizer.new(html).normalize
44
52
  Nokogiri::HTML::SAX::Parser.new(self).parse(html) { |ctx| ctx.recovery = true }
45
53
  end
46
54
 
47
55
  def start_element(name, attrs = [])
48
- stack.push(name: name, attrs: Hash[attrs])
49
- if self.class.known_elements.include?(name)
50
- send("start_#{name}") if respond_to?("start_#{name}", true)
51
- end
56
+ stack.push(name: name, attrs: attrs.to_h)
57
+ send("start_#{name}") if known_element?(name) && respond_to?("start_#{name}", true)
52
58
  end
53
59
 
54
60
  def end_element(name)
@@ -62,11 +68,11 @@ module Prawn
62
68
  end
63
69
 
64
70
  def error(string)
65
- logger.debug('SAX parsing error: ' + string) if logger
71
+ logger.info("SAX parsing error: #{string.strip}") if logger
66
72
  end
67
73
 
68
74
  def warning(string)
69
- logger.warn(string) if logger
75
+ logger.info("SAX parsing warning: #{string.strip}") if logger
70
76
  end
71
77
 
72
78
  private
@@ -75,7 +81,11 @@ module Prawn
75
81
 
76
82
  def reset
77
83
  @stack = []
78
- @text_buffer = ''
84
+ @text_buffer = +''
85
+ end
86
+
87
+ def known_element?(name)
88
+ self.class.known_elements.include?(name)
79
89
  end
80
90
 
81
91
  def append_text(string)
@@ -115,15 +125,14 @@ module Prawn
115
125
  def style_properties
116
126
  style = current_attrs['style']
117
127
  if style
118
- tokens = style.split(';').map { |p| p.split(':', 2).map(&:strip) }
119
- Hash[tokens]
128
+ style.split(';').map { |p| p.split(':', 2).map(&:strip) }.to_h
120
129
  else
121
130
  {}
122
131
  end
123
132
  end
124
133
 
125
134
  def placeholder_value(keys, *args)
126
- placeholder = dig_options(keys)
135
+ placeholder = dig_options(*keys)
127
136
  return if placeholder.nil?
128
137
 
129
138
  if placeholder.respond_to?(:call)
@@ -133,7 +142,7 @@ module Prawn
133
142
  end
134
143
  end
135
144
 
136
- def dig_options(keys)
145
+ def dig_options(*keys)
137
146
  keys.inject(options) { |opts, key| opts ? opts[key] : nil }
138
147
  end
139
148
 
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module Markup
5
+ module Processor::Blocks
6
+ def self.prepended(base)
7
+ base.known_elements.push('p', 'br', 'div', 'hr')
8
+ end
9
+
10
+ def start_br
11
+ append_text("\n")
12
+ end
13
+
14
+ def start_p
15
+ handle_text_element
16
+ end
17
+
18
+ def end_p
19
+ if inside_container?
20
+ append_new_line
21
+ append_text("\n")
22
+ else
23
+ add_paragraph
24
+ end
25
+ end
26
+
27
+ def start_div
28
+ handle_text_element
29
+ end
30
+
31
+ def end_div
32
+ handle_text_element
33
+ end
34
+
35
+ def start_hr
36
+ return if inside_container?
37
+
38
+ put_bottom_margin(nil)
39
+ add_current_text
40
+ pdf.move_down(hr_vertical_margin_top)
41
+ pdf.stroke_horizontal_rule
42
+ pdf.move_down(hr_vertical_margin_bottom)
43
+ end
44
+
45
+ def end_document
46
+ add_current_text
47
+ end
48
+
49
+ private
50
+
51
+ def handle_text_element
52
+ if inside_container?
53
+ append_new_line
54
+ else
55
+ add_current_text
56
+ end
57
+ end
58
+
59
+ def append_new_line
60
+ append_text("\n") if buffered_text? && text_buffer[-1] != "\n"
61
+ end
62
+
63
+ def add_paragraph
64
+ text = dump_text
65
+ text.gsub!(/[^\n]/, '') if text.strip.empty?
66
+ unless text.empty?
67
+ add_bottom_margin
68
+ add_formatted_text(text, text_options)
69
+ put_bottom_margin(text_margin_bottom)
70
+ end
71
+ end
72
+
73
+ def add_current_text(options = text_options)
74
+ add_bottom_margin
75
+ return unless buffered_text?
76
+
77
+ string = dump_text
78
+ string.strip!
79
+ add_formatted_text(string, options)
80
+ end
81
+
82
+ def add_bottom_margin
83
+ if @bottom_margin
84
+ pdf.move_down(@bottom_margin)
85
+ @bottom_margin = nil
86
+ end
87
+ end
88
+
89
+ def add_formatted_text(string, options = text_options)
90
+ with_font(options) do
91
+ pdf.text(string, options)
92
+ end
93
+ end
94
+
95
+ def with_font(options)
96
+ pdf.font(options[:font] || pdf.font.family,
97
+ size: options[:size],
98
+ style: options[:style]) do
99
+ return yield
100
+ end
101
+ end
102
+
103
+ def hr_vertical_margin_top
104
+ @hr_vertical_margin_top ||=
105
+ (text_options[:size] || pdf.font_size) / 2.0
106
+ end
107
+
108
+ def hr_vertical_margin_bottom
109
+ @hr_vertical_margin_bottom ||= with_font(text_options) do
110
+ hr_vertical_margin_top +
111
+ pdf.font.descender +
112
+ text_leading -
113
+ pdf.line_width
114
+ end
115
+ end
116
+
117
+ def reset
118
+ super
119
+ text_margin_bottom # pre-calculate
120
+ end
121
+
122
+ def text_margin_bottom
123
+ options[:text] ||= {}
124
+ options[:text][:margin_bottom] ||= default_text_margin_bottom
125
+ end
126
+
127
+ def default_text_margin_bottom
128
+ text_line_gap +
129
+ text_descender +
130
+ text_leading
131
+ end
132
+
133
+ def text_line_gap
134
+ @text_line_gap ||= with_font(text_options) { pdf.font.line_gap }
135
+ end
136
+
137
+ def text_descender
138
+ @text_descender ||= with_font(text_options) { pdf.font.descender }
139
+ end
140
+
141
+ def text_leading
142
+ text_options[:leading] || pdf.default_leading
143
+ end
144
+
145
+ def text_options
146
+ @text_options ||= HashMerger.deep(default_text_options, options[:text] || {})
147
+ end
148
+
149
+ def default_text_options
150
+ {
151
+ inline_format: true
152
+ }
153
+ end
154
+ end
155
+ end
156
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Processor::Headings
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'open-uri'
2
4
 
3
5
  module Prawn
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module Markup
5
+ module Processor::Inputs
6
+
7
+ DEFAULT_CHECKABLE_CHARS = {
8
+ checkbox: {
9
+ checked: '☑',
10
+ unchecked: '☐'
11
+ },
12
+ radio: {
13
+ checked: '◉',
14
+ unchecked: '○'
15
+ }
16
+ }.freeze
17
+
18
+ def self.prepended(base)
19
+ base.known_elements.push('input')
20
+ end
21
+
22
+ def start_input
23
+ type = current_attrs['type'].to_sym
24
+ if DEFAULT_CHECKABLE_CHARS.keys.include?(type)
25
+ append_checked_symbol(type)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def append_checked_symbol(type)
32
+ char = checkable_symbol(type)
33
+ append_text(build_font_tag(char))
34
+ end
35
+
36
+ def checkable_symbol(type)
37
+ state = current_attrs.key?('checked') ? :checked : :unchecked
38
+ dig_options(:input, type, state) || DEFAULT_CHECKABLE_CHARS[type][state]
39
+ end
40
+
41
+ def symbol_font_options
42
+ @symbol_font_options ||= {
43
+ name: dig_options(:input, :symbol_font),
44
+ size: dig_options(:input, :symbol_font_size)
45
+ }.compact
46
+ end
47
+
48
+ def build_font_tag(content)
49
+ return content if symbol_font_options.empty?
50
+
51
+ out = +'<font'
52
+ symbol_font_options.each do |key, value|
53
+ out << " #{key}=\"#{value}\""
54
+ end
55
+ out << '>'
56
+ out << content
57
+ out << '</font>'
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Processor::Lists
@@ -32,10 +34,14 @@ module Prawn
32
34
  alias end_ul end_list
33
35
 
34
36
  def start_li
37
+ return unless inside_container?
38
+
35
39
  current_list.items << Elements::Item.new
36
40
  end
37
41
 
38
42
  def end_li
43
+ return unless inside_container?
44
+
39
45
  add_cell_text_node(current_list_item)
40
46
  end
41
47
 
@@ -61,7 +67,9 @@ module Prawn
61
67
  end
62
68
 
63
69
  def current_list_item
64
- current_list.items.last
70
+ items = current_list.items
71
+ items << Elements::Item.new if items.empty?
72
+ items.last
65
73
  end
66
74
 
67
75
  def inside_container?
@@ -81,12 +89,17 @@ module Prawn
81
89
  end
82
90
 
83
91
  def add_list(list)
84
- Builders::ListBuilder.new(pdf, list, pdf.bounds.width, options).draw
85
- put_bottom_margin(text_margin_bottom)
92
+ pdf.move_up(additional_cell_padding_top)
93
+ draw_list(list)
94
+ put_bottom_margin(text_margin_bottom + additional_cell_padding_top)
86
95
  rescue Prawn::Errors::CannotFit => e
87
96
  append_text(list_too_large_placeholder(e))
88
97
  end
89
98
 
99
+ def draw_list(list)
100
+ Builders::ListBuilder.new(pdf, list, pdf.bounds.width, options).draw
101
+ end
102
+
90
103
  def list_too_large_placeholder(error)
91
104
  placeholder_value(%i[list placeholder too_large], error) || '[list content too large]'
92
105
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Processor::Tables
@@ -26,19 +28,29 @@ module Prawn
26
28
  end
27
29
 
28
30
  def start_tr
31
+ return unless current_table
32
+
29
33
  current_table << []
30
34
  end
31
35
 
32
36
  def start_td
37
+ return unless current_table
38
+
33
39
  current_table.last << Elements::Cell.new(width: style_properties['width'])
34
40
  end
35
41
 
36
42
  def start_th
43
+ return unless current_table
44
+
37
45
  current_table.last << Elements::Cell.new(width: style_properties['width'], header: true)
38
46
  end
39
47
 
40
48
  def end_td
41
- add_cell_text_node(current_cell)
49
+ if current_table
50
+ add_cell_text_node(current_cell)
51
+ else
52
+ add_current_text
53
+ end
42
54
  end
43
55
  alias end_th end_td
44
56
 
@@ -73,6 +85,7 @@ module Prawn
73
85
 
74
86
  def add_cell_text_node(cell, options = {})
75
87
  return unless buffered_text?
88
+
76
89
  cell.nodes << options.merge(content: dump_text.strip)
77
90
  end
78
91
 
@@ -83,15 +96,24 @@ module Prawn
83
96
  end
84
97
 
85
98
  def add_table(cells)
86
- Builders::TableBuilder.new(pdf, cells, pdf.bounds.width, options).draw
87
- put_bottom_margin(text_margin_bottom)
99
+ draw_table(cells)
100
+ put_bottom_margin(text_margin_bottom + additional_cell_padding_top + text_leading)
88
101
  rescue Prawn::Errors::CannotFit => e
89
102
  append_text(table_too_large_placeholder(e))
90
103
  end
91
104
 
105
+ def draw_table(cells)
106
+ Builders::TableBuilder.new(pdf, cells, pdf.bounds.width, options).draw
107
+ end
108
+
92
109
  def table_too_large_placeholder(error)
93
110
  placeholder_value(%i[table placeholder too_large], error) || '[table content too large]'
94
111
  end
112
+
113
+ def additional_cell_padding_top
114
+ # as used in Prawn::Table::Cell::Text#draw_content move_down
115
+ (text_line_gap + text_descender) / 2
116
+ end
95
117
  end
96
118
  end
97
119
  end
@@ -1,42 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module Processor::Text
4
6
  def self.prepended(base)
5
- base.known_elements.push('p', 'br', 'div', 'b', 'strong', 'i', 'em', 'u', 'a', 'hr')
6
- end
7
-
8
- def start_br
9
- append_text("\n")
10
- end
11
-
12
- def start_p
13
- handle_text_element
14
- end
15
-
16
- def end_p
17
- if inside_container?
18
- append_new_line
19
- append_text("\n")
20
- else
21
- add_paragraph
22
- end
23
- end
24
-
25
- def start_div
26
- handle_text_element
27
- end
28
-
29
- def end_div
30
- handle_text_element
7
+ base.known_elements.push(
8
+ 'a', 'b', 'strong', 'i', 'em', 'u', 'strikethrough', 'strike', 's', 'del',
9
+ 'sub', 'sup'
10
+ )
31
11
  end
32
12
 
33
13
  def start_a
34
14
  append_text("<link href=\"#{current_attrs['href']}\">")
35
15
  end
16
+ alias start_link start_a
36
17
 
37
18
  def end_a
38
19
  append_text('</link>')
39
20
  end
21
+ alias end_link end_a
40
22
 
41
23
  def start_b
42
24
  append_text('<b>')
@@ -58,119 +40,44 @@ module Prawn
58
40
  end
59
41
  alias end_em end_i
60
42
 
61
- def start_hr
62
- return if inside_container?
63
-
64
- put_bottom_margin(nil)
65
- add_current_text
66
- pdf.move_down(hr_vertical_margin_top)
67
- pdf.stroke_horizontal_rule
68
- pdf.move_down(hr_vertical_margin_bottom)
43
+ def start_u
44
+ append_text('<u>')
69
45
  end
70
46
 
71
- def end_document
72
- add_current_text
47
+ def end_u
48
+ append_text('</u>')
73
49
  end
74
50
 
75
- private
76
-
77
- def handle_text_element
78
- if inside_container?
79
- append_new_line
80
- else
81
- add_current_text
82
- end
83
- end
84
-
85
- def append_new_line
86
- append_text("\n") if buffered_text? && text_buffer[-1] != "\n"
51
+ def start_strikethrough
52
+ append_text('<strikethrough>')
87
53
  end
54
+ alias start_s start_strikethrough
55
+ alias start_strike start_strikethrough
56
+ alias start_del start_strikethrough
88
57
 
89
- def add_paragraph
90
- text = dump_text
91
- text.gsub!(/[^\n]/, '') if text.strip.empty?
92
- unless text.empty?
93
- add_bottom_margin
94
- add_formatted_text(text, text_options)
95
- put_bottom_margin(text_margin_bottom)
96
- end
58
+ def end_strikethrough
59
+ append_text('</strikethrough>')
97
60
  end
61
+ alias end_s end_strikethrough
62
+ alias end_strike end_strikethrough
63
+ alias end_del end_strikethrough
98
64
 
99
- def add_current_text(options = text_options)
100
- add_bottom_margin
101
- return unless buffered_text?
102
-
103
- string = dump_text
104
- string.strip!
105
- add_formatted_text(string, options)
65
+ def start_sub
66
+ append_text('<sub>')
106
67
  end
107
68
 
108
- def add_bottom_margin
109
- if @bottom_margin
110
- pdf.move_down(@bottom_margin)
111
- @bottom_margin = nil
112
- end
69
+ def end_sub
70
+ append_text('</sub>')
113
71
  end
114
72
 
115
- def add_formatted_text(string, options = text_options)
116
- with_font(options) do
117
- pdf.text(string, options)
118
- end
73
+ def start_sup
74
+ append_text('<sup>')
119
75
  end
120
76
 
121
- def with_font(options)
122
- pdf.font(options[:font] || pdf.font.family,
123
- size: options[:size],
124
- style: options[:style]) do
125
- return yield
126
- end
77
+ def end_sup
78
+ append_text('</sup>')
127
79
  end
128
80
 
129
- def hr_vertical_margin_top
130
- @hr_vertical_margin_top ||=
131
- (text_options[:size] || pdf.font_size) / 2.0
132
- end
133
-
134
- def hr_vertical_margin_bottom
135
- @hr_vertical_margin_bottom ||= with_font(text_options) do
136
- hr_vertical_margin_top +
137
- pdf.font.descender +
138
- text_leading -
139
- pdf.line_width
140
- end
141
- end
142
-
143
- def reset
144
- super
145
- text_margin_bottom # pre-calculate
146
- end
147
-
148
- def text_margin_bottom
149
- options[:text] ||= {}
150
- options[:text][:margin_bottom] ||= default_text_margin_bottom
151
- end
152
-
153
- def default_text_margin_bottom
154
- with_font(text_options) do
155
- pdf.font.line_gap +
156
- pdf.font.descender +
157
- text_leading
158
- end
159
- end
160
-
161
- def text_leading
162
- text_options[:leading] || pdf.default_leading
163
- end
164
-
165
- def text_options
166
- @text_options ||= HashMerger.deep(default_text_options, options[:text] || {})
167
- end
168
-
169
- def default_text_options
170
- {
171
- inline_format: true
172
- }
173
- end
174
81
  end
175
82
  end
176
83
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  module HashMerger
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  # Normalizes HTML markup:
@@ -22,7 +24,7 @@ module Prawn
22
24
  close_self_closing_elements
23
25
  normalize_spaces
24
26
  replace_html_entities
25
- "<root>#{html}</root>"
27
+ "<body>#{html}</body>"
26
28
  end
27
29
 
28
30
  private
@@ -43,6 +45,7 @@ module Prawn
43
45
  html.gsub!(/&#{entity};/, string)
44
46
  end
45
47
  end
48
+
46
49
  end
47
50
  end
48
51
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
5
  class SizeConverter
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Prawn
2
4
  module Markup
3
- VERSION = '0.1.0'.freeze
5
+ VERSION = '0.3.1'
4
6
  end
5
7
  end
data/prawn-markup.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.license = 'MIT'
18
18
 
19
19
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
- f.match(%r{^(test|spec|features)/})
20
+ f.match(%r{^(((spec|bin)/)|\.|Gemfile|Rakefile)})
21
21
  end
22
22
  spec.bindir = 'exe'
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prawn-markup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pascal Zumkehr
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-09-10 00:00:00.000000000 Z
11
+ date: 2021-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -159,18 +159,9 @@ executables: []
159
159
  extensions: []
160
160
  extra_rdoc_files: []
161
161
  files:
162
- - ".gitignore"
163
- - ".rspec"
164
- - ".rubocop.yml"
165
- - ".travis.yml"
166
162
  - CODE_OF_CONDUCT.md
167
- - Gemfile
168
- - Gemfile.lock
169
163
  - LICENSE.txt
170
164
  - README.md
171
- - Rakefile
172
- - bin/console
173
- - bin/setup
174
165
  - lib/prawn/markup.rb
175
166
  - lib/prawn/markup/builders/list_builder.rb
176
167
  - lib/prawn/markup/builders/nestable_builder.rb
@@ -180,8 +171,10 @@ files:
180
171
  - lib/prawn/markup/elements/list.rb
181
172
  - lib/prawn/markup/interface.rb
182
173
  - lib/prawn/markup/processor.rb
174
+ - lib/prawn/markup/processor/blocks.rb
183
175
  - lib/prawn/markup/processor/headings.rb
184
176
  - lib/prawn/markup/processor/images.rb
177
+ - lib/prawn/markup/processor/inputs.rb
185
178
  - lib/prawn/markup/processor/lists.rb
186
179
  - lib/prawn/markup/processor/tables.rb
187
180
  - lib/prawn/markup/processor/text.rb
@@ -194,7 +187,7 @@ homepage: https://github.com/puzzle/prawn-markup
194
187
  licenses:
195
188
  - MIT
196
189
  metadata: {}
197
- post_install_message:
190
+ post_install_message:
198
191
  rdoc_options: []
199
192
  require_paths:
200
193
  - lib
@@ -209,9 +202,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
209
202
  - !ruby/object:Gem::Version
210
203
  version: '0'
211
204
  requirements: []
212
- rubyforge_project:
213
- rubygems_version: 2.4.8
214
- signing_key:
205
+ rubygems_version: 3.0.6
206
+ signing_key:
215
207
  specification_version: 4
216
208
  summary: Parse simple HTML markup to include in Prawn PDFs
217
209
  test_files: []
data/.gitignore DELETED
@@ -1,12 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /spec/coverage/
9
- /tmp/
10
-
11
- # rspec failure tracking
12
- .rspec_status
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,64 +0,0 @@
1
- AllCops:
2
- DisplayCopNames: true
3
- Exclude:
4
- - '*.gemspec'
5
- - spec/**/*
6
- - vendor/**/*
7
-
8
- Metrics/AbcSize:
9
- Severity: error
10
-
11
- Metrics/ClassLength:
12
- Max: 250
13
- Severity: error
14
-
15
- Metrics/CyclomaticComplexity:
16
- Severity: error
17
-
18
- Metrics/LineLength:
19
- Max: 100
20
- Severity: error
21
-
22
- Metrics/MethodLength:
23
- Max: 10
24
- Severity: error
25
-
26
- Metrics/ModuleLength:
27
- Max: 150
28
- Severity: error
29
-
30
- Metrics/ParameterLists:
31
- Max: 4
32
- Severity: warning
33
-
34
- # Keep for now, easier with superclass definitions
35
- ClassAndModuleChildren:
36
- Enabled: false
37
-
38
- # The ones we use must exist for the entire class hierarchy.
39
- ClassVars:
40
- Enabled: false
41
-
42
- # Well, well, well
43
- Documentation:
44
- Enabled: false
45
-
46
- # Keep single line bodys for if and unless
47
- IfUnlessModifier:
48
- Enabled: false
49
-
50
- # We thinks that's fine for specs
51
- Layout/EmptyLinesAroundBlockBody:
52
- Enabled: false
53
-
54
- # We thinks that's fine
55
- Layout/EmptyLinesAroundClassBody:
56
- Enabled: false
57
-
58
- # We thinks that's fine
59
- Layout/EmptyLinesAroundModuleBody:
60
- Enabled: false
61
-
62
- # We think that's the developers choice
63
- Style/GuardClause:
64
- Enabled: false
data/.travis.yml DELETED
@@ -1,25 +0,0 @@
1
- sudo: false
2
-
3
- language: ruby
4
-
5
- rvm:
6
- - 2.5.1
7
-
8
- env:
9
- global:
10
- - CC_TEST_REPORTER_ID=d70fe39ffb8f2f7c5f837f0133d10a3a1c6784d0dd189153111577b60f74c603
11
-
12
- before_install: gem install bundler
13
-
14
- before_script:
15
- - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
16
- - chmod +x ./cc-test-reporter
17
- - ./cc-test-reporter before-build
18
-
19
- script:
20
- - bundle exec rake
21
- - bundle exec rubocop
22
-
23
- after_script:
24
- - ./cc-test-reporter format-coverage -t simplecov -o spec/coverage/codeclimate.json spec/coverage/.resultset.json
25
- - if [[ "$TRAVIS_TEST_RESULT" == 0 ]]; then ./cc-test-reporter upload-coverage -i spec/coverage/codeclimate.json; fi
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in prawn-markup.gemspec
4
- gemspec
data/Gemfile.lock DELETED
@@ -1,89 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- prawn-markup (0.1.0)
5
- nokogiri
6
- prawn
7
- prawn-table
8
-
9
- GEM
10
- remote: https://rubygems.org/
11
- specs:
12
- Ascii85 (1.0.3)
13
- afm (0.2.2)
14
- ast (2.4.0)
15
- byebug (10.0.2)
16
- diff-lcs (1.3)
17
- docile (1.3.1)
18
- hashery (2.1.2)
19
- jaro_winkler (1.5.1)
20
- json (2.1.0)
21
- mini_portile2 (2.3.0)
22
- nokogiri (1.8.4)
23
- mini_portile2 (~> 2.3.0)
24
- parallel (1.12.1)
25
- parser (2.5.1.2)
26
- ast (~> 2.4.0)
27
- pdf-core (0.7.0)
28
- pdf-inspector (1.3.0)
29
- pdf-reader (>= 1.0, < 3.0.a)
30
- pdf-reader (2.1.0)
31
- Ascii85 (~> 1.0.0)
32
- afm (~> 0.2.1)
33
- hashery (~> 2.0)
34
- ruby-rc4
35
- ttfunk
36
- powerpack (0.1.2)
37
- prawn (2.2.2)
38
- pdf-core (~> 0.7.0)
39
- ttfunk (~> 1.5)
40
- prawn-table (0.2.2)
41
- prawn (>= 1.3.0, < 3.0.0)
42
- rainbow (3.0.0)
43
- rake (12.3.1)
44
- rspec (3.8.0)
45
- rspec-core (~> 3.8.0)
46
- rspec-expectations (~> 3.8.0)
47
- rspec-mocks (~> 3.8.0)
48
- rspec-core (3.8.0)
49
- rspec-support (~> 3.8.0)
50
- rspec-expectations (3.8.1)
51
- diff-lcs (>= 1.2.0, < 2.0)
52
- rspec-support (~> 3.8.0)
53
- rspec-mocks (3.8.0)
54
- diff-lcs (>= 1.2.0, < 2.0)
55
- rspec-support (~> 3.8.0)
56
- rspec-support (3.8.0)
57
- rubocop (0.58.2)
58
- jaro_winkler (~> 1.5.1)
59
- parallel (~> 1.10)
60
- parser (>= 2.5, != 2.5.1.1)
61
- powerpack (~> 0.1)
62
- rainbow (>= 2.2.2, < 4.0)
63
- ruby-progressbar (~> 1.7)
64
- unicode-display_width (~> 1.0, >= 1.0.1)
65
- ruby-progressbar (1.10.0)
66
- ruby-rc4 (0.1.5)
67
- simplecov (0.16.1)
68
- docile (~> 1.1)
69
- json (>= 1.8, < 3)
70
- simplecov-html (~> 0.10.0)
71
- simplecov-html (0.10.2)
72
- ttfunk (1.5.1)
73
- unicode-display_width (1.4.0)
74
-
75
- PLATFORMS
76
- ruby
77
-
78
- DEPENDENCIES
79
- bundler
80
- byebug
81
- pdf-inspector
82
- prawn-markup!
83
- rake
84
- rspec
85
- rubocop
86
- simplecov
87
-
88
- BUNDLED WITH
89
- 1.16.2
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require 'bundler/gem_tasks'
2
- require 'rspec/core/rake_task'
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task default: :spec
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'bundler/setup'
4
- require 'prawn/markup'
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require 'irb'
14
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here