prawn-table-html 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +236 -0
  4. data/lib/prawn-html.rb +180 -0
  5. data/lib/prawn_html/attributes.rb +166 -0
  6. data/lib/prawn_html/callbacks/background.rb +19 -0
  7. data/lib/prawn_html/callbacks/strike_through.rb +18 -0
  8. data/lib/prawn_html/context.rb +100 -0
  9. data/lib/prawn_html/document_renderer.rb +172 -0
  10. data/lib/prawn_html/html_parser.rb +104 -0
  11. data/lib/prawn_html/instance.rb +18 -0
  12. data/lib/prawn_html/pdf_wrapper.rb +145 -0
  13. data/lib/prawn_html/tag.rb +93 -0
  14. data/lib/prawn_html/tags/a.rb +20 -0
  15. data/lib/prawn_html/tags/b.rb +13 -0
  16. data/lib/prawn_html/tags/blockquote.rb +25 -0
  17. data/lib/prawn_html/tags/body.rb +13 -0
  18. data/lib/prawn_html/tags/br.rb +21 -0
  19. data/lib/prawn_html/tags/code.rb +13 -0
  20. data/lib/prawn_html/tags/col.rb +37 -0
  21. data/lib/prawn_html/tags/colgroup.rb +13 -0
  22. data/lib/prawn_html/tags/del.rb +13 -0
  23. data/lib/prawn_html/tags/div.rb +13 -0
  24. data/lib/prawn_html/tags/h.rb +49 -0
  25. data/lib/prawn_html/tags/hr.rb +39 -0
  26. data/lib/prawn_html/tags/i.rb +13 -0
  27. data/lib/prawn_html/tags/img.rb +31 -0
  28. data/lib/prawn_html/tags/li.rb +39 -0
  29. data/lib/prawn_html/tags/mark.rb +13 -0
  30. data/lib/prawn_html/tags/ol.rb +43 -0
  31. data/lib/prawn_html/tags/p.rb +23 -0
  32. data/lib/prawn_html/tags/pre.rb +25 -0
  33. data/lib/prawn_html/tags/small.rb +15 -0
  34. data/lib/prawn_html/tags/span.rb +9 -0
  35. data/lib/prawn_html/tags/sub.rb +13 -0
  36. data/lib/prawn_html/tags/sup.rb +13 -0
  37. data/lib/prawn_html/tags/table.rb +53 -0
  38. data/lib/prawn_html/tags/tbody.rb +13 -0
  39. data/lib/prawn_html/tags/td.rb +43 -0
  40. data/lib/prawn_html/tags/th.rb +43 -0
  41. data/lib/prawn_html/tags/tr.rb +37 -0
  42. data/lib/prawn_html/tags/u.rb +13 -0
  43. data/lib/prawn_html/tags/ul.rb +40 -0
  44. data/lib/prawn_html/utils.rb +139 -0
  45. data/lib/prawn_html/version.rb +5 -0
  46. metadata +131 -0
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ class Context < Array
5
+ DEFAULT_STYLES = {
6
+ size: 16 * PX
7
+ }.freeze
8
+
9
+ attr_reader :previous_tag
10
+ attr_accessor :last_text_node
11
+
12
+ # Init the Context
13
+ def initialize(*_args)
14
+ super
15
+ @last_text_node = false
16
+ @merged_styles = nil
17
+ @previous_tag = nil
18
+ end
19
+
20
+ # Add an element to the context
21
+ #
22
+ # Set the parent for the previous element in the chain.
23
+ # Run `on_context_add` callback method on the added element.
24
+ #
25
+ # @param element [Tag] the element to add
26
+ #
27
+ # @return [Context] the context updated
28
+ def add(element)
29
+ element.parent = last
30
+ push(element)
31
+ element.on_context_add(self) if element.respond_to?(:on_context_add)
32
+ @merged_styles = nil
33
+ self
34
+ end
35
+
36
+ # Evaluate before content
37
+ #
38
+ # @return [String] before content string
39
+ def before_content
40
+ (last.respond_to?(:before_content) && last.before_content) || ''
41
+ end
42
+
43
+ # Merges the context block styles
44
+ #
45
+ # @return [Hash] the hash of merged styles
46
+ def block_styles
47
+ each_with_object({}) do |element, res|
48
+ element.block_styles.each do |key, value|
49
+ Attributes.merge_attr!(res, key, value)
50
+ end
51
+ end
52
+ end
53
+
54
+ # Merge the context styles for text nodes
55
+ #
56
+ # @return [Hash] the hash of merged styles
57
+ def merged_styles
58
+ @merged_styles ||=
59
+ each_with_object(DEFAULT_STYLES.dup) do |element, res|
60
+ evaluate_element_styles(element, res)
61
+ element.update_styles(res)
62
+ end
63
+ end
64
+
65
+ # :nocov:
66
+ def inspect
67
+ map(&:class).map(&:to_s).join(', ')
68
+ end
69
+ # :nocov:
70
+
71
+ # Remove the last element from the context
72
+ def remove_last
73
+ last.on_context_remove(self) if last.respond_to?(:on_context_remove)
74
+ @merged_styles = nil
75
+ @last_text_node = false
76
+ @previous_tag = last
77
+ pop
78
+ end
79
+
80
+ # White space is equal to 'pre'?
81
+ #
82
+ # @return [boolean] white space property of the last element is equal to 'pre'
83
+ def white_space_pre?
84
+ last && last.styles[:white_space] == :pre
85
+ end
86
+
87
+ private
88
+
89
+ def evaluate_element_styles(element, res)
90
+ styles = element.styles.slice(*Attributes::STYLES_APPLY[:text_node])
91
+ styles.each do |key, val|
92
+ if res.include?(key) && res[key].is_a?(Array)
93
+ res[key] += val
94
+ else
95
+ res[key] = val
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ class DocumentRenderer
5
+ NEW_LINE = { text: "\n" }.freeze
6
+ SPACE = { text: ' ' }.freeze
7
+
8
+ # Init the DocumentRenderer
9
+ #
10
+ # @param pdf [PdfWrapper] target PDF wrapper
11
+ def initialize(pdf)
12
+ @before_content = []
13
+ @buffer = []
14
+ @context = Context.new
15
+ @last_margin = 0
16
+ @last_text = ''
17
+ @last_tag_open = false
18
+ @pdf = pdf
19
+ end
20
+
21
+ # On tag close callback
22
+ #
23
+ # @param element [Tag] closing element wrapper
24
+ def on_tag_close(element)
25
+ render_if_needed(element)
26
+ apply_tag_close_styles(element)
27
+ context.remove_last
28
+ @last_tag_open = false
29
+ @last_text = ''
30
+ end
31
+
32
+ # On tag open callback
33
+ #
34
+ # @param tag_name [String] the tag name of the opening element
35
+ # @param attributes [Hash] an hash of the element attributes
36
+ # @param element_styles [String] document styles to apply to the element
37
+ #
38
+ # @return [Tag] the opening element wrapper
39
+ def on_tag_open(tag_name, attributes:, element_styles: '')
40
+ tag_class = Tag.class_for(tag_name)
41
+ return unless tag_class
42
+
43
+ options = { width: pdf.page_width, height: pdf.page_height }
44
+ tag_class.new(tag_name, attributes: attributes, options: options).tap do |element|
45
+ setup_element(element, element_styles: element_styles)
46
+ @before_content.push(element.before_content) if element.respond_to?(:before_content)
47
+ @last_tag_open = true
48
+ end
49
+ end
50
+
51
+ # On text node callback
52
+ #
53
+ # @param content [String] the text node content
54
+ #
55
+ # @return [NilClass] nil value (=> no element)
56
+ def on_text_node(content)
57
+ if context.last.respond_to?(:on_text_node)
58
+ # Avoid geting text from table rendered before it.
59
+ context.last.on_text_node(content)
60
+ return
61
+ end
62
+
63
+ return if context.previous_tag&.block? && content.match?(/\A\s*\Z/)
64
+
65
+ text = prepare_text(content)
66
+ buffer << context.merged_styles.merge(text: text) unless text.empty?
67
+ context.last_text_node = true
68
+ nil
69
+ end
70
+
71
+ # Render the buffer content to the PDF document
72
+ def render
73
+ return if buffer.empty?
74
+
75
+ output_content(buffer.dup, context.block_styles)
76
+ buffer.clear
77
+ @last_margin = 0
78
+ end
79
+
80
+ alias_method :flush, :render
81
+
82
+ private
83
+
84
+ attr_reader :buffer, :context, :last_margin, :pdf
85
+
86
+ def setup_element(element, element_styles:)
87
+ render_if_needed(element)
88
+ context.add(element)
89
+ element.process_styles(element_styles: element_styles)
90
+ apply_tag_open_styles(element)
91
+ element.custom_render(pdf, context) if element.respond_to?(:custom_render)
92
+ end
93
+
94
+ def render_if_needed(element)
95
+ render_needed = element&.block? && buffer.any? && buffer.last != NEW_LINE
96
+ return false unless render_needed
97
+
98
+ render
99
+ true
100
+ end
101
+
102
+ def apply_tag_close_styles(element)
103
+ tag_styles = element.tag_close_styles
104
+ @last_margin = tag_styles[:margin_bottom].to_f
105
+ puts "apply_tag_close_styles(#{element.tag}), margin_bottom=#{tag_styles[:margin_bottom]}, last_margin=#{@last_margin}"
106
+ pdf.advance_cursor(last_margin + tag_styles[:padding_bottom].to_f)
107
+ pdf.start_new_page if tag_styles[:break_after]
108
+ end
109
+
110
+ def apply_tag_open_styles(element)
111
+ tag_styles = element.tag_open_styles
112
+ move_down = (tag_styles[:margin_top].to_f - last_margin) + tag_styles[:padding_top].to_f
113
+ puts "apply_tag_open_styles(#{element.tag}), margin_top=#{tag_styles[:margin_top]}, padding_top: #{tag_styles[:padding_top]} last_margin=#{@last_margin}"
114
+ pdf.advance_cursor(move_down) if move_down > 0
115
+ pdf.start_new_page if tag_styles[:break_before]
116
+ end
117
+
118
+ def prepare_text(content)
119
+ text = @before_content.any? ? ::Oga::HTML::Entities.decode(@before_content.join) : ''
120
+ @before_content.clear
121
+
122
+ return (@last_text = text + content) if context.white_space_pre?
123
+
124
+ content = content.lstrip if @last_text[-1] == ' ' || @last_tag_open
125
+ text += content.tr("\n", ' ').squeeze(' ')
126
+ @last_text = text
127
+ end
128
+
129
+ def output_content(buffer, block_styles)
130
+ apply_callbacks(buffer)
131
+ left_indent = block_styles[:margin_left].to_f + block_styles[:padding_left].to_f
132
+ options = block_styles.slice(:align, :indent_paragraphs, :leading, :mode, :padding_left)
133
+ options[:leading] = adjust_leading(buffer, options[:leading])
134
+ pdf.puts(buffer, options, bounding_box: bounds(buffer, options, block_styles), left_indent: left_indent)
135
+ end
136
+
137
+ def apply_callbacks(buffer)
138
+ buffer.select { |item| item[:callback] }.each do |item|
139
+ callback, arg = item[:callback]
140
+ callback_class = Tag::CALLBACKS[callback]
141
+ item[:callback] = callback_class.new(pdf, arg)
142
+ end
143
+ end
144
+
145
+ def adjust_leading(buffer, leading)
146
+ return leading if leading
147
+
148
+ leadings = buffer.map do |item|
149
+ (item[:size] || Context::DEFAULT_STYLES[:size]) * (ADJUST_LEADING[item[:font]] || ADJUST_LEADING[nil])
150
+ end
151
+ leadings.max.round(4)
152
+ end
153
+
154
+ def bounds(buffer, options, block_styles)
155
+ return unless block_styles[:position] == :absolute
156
+
157
+ x = if block_styles.include?(:right)
158
+ x1 = pdf.calc_buffer_width(buffer) + block_styles[:right]
159
+ x1 < pdf.page_width ? (pdf.page_width - x1) : 0
160
+ else
161
+ block_styles[:left] || 0
162
+ end
163
+ y = if block_styles.include?(:bottom)
164
+ pdf.calc_buffer_height(buffer, options) + block_styles[:bottom]
165
+ else
166
+ pdf.page_height - (block_styles[:top] || 0)
167
+ end
168
+
169
+ [[x, y], { width: pdf.page_width - x }]
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oga'
4
+
5
+ module PrawnHtml
6
+ class HtmlParser
7
+ REGEXP_STYLES = /\s*([^{\s]+)\s*{\s*([^}]*?)\s*}/m.freeze
8
+
9
+ # Init the HtmlParser
10
+ #
11
+ # @param renderer [DocumentRenderer] document renderer
12
+ # @param ignore_content_tags [Array] array of tags (symbols) to skip their contents while preparing the PDF document
13
+ def initialize(renderer, ignore_content_tags: %i[script style])
14
+ @processing = false
15
+ @ignore = false
16
+ @ignore_content_tags = ignore_content_tags
17
+ @renderer = renderer
18
+ @raw_styles = {}
19
+ end
20
+
21
+ # Processes HTML and renders it
22
+ #
23
+ # @param html [String] The HTML content to process
24
+ def process(html)
25
+ @styles = {}
26
+ @processing = !html.include?('<body')
27
+ @document = Oga.parse_html(html)
28
+ process_styles # apply previously loaded styles
29
+ traverse_nodes(document.children)
30
+ renderer.flush
31
+ end
32
+
33
+ # Parses CSS styles
34
+ #
35
+ # @param text_styles [String] The CSS styles to evaluate
36
+ def parse_styles(text_styles)
37
+ @raw_styles = text_styles.scan(REGEXP_STYLES).to_h
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :document, :ignore, :processing, :renderer, :styles
43
+
44
+ def traverse_nodes(nodes)
45
+ nodes.each do |node|
46
+ next if node.is_a?(Oga::XML::Comment)
47
+
48
+ element = node_open(node)
49
+ traverse_nodes(node.children) if node.children.any?
50
+ node_close(element) if element
51
+ end
52
+ end
53
+
54
+ def node_open(node)
55
+ tag = node.is_a?(Oga::XML::Element) && init_element(node)
56
+ return unless processing
57
+ return IgnoredTag.new(tag) if ignore
58
+ return renderer.on_text_node(node.text) unless tag
59
+
60
+ renderer.on_tag_open(tag, attributes: prepare_attributes(node), element_styles: styles[node])
61
+ end
62
+
63
+ def init_element(node)
64
+ node.name.downcase.to_sym.tap do |tag_name|
65
+ @processing = true if tag_name == :body
66
+ @ignore = true if @processing && @ignore_content_tags.include?(tag_name)
67
+ process_styles(node.text) if tag_name == :style
68
+ end
69
+ end
70
+
71
+ def process_styles(text_styles = nil)
72
+ parse_styles(text_styles) if text_styles
73
+ @raw_styles.each do |selector, rule|
74
+ document.css(selector).each do |node|
75
+ styles[node] = rule
76
+ end
77
+ end
78
+ end
79
+
80
+ def prepare_attributes(node)
81
+ node.attributes.each_with_object({}) do |attr, res|
82
+ res[attr.name] = attr.value
83
+ end
84
+ end
85
+
86
+ def node_close(element)
87
+ if processing
88
+ renderer.on_tag_close(element) unless ignore
89
+ @ignore = false if ignore && @ignore_content_tags.include?(element.tag)
90
+ end
91
+ @processing = false if element.tag == :body
92
+ end
93
+ end
94
+
95
+ class IgnoredTag
96
+ attr_accessor :tag
97
+
98
+ def initialize(tag_name)
99
+ @tag = tag_name
100
+ end
101
+ end
102
+
103
+ HtmlHandler = HtmlParser
104
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ class Instance
5
+ attr_reader :html_parser, :pdf_wrapper, :renderer
6
+
7
+ def initialize(pdf)
8
+ @pdf_wrapper = PrawnHtml::PdfWrapper.new(pdf)
9
+ @renderer = PrawnHtml::DocumentRenderer.new(@pdf_wrapper)
10
+ @html_parser = PrawnHtml::HtmlParser.new(@renderer)
11
+ end
12
+
13
+ def append(css: nil, html: nil)
14
+ html_parser.parse_styles(css) if css
15
+ html_parser.process(html) if html
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module PrawnHtml
6
+ class PdfWrapper
7
+ extend Forwardable
8
+
9
+ def_delegators :@pdf, :start_new_page
10
+
11
+ # Wrapper for Prawn PDF Document
12
+ #
13
+ # @param pdf_document [Prawn::Document] PDF document to wrap
14
+ def initialize(pdf_document)
15
+ @pdf = pdf_document
16
+ end
17
+
18
+ # Advance the cursor
19
+ #
20
+ # @param move_down [Float] Quantity to advance (move down)
21
+ def advance_cursor(move_down)
22
+ return if !move_down || move_down == 0
23
+
24
+ pdf.move_down(move_down)
25
+ end
26
+
27
+ # Calculate the height of a buffer of items
28
+ #
29
+ # @param buffer [Array] Buffer of items
30
+ # @param options [Hash] Output options
31
+ #
32
+ # @return [Float] calculated height
33
+ def calc_buffer_height(buffer, options)
34
+ pdf.height_of_formatted(buffer, options)
35
+ end
36
+
37
+ # Calculate the width of a buffer of items
38
+ #
39
+ # @param buffer [Array] Buffer of items
40
+ #
41
+ # @return [Float] calculated width
42
+ def calc_buffer_width(buffer)
43
+ width = 0
44
+ buffer.each do |item|
45
+ font_family = item[:font] || pdf.font.name
46
+ pdf.font(font_family, size: item[:size] || pdf.font_size) do
47
+ width += pdf.width_of(item[:text], inline_format: true)
48
+ end
49
+ end
50
+ width
51
+ end
52
+
53
+ # Height of the page
54
+ #
55
+ # @return [Float] height
56
+ def page_height
57
+ pdf.bounds.height
58
+ end
59
+
60
+ # Width of the page
61
+ #
62
+ # @return [Float] width
63
+ def page_width
64
+ pdf.bounds.width
65
+ end
66
+
67
+ # Draw a rectangle
68
+ #
69
+ # @param x [Float] left position of the rectangle
70
+ # @param y [Float] top position of the rectangle
71
+ # @param width [Float] width of the rectangle
72
+ # @param height [Float] height of the rectangle
73
+ # @param color [String] fill color
74
+ def draw_rectangle(x:, y:, width:, height:, color:)
75
+ current_fill_color = pdf.fill_color
76
+ pdf.fill_color = color
77
+ pdf.fill_rectangle([y, x], width, height)
78
+ pdf.fill_color = current_fill_color
79
+ end
80
+
81
+ # Horizontal line
82
+ #
83
+ # @param color [String] line color
84
+ # @param dash [Integer|Array] integer or array of integer with dash options
85
+ def horizontal_rule(color:, dash:)
86
+ current_color = pdf.stroke_color
87
+ pdf.dash(dash) if dash
88
+ pdf.stroke_color = color if color
89
+ pdf.stroke_horizontal_rule
90
+ pdf.stroke_color = current_color if color
91
+ pdf.undash if dash
92
+ end
93
+
94
+ # Image
95
+ #
96
+ # @param src [String] image source path
97
+ # @param options [Hash] hash of options
98
+ def image(src, options = {})
99
+ return unless src
100
+
101
+ pdf.image(src, options)
102
+ end
103
+
104
+ # Output to the PDF document
105
+ #
106
+ # @param buffer [Array] array of text items
107
+ # @param options [Hash] hash of options
108
+ # @param bounding_box [Array] bounding box arguments, if bounded
109
+ def puts(buffer, options, bounding_box: nil, left_indent: 0)
110
+ return output_buffer(buffer, options, left_indent: left_indent) unless bounding_box
111
+
112
+ current_y = pdf.cursor
113
+ pdf.bounding_box(*bounding_box) do
114
+ output_buffer(buffer, options, left_indent: left_indent)
115
+ end
116
+ pdf.move_cursor_to(current_y)
117
+ end
118
+
119
+ # Underline
120
+ #
121
+ # @param x1 [Float] left position of the line
122
+ # @param x2 [Float] right position of the line
123
+ # @param y [Float] vertical position of the line
124
+ def underline(x1:, x2:, y:)
125
+ pdf.stroke do
126
+ pdf.line [x1, y], [x2, y]
127
+ end
128
+ end
129
+
130
+ def table(rows, options = {})
131
+ pdf.table(rows, options) # uses prawn-table
132
+ end
133
+
134
+ private
135
+
136
+ attr_reader :pdf
137
+
138
+ def output_buffer(buffer, options, left_indent:)
139
+ formatted_text = proc { pdf.formatted_text(buffer, options) }
140
+ return formatted_text.call if left_indent == 0
141
+
142
+ pdf.indent(left_indent, 0, &formatted_text)
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ class Tag
5
+ extend Forwardable
6
+
7
+ CALLBACKS = {
8
+ 'Background' => Callbacks::Background,
9
+ 'StrikeThrough' => Callbacks::StrikeThrough
10
+ }.freeze
11
+
12
+ TAG_CLASSES = %w[
13
+ A B Blockquote Body Br Code Del Div H Hr I Img Li Mark Ol P Pre Small Span Sub Sup U Ul
14
+ Table Tr Td Th Tbody Colgroup Col
15
+ ].freeze
16
+
17
+ def_delegators :@attrs, :styles, :update_styles
18
+
19
+ attr_accessor :parent
20
+ attr_reader :attrs, :tag
21
+
22
+ # Init the Tag
23
+ #
24
+ # @param tag [Symbol] tag name
25
+ # @param attributes [Hash] hash of element attributes
26
+ # @param options [Hash] options (container width/height/etc.)
27
+ def initialize(tag, attributes: {}, options: {})
28
+ @tag = tag
29
+ @options = options
30
+ @attrs = Attributes.new(attributes)
31
+ end
32
+
33
+ # Is a block tag?
34
+ #
35
+ # @return [Boolean] true if the type of the tag is block, false otherwise
36
+ def block?
37
+ false
38
+ end
39
+
40
+ # Styles to apply to the block
41
+ #
42
+ # @return [Hash] hash of styles to apply
43
+ def block_styles
44
+ block_styles = styles.slice(*Attributes::STYLES_APPLY[:block])
45
+ block_styles[:mode] = attrs.data['mode'].to_sym if attrs.data.include?('mode')
46
+ block_styles
47
+ end
48
+
49
+ # Process tag styles
50
+ #
51
+ # @param element_styles [String] extra styles to apply to the element
52
+ def process_styles(element_styles: nil)
53
+ attrs.merge_text_styles!(tag_styles, options: options) if respond_to?(:tag_styles)
54
+ attrs.merge_text_styles!(element_styles, options: options) if element_styles
55
+ attrs.merge_text_styles!(attrs.style, options: options)
56
+ attrs.merge_text_styles!(extra_styles, options: options) if respond_to?(:extra_styles)
57
+ end
58
+
59
+ # Styles to apply on tag closing
60
+ #
61
+ # @return [Hash] hash of styles to apply
62
+ def tag_close_styles
63
+ styles.slice(*Attributes::STYLES_APPLY[:tag_close])
64
+ end
65
+
66
+ # Styles to apply on tag opening
67
+ #
68
+ # @return [Hash] hash of styles to apply
69
+ def tag_open_styles
70
+ styles.slice(*Attributes::STYLES_APPLY[:tag_open])
71
+ end
72
+
73
+ class << self
74
+ # Evaluate the Tag class from a tag name
75
+ #
76
+ # @params tag_name [Symbol] the tag name
77
+ #
78
+ # @return [Tag] the class for the tag if available or nil
79
+ def class_for(tag_name)
80
+ @tag_classes ||= TAG_CLASSES.each_with_object({}) do |tag_class, res|
81
+ klass = const_get("PrawnHtml::Tags::#{tag_class}")
82
+ k = [klass] * klass::ELEMENTS.size
83
+ res.merge!(klass::ELEMENTS.zip(k).to_h)
84
+ end
85
+ @tag_classes[tag_name]
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ attr_reader :options
92
+ end
93
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class A < Tag
6
+ ELEMENTS = [:a].freeze
7
+
8
+ def extra_styles
9
+ attrs.href ? "href: #{attrs.href}" : nil
10
+ end
11
+
12
+ def tag_styles
13
+ <<~STYLES
14
+ color: #00E;
15
+ text-decoration: underline;
16
+ STYLES
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class B < Tag
6
+ ELEMENTS = [:b, :strong].freeze
7
+
8
+ def tag_styles
9
+ 'font-weight: bold'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class Blockquote < Tag
6
+ ELEMENTS = [:blockquote].freeze
7
+
8
+ MARGIN_BOTTOM = 12.7
9
+ MARGIN_LEFT = 40.4
10
+ MARGIN_TOP = 12.7
11
+
12
+ def block?
13
+ true
14
+ end
15
+
16
+ def tag_styles
17
+ <<~STYLES
18
+ margin-bottom: #{MARGIN_BOTTOM}px;
19
+ margin-left: #{MARGIN_LEFT}px;
20
+ margin-top: #{MARGIN_TOP}px;
21
+ STYLES
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class Body < Tag
6
+ ELEMENTS = [:body].freeze
7
+
8
+ def block?
9
+ true
10
+ end
11
+ end
12
+ end
13
+ end