prawn-table-html 0.0.1

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 (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