prawn-html 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a5a671ae12f6a66358c10f739404ab709f3fdcaacb059b9943433a7535a539a5
4
+ data.tar.gz: 274afa7a2d6cbb2a75e1ca03e2b82ee38985bf279f41ecd3878166ce9eabf15a
5
+ SHA512:
6
+ metadata.gz: a67e743e2c77c1b0e9caecefd9b6f1fc2d33243d0b86b37548f5bbed7e8e569ea11562d849c82fbdfd8ca7eeb5bdf2921cf789464868f0e90de4b631dcf89a10
7
+ data.tar.gz: dd06b9b0410cceb28eaf68ab6eaeb6007086047478f3ec1ba2ffd37a0a38de38264f56fae3232b4025a5d6f45b776fe2d79bb23eb76926a8a87139e82294e14d
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2021 Mattia Roccoberton
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Prawn HTML
2
+ [![linters](https://github.com/blocknotes/prawn-html/actions/workflows/linters.yml/badge.svg)](https://github.com/blocknotes/prawn-html/actions/workflows/linters.yml)
3
+ [![specs](https://github.com/blocknotes/prawn-html/actions/workflows/specs.yml/badge.svg)](https://github.com/blocknotes/prawn-html/actions/workflows/specs.yml)
4
+
5
+ HTML to PDF renderer using [Prawn PDF](https://github.com/prawnpdf/prawn).
6
+
7
+ > Still in beta. [prawn-styled-text](https://github.com/blocknotes/prawn-styled-text) rewritten from scratch
8
+
9
+ **Notice**: render HTML documents properly is not an easy task, this gem support only some HTML tags and a small set of CSS attributes. If you need more rendering accuracy take a look at other projects like WickedPDF.
10
+
11
+ Please :star: if you like it.
12
+
13
+ ## Install
14
+
15
+ - Add to your Gemfile: `gem 'prawn-html', git: 'https://github.com/blocknotes/prawn-html.git'` (and execute `bundle`)
16
+ - Use the class `HtmlHandler` on a `Prawn::Document` instance
17
+
18
+ ## Examples
19
+
20
+ ```rb
21
+ require 'prawn-html'
22
+ pdf = Prawn::Document.new(page_size: 'A4')
23
+ PrawnHtml::HtmlHandler.new(pdf).process('<h1 style="text-align: center">Just a test</h1>')
24
+ pdf.render_file('test.pdf')
25
+ ```
26
+
27
+ ## Supported tags & attributes
28
+
29
+ HTML tags:
30
+
31
+ - **a**: link
32
+ - **b**: bold
33
+ - **br**: new line
34
+ - **del**: strike-through
35
+ - **div**: block element
36
+ - **em**: italic
37
+ - **h1** - **h6**: headings
38
+ - **hr**: horizontal line
39
+ - **i**: italic
40
+ - **ins**: underline
41
+ - **img**: image
42
+ - **li**: list item
43
+ - **mark**: highlight
44
+ - **p**: block element
45
+ - **s**: strike-through
46
+ - **small**: smaller text
47
+ - **span**: inline element
48
+ - **strong**: bold
49
+ - **u**: underline
50
+ - **ul**: list
51
+
52
+ CSS attributes (dimensional units are ignored and considered in pixel):
53
+
54
+ - **background**: for *mark* tag, only 3 or 6 hex digits format, ex. `style="background: #FECD08"`
55
+ - **color**: only 3 or 6 hex digits format - ex. `style="color: #FB1"`
56
+ - **font-family**: font must be registered, quotes are optional, ex. `style="font-family: Courier"`
57
+ - **font-size**: ex. `style="font-size: 20px"`
58
+ - **font-style**: values: *:italic*, ex. `style="font-style: italic"`
59
+ - **font-weight**: values: *:bold*, ex. `style="font-weight: bold"`
60
+ - **height**: for *img* tag, ex. `<img src="image.jpg" style="height: 200px"/>`
61
+ - **href**: for *a* tag, ex. `<a href="http://www.google.com/">Google</a>`
62
+ - **letter-spacing**: ex. `style="letter-spacing: 1.5"`
63
+ - **line-height**: ex. `style="line-height: 10px"`
64
+ - **margin-bottom**: ex. `style="margin-bottom: 10px"`
65
+ - **margin-left**: ex. `style="margin-left: 15px"`
66
+ - **margin-top**: ex. `style="margin-top: 20px"`
67
+ - **src**: for *img* tag, ex. `<img src="image.jpg"/>`
68
+ - **text-align**: `left` | `center` | `right` | `justify`, ex. `style="text-align: center"`
69
+ - **text-decoration**: `underline`, ex. `style="text-decoration: underline"`
70
+ - **width**: for *img* tag, support also percentage, ex. `<img src="image.jpg" style="width: 50%; height: 200px"/>`
71
+
72
+ ## Do you like it? Star it!
73
+
74
+ If you use this component just star it. A developer is more motivated to improve a project when there is some interest.
75
+
76
+ Or consider offering me a coffee, it's a small thing but it is greatly appreciated: [about me](https://www.blocknot.es/about-me).
77
+
78
+ ## Contributors
79
+
80
+ - [Mattia Roccoberton](https://www.blocknot.es): author
81
+
82
+ ## License
83
+
84
+ The gem is available as open-source under the terms of the [MIT](LICENSE.txt).
data/lib/prawn-html.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prawn'
4
+
5
+ require 'prawn_html/tags/base'
6
+ Dir["#{__dir__}/prawn_html/tags/*.rb"].sort.each { |f| require f }
7
+
8
+ Dir["#{__dir__}/prawn_html/callbacks/*.rb"].sort.each { |f| require f }
9
+
10
+ require 'prawn_html/attributes'
11
+ require 'prawn_html/context'
12
+ require 'prawn_html/document_renderer'
13
+ require 'prawn_html/html_handler'
14
+
15
+ module PrawnHtml
16
+ PX = 0.66 # conversion costant for pixel sixes
17
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module PrawnHtml
6
+ class Attributes
7
+ attr_reader :hash, :options, :post_styles, :pre_styles, :styles
8
+
9
+ STYLES_LIST = {
10
+ # styles
11
+ 'background' => { key: :background, set: :convert_color, dest: :styles },
12
+ 'color' => { key: :color, set: :convert_color, dest: :styles },
13
+ 'font-family' => { key: :font, set: :unquote, dest: :styles },
14
+ 'font-size' => { key: :size, set: :convert_size, dest: :styles },
15
+ 'font-style' => { key: :styles, set: :append_symbol, dest: :styles },
16
+ 'font-weight' => { key: :styles, set: :append_symbol, dest: :styles },
17
+ 'letter-spacing' => { key: :character_spacing, set: :convert_float, dest: :styles },
18
+ # pre styles
19
+ 'margin-top' => { key: :margin_top, set: :convert_size, dest: :pre_styles },
20
+ # post styles
21
+ 'margin-bottom' => { key: :margin_bottom, set: :convert_size, dest: :post_styles },
22
+ 'padding-bottom' => { key: :padding_bottom, set: :convert_size, dest: :post_styles },
23
+ # options
24
+ 'line-height' => { key: :leading, set: :convert_size, dest: :options },
25
+ 'margin-left' => { key: :margin_left, set: :convert_size, dest: :options },
26
+ 'padding-left' => { key: :padding_left, set: :convert_size, dest: :options },
27
+ 'padding-top' => { key: :padding_top, set: :convert_size, dest: :options },
28
+ 'text-align' => { key: :align, set: :convert_symbol, dest: :options },
29
+ 'text-decoration' => { key: :styles, set: :append_symbol, dest: :styles }
30
+ }.freeze
31
+
32
+ STYLES_MERGE = %i[margin_left padding_left].freeze
33
+
34
+ # Init the Attributes
35
+ #
36
+ # @param attributes [Hash] hash of attributes to parse
37
+ def initialize(attributes)
38
+ @hash = ::OpenStruct.new(attributes)
39
+ @options = {}
40
+ @post_styles = {}
41
+ @pre_styles = {}
42
+ @styles = {} # result styles
43
+ parsed_styles = Attributes.parse_styles(hash.style)
44
+ process_styles(parsed_styles)
45
+ end
46
+
47
+ # Processes the styles attributes
48
+ #
49
+ # @param attributes [Hash] hash of styles attributes
50
+ def process_styles(styles)
51
+ styles.each do |key, value|
52
+ rule = STYLES_LIST[key]
53
+ next unless rule
54
+
55
+ apply_rule(rule, value)
56
+ end
57
+ end
58
+
59
+ class << self
60
+ # Converts a color string
61
+ #
62
+ # @param value [String] HTML string color
63
+ #
64
+ # @return [String] adjusted string color
65
+ def convert_color(value)
66
+ val = value&.downcase || +''
67
+ val.gsub!(/[^a-f0-9]/, '')
68
+ return val unless val.size == 3
69
+
70
+ a, b, c = val.chars
71
+ a * 2 + b * 2 + c * 2
72
+ end
73
+
74
+ # Converts a decimal number string
75
+ #
76
+ # @param value [String] string decimal
77
+ #
78
+ # @return [Float] converted and rounded float number
79
+ def convert_float(value)
80
+ val = value&.gsub(/[^0-9.]/, '') || ''
81
+ val.to_f.round(4)
82
+ end
83
+
84
+ # Converts a size string
85
+ #
86
+ # @param value [String] size string
87
+ # @param container_size [Numeric] container size
88
+ #
89
+ # @return [Float] converted and rounded size
90
+ def convert_size(value, container_size = nil)
91
+ val = value&.gsub(/[^0-9.]/, '') || ''
92
+ val =
93
+ if container_size && value.include?('%')
94
+ val.to_f * container_size * 0.01
95
+ else
96
+ val.to_f * PX
97
+ end
98
+ # pdf.bounds.height
99
+ val.round(4)
100
+ end
101
+
102
+ # Converts a string to symbol
103
+ #
104
+ # @param value [String] string
105
+ #
106
+ # @return [Symbol] symbol
107
+ def convert_symbol(value)
108
+ value.to_sym if value && !value.match?(/\A\s*\Z/)
109
+ end
110
+
111
+ # Merges attributes
112
+ #
113
+ # @param hash [Hash] target attributes hash
114
+ # @param key [Symbol] key
115
+ # @param value
116
+ #
117
+ # @return [Hash] the updated hash of attributes
118
+ def merge_attr!(hash, key, value)
119
+ return unless key
120
+ return (hash[key] = value) unless Attributes::STYLES_MERGE.include?(key)
121
+
122
+ hash[key] ||= 0
123
+ hash[key] += value
124
+ end
125
+
126
+ # Parses a string of styles
127
+ #
128
+ # @param styles [String] styles to parse
129
+ #
130
+ # @return [Hash] hash of styles
131
+ def parse_styles(styles)
132
+ (styles || '').scan(/\s*([^:;]+)\s*:\s*([^;]+)\s*/).to_h
133
+ end
134
+
135
+ # Unquotes a string
136
+ #
137
+ # @param value [String] string
138
+ #
139
+ # @return [String] string without quotes at the beginning/ending
140
+ def unquote(value)
141
+ (value&.strip || +'').tap do |val|
142
+ val.gsub!(/\A['"]|["']\Z/, '')
143
+ end
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ def apply_rule(rule, value)
150
+ if rule[:set] == :append_symbol
151
+ (send(rule[:dest])[rule[:key]] ||= []) << Attributes.convert_symbol(value)
152
+ else
153
+ send(rule[:dest])[rule[:key]] = Attributes.send(rule[:set], value)
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Callbacks
5
+ class Highlight
6
+ DEF_HIGHLIGHT = 'ffff00'
7
+
8
+ def initialize(pdf, item)
9
+ @pdf = pdf
10
+ @color = item.delete(:background) || DEF_HIGHLIGHT
11
+ end
12
+
13
+ def render_behind(fragment)
14
+ original_color = @pdf.fill_color
15
+ @pdf.fill_color = @color
16
+ @pdf.fill_rectangle(fragment.top_left, fragment.width, fragment.height)
17
+ @pdf.fill_color = original_color
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Callbacks
5
+ class StrikeThrough
6
+ def initialize(pdf, _item)
7
+ @pdf = pdf
8
+ end
9
+
10
+ def render_in_front(fragment)
11
+ y = (fragment.top_left[1] + fragment.bottom_left[1]) / 2
12
+ @pdf.stroke do
13
+ @pdf.line [fragment.top_left[0], y], [fragment.top_right[0], y]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ class Context < Array
5
+ DEF_FONT_SIZE = 10.3
6
+
7
+ attr_accessor :last_margin, :last_text_node
8
+
9
+ # Init the Context
10
+ def initialize(*_args)
11
+ super
12
+ @last_margin = 0
13
+ end
14
+
15
+ def before_content
16
+ return '' if empty?
17
+
18
+ last.options[:before_content].to_s
19
+ end
20
+
21
+ # Merges the context options
22
+ #
23
+ # @return [Hash] the hash of merged options
24
+ def merge_options
25
+ each_with_object({}) do |element, res|
26
+ element.options.each do |key, value|
27
+ Attributes.merge_attr!(res, key, value)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Merge the context styles
33
+ #
34
+ # @return [Hash] the hash of merged styles
35
+ def merge_styles
36
+ context_styles = each_with_object({}) do |element, res|
37
+ evaluate_element_styles(element, res)
38
+ element.update_styles(res) if element.respond_to?(:update_styles)
39
+ end
40
+ base_styles.merge(context_styles)
41
+ end
42
+
43
+ private
44
+
45
+ def base_styles
46
+ {
47
+ size: DEF_FONT_SIZE
48
+ }
49
+ end
50
+
51
+ def evaluate_element_styles(element, res)
52
+ element.styles.each do |key, val|
53
+ if res.include?(key) && res[key].is_a?(Array)
54
+ res[key] += val
55
+ else
56
+ res[key] = val
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ class DocumentRenderer
5
+ NEW_LINE = { text: "\n" }.freeze
6
+ SPACE = { text: ' ' }.freeze
7
+ TAG_CLASSES = [Tags::A, Tags::B, Tags::Body, Tags::Br, Tags::Del, Tags::Div, Tags::H, Tags::Hr, Tags::I, Tags::Img, Tags::Li, Tags::Mark, Tags::P, Tags::Small, Tags::Span, Tags::U, Tags::Ul].freeze
8
+
9
+ # Init the DocumentRenderer
10
+ #
11
+ # @param pdf [Prawn::Document] target Prawn PDF document
12
+ def initialize(pdf)
13
+ @buffer = []
14
+ @context = Context.new
15
+ @doc_styles = {}
16
+ @pdf = pdf
17
+ end
18
+
19
+ # Assigns the document styles
20
+ #
21
+ # @param styles [Hash] styles hash with CSS selectors as keys and rules as values
22
+ def assign_document_styles(styles)
23
+ @doc_styles = styles.transform_values do |style_rules|
24
+ Attributes.new(style: style_rules).styles
25
+ end
26
+ end
27
+
28
+ # On tag close callback
29
+ #
30
+ # @param element [Tags::Base] closing element wrapper
31
+ def on_tag_close(element)
32
+ render_if_needed(element)
33
+ apply_post_styles(element&.post_styles)
34
+ context.last_text_node = false
35
+ context.pop
36
+ end
37
+
38
+ # On tag open callback
39
+ #
40
+ # @param tag [String] the tag name of the opening element
41
+ # @param attributes [Hash] an hash of the element attributes
42
+ #
43
+ # @return [Tags::Base] the opening element wrapper
44
+ def on_tag_open(tag, attributes)
45
+ tag_class = tag_classes[tag]
46
+ return unless tag_class
47
+
48
+ tag_class.new(tag, attributes).tap do |element|
49
+ setup_element(element)
50
+ end
51
+ end
52
+
53
+ # On text node callback
54
+ #
55
+ # @param content [String] the text node content
56
+ #
57
+ # @return [NilClass] nil value (=> no element)
58
+ def on_text_node(content)
59
+ return if content.match?(/\A\s*\Z/)
60
+
61
+ text = content.gsub(/\A\s*\n\s*|\s*\n\s*\Z/, '').delete("\n").squeeze(' ')
62
+ buffer << context.merge_styles.merge(text: ::Oga::HTML::Entities.decode(context.before_content) + text)
63
+ context.last_text_node = true
64
+ nil
65
+ end
66
+
67
+ # Render the buffer content to the PDF document
68
+ def render
69
+ return if buffer.empty?
70
+
71
+ options = context.merge_options.slice(:align, :leading, :margin_left, :padding_left)
72
+ output_content(buffer.dup, options)
73
+ buffer.clear
74
+ context.last_margin = 0
75
+ end
76
+
77
+ alias_method :flush, :render
78
+
79
+ private
80
+
81
+ attr_reader :buffer, :context, :doc_styles, :pdf
82
+
83
+ def tag_classes
84
+ @tag_classes ||= TAG_CLASSES.each_with_object({}) do |klass, res|
85
+ res.merge!(klass.elements)
86
+ end
87
+ end
88
+
89
+ def setup_element(element)
90
+ add_space_if_needed unless render_if_needed(element)
91
+ apply_pre_styles(element)
92
+ element.apply_doc_styles(doc_styles)
93
+ context.push(element)
94
+ element.custom_render(pdf, context) if element.respond_to?(:custom_render)
95
+ end
96
+
97
+ def add_space_if_needed
98
+ buffer << SPACE if buffer.any? && !context.last_text_node && ![NEW_LINE, SPACE].include?(buffer.last)
99
+ end
100
+
101
+ def render_if_needed(element)
102
+ render_needed = element&.block? && buffer.any? && buffer.last != NEW_LINE
103
+ return false unless render_needed
104
+
105
+ render
106
+ true
107
+ end
108
+
109
+ def apply_post_styles(styles)
110
+ context.last_margin = styles[:margin_bottom].to_f
111
+ return if !styles || styles.empty?
112
+
113
+ pdf.move_down(context.last_margin.round(4)) if context.last_margin > 0
114
+ pdf.move_down(styles[:padding_bottom].round(4)) if styles[:padding_bottom].to_f > 0
115
+ end
116
+
117
+ def apply_pre_styles(element)
118
+ pdf.move_down(element.options[:padding_top].round(4)) if element.options.include?(:padding_top)
119
+ return if !element.pre_styles || element.pre_styles.empty?
120
+
121
+ margin = (element.pre_styles[:margin_top] - context.last_margin).round(4)
122
+ pdf.move_down(margin) if margin > 0
123
+ end
124
+
125
+ def output_content(buffer, options)
126
+ buffer.each { |item| item[:callback] = item[:callback].new(pdf, item) if item[:callback] }
127
+ if (left = options.delete(:margin_left).to_f + options.delete(:padding_left).to_f) > 0
128
+ pdf.indent(left) { pdf.formatted_text(buffer, options) }
129
+ else
130
+ pdf.formatted_text(buffer, options)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oga'
4
+
5
+ module PrawnHtml
6
+ class HtmlHandler
7
+ # Init the HtmlHandler
8
+ #
9
+ # @param pdf [Prawn::Document] Target Prawn PDF document
10
+ def initialize(pdf)
11
+ @processing = false
12
+ @renderer = DocumentRenderer.new(pdf)
13
+ end
14
+
15
+ # Processes HTML and renders it on the PDF document
16
+ #
17
+ # @param html [String] The HTML content to process
18
+ def process(html)
19
+ @processing = !html.include?('<body')
20
+ doc = Oga.parse_html(html)
21
+ traverse_nodes(doc.children)
22
+ renderer.flush
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :processing, :renderer
28
+
29
+ def traverse_nodes(nodes)
30
+ nodes.each do |node|
31
+ element = node_open(node)
32
+ traverse_nodes(node.children) if node.children.any?
33
+ node_close(element) if element
34
+ end
35
+ end
36
+
37
+ def node_open(node)
38
+ tag = node.is_a?(Oga::XML::Element) && init_element(node)
39
+ return unless processing
40
+ return renderer.on_text_node(node.text) unless tag
41
+
42
+ attributes = prepare_attributes(node)
43
+ renderer.on_tag_open(tag, attributes)
44
+ end
45
+
46
+ def init_element(node)
47
+ node.name.downcase.to_sym.tap do |tag_name|
48
+ @processing = true if tag_name == :body
49
+ renderer.assign_document_styles(extract_styles(node.text)) if tag_name == :style && !@processing
50
+ end
51
+ end
52
+
53
+ def extract_styles(text)
54
+ text.scan(/\s*([^{\s]+)\s*{\s*([^}]*?)\s*}/m).to_h
55
+ end
56
+
57
+ def prepare_attributes(node)
58
+ node.attributes.each_with_object({}) do |attr, res|
59
+ res[attr.name] = attr.value
60
+ end
61
+ end
62
+
63
+ def node_close(element)
64
+ renderer.on_tag_close(element) if @processing
65
+ @processing = false if element.tag == :body
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class A < Base
8
+ ELEMENTS = [:a].freeze
9
+
10
+ def styles
11
+ super.merge(
12
+ link: attrs.hash.href
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class B < Base
8
+ ELEMENTS = [:b, :strong].freeze
9
+
10
+ def extra_attrs
11
+ {
12
+ 'font-weight' => 'bold'
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class Base
6
+ attr_reader :attrs, :styles, :tag
7
+
8
+ def initialize(tag, attributes = {})
9
+ @attrs = Attributes.new(attributes)
10
+ @styles = attrs.styles
11
+ @tag = tag
12
+ attrs.process_styles(extra_attrs) unless extra_attrs.empty?
13
+ end
14
+
15
+ def apply_doc_styles(document_styles)
16
+ selectors = [
17
+ tag.to_s,
18
+ attrs.hash['class'] ? ".#{attrs.hash['class']}" : nil,
19
+ attrs.hash['id'] ? "##{attrs.hash['id']}" : nil
20
+ ].compact!
21
+ merged_styles = document_styles.each_with_object({}) do |(sel, attributes), res|
22
+ res.merge!(attributes) if selectors.include?(sel)
23
+ end
24
+ styles.merge!(merged_styles)
25
+ end
26
+
27
+ def block?
28
+ false
29
+ end
30
+
31
+ def extra_attrs
32
+ {}
33
+ end
34
+
35
+ def options
36
+ attrs.options
37
+ end
38
+
39
+ def post_styles
40
+ attrs.post_styles
41
+ end
42
+
43
+ def pre_styles
44
+ attrs.pre_styles
45
+ end
46
+
47
+ class << self
48
+ def elements
49
+ self::ELEMENTS.each_with_object({}) { |el, list| list[el] = self }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Body < Base
8
+ ELEMENTS = [:body].freeze
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Br < Base
8
+ ELEMENTS = [:br].freeze
9
+
10
+ BR_SPACING = 12
11
+
12
+ def block?
13
+ true
14
+ end
15
+
16
+ def custom_render(pdf, context)
17
+ return if context.last_text_node
18
+
19
+ @spacing ||= Attributes.convert_size(BR_SPACING.to_s)
20
+ pdf.move_down(@spacing)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Del < Base
8
+ ELEMENTS = [:del, :s].freeze
9
+
10
+ def styles
11
+ super.merge(
12
+ callback: Callbacks::StrikeThrough
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Div < Base
8
+ ELEMENTS = [:div].freeze
9
+
10
+ def block?
11
+ true
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class H < Base
8
+ ELEMENTS = [:h1, :h2, :h3, :h4, :h5, :h6].freeze
9
+
10
+ MARGINS_TOP = {
11
+ h1: 25.5,
12
+ h2: 20.5,
13
+ h3: 19,
14
+ h4: 20,
15
+ h5: 21.2,
16
+ h6: 23.5
17
+ }.freeze
18
+
19
+ MARGINS_BOTTOM = {
20
+ h1: 18.2,
21
+ h2: 17.5,
22
+ h3: 17.5,
23
+ h4: 22,
24
+ h5: 22,
25
+ h6: 26.5
26
+ }.freeze
27
+
28
+ SIZES = {
29
+ h1: 31,
30
+ h2: 23.5,
31
+ h3: 18.2,
32
+ h4: 16,
33
+ h5: 13,
34
+ h6: 10.5
35
+ }.freeze
36
+
37
+ def block?
38
+ true
39
+ end
40
+
41
+ def extra_attrs
42
+ @extra_attrs ||= {
43
+ 'font-size' => SIZES[tag].to_s,
44
+ 'font-weight' => 'bold',
45
+ 'margin-bottom' => MARGINS_BOTTOM[tag].to_s,
46
+ 'margin-top' => MARGINS_TOP[tag].to_s
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Hr < Base
8
+ ELEMENTS = [:hr].freeze
9
+
10
+ MARGIN_BOTTOM = 12
11
+ MARGIN_TOP = 6
12
+
13
+ def block?
14
+ true
15
+ end
16
+
17
+ def custom_render(pdf, _context)
18
+ pdf.stroke_horizontal_rule
19
+ end
20
+
21
+ def extra_attrs
22
+ @extra_attrs ||= {
23
+ 'margin-bottom' => MARGIN_BOTTOM.to_s,
24
+ 'margin-top' => MARGIN_TOP.to_s,
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class I < Base
8
+ ELEMENTS = [:i, :em].freeze
9
+
10
+ def extra_attrs
11
+ {
12
+ 'font-style' => 'italic'
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Img < Base
8
+ ELEMENTS = [:img].freeze
9
+
10
+ def block?
11
+ true
12
+ end
13
+
14
+ def custom_render(pdf, context)
15
+ styles = Attributes.parse_styles(attrs.hash.style)
16
+ context_options = context.merge_options
17
+ options = evaluate_styles(pdf, context_options.merge(styles))
18
+ pdf.image(@attrs.hash.src, options)
19
+ end
20
+
21
+ private
22
+
23
+ def evaluate_styles(pdf, styles)
24
+ options = {}
25
+ options[:width] = Attributes.convert_size(styles['width'], pdf.bounds.width) if styles.include?('width')
26
+ options[:height] = Attributes.convert_size(styles['height'], pdf.bounds.height) if styles.include?('height')
27
+ options[:position] = styles[:align] if %i[left center right].include?(styles[:align])
28
+ options
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Li < Base
8
+ ELEMENTS = [:li].freeze
9
+
10
+ def block?
11
+ true
12
+ end
13
+
14
+ def options
15
+ super.merge(
16
+ before_content: '&bullet; '
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Mark < Base
8
+ ELEMENTS = [:mark].freeze
9
+
10
+ def styles
11
+ super.merge(
12
+ callback: Callbacks::Highlight
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class P < Base
8
+ ELEMENTS = [:p].freeze
9
+
10
+ MARGIN_BOTTOM = 6
11
+ MARGIN_TOP = 6
12
+
13
+ def block?
14
+ true
15
+ end
16
+
17
+ def extra_attrs
18
+ @extra_attrs ||= {
19
+ 'margin-bottom' => MARGIN_BOTTOM.to_s,
20
+ 'margin-top' => MARGIN_TOP.to_s
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Small < Base
8
+ ELEMENTS = [:small].freeze
9
+
10
+ def update_styles(styles)
11
+ size = (styles[:size] || Context::DEF_FONT_SIZE) * 0.85
12
+ styles[:size] = size
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Span < Base
8
+ ELEMENTS = [:span].freeze
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class U < Base
6
+ ELEMENTS = [:ins, :u].freeze
7
+
8
+ def extra_attrs
9
+ {
10
+ 'text-decoration' => 'underline'
11
+ }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module PrawnHtml
6
+ module Tags
7
+ class Ul < Base
8
+ ELEMENTS = [:ul].freeze
9
+
10
+ MARGIN_LEFT = 25
11
+
12
+ def block?
13
+ true
14
+ end
15
+
16
+ def extra_attrs
17
+ @extra_attrs ||= {
18
+ 'margin-left' => MARGIN_LEFT.to_s,
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml # :nodoc:
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prawn-html
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mattia Roccoberton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-08-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: oga
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: prawn
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.4'
41
+ description: HTML to PDF with Prawn PDF
42
+ email: mat@blocknot.es
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - LICENSE.txt
48
+ - README.md
49
+ - lib/prawn-html.rb
50
+ - lib/prawn_html/attributes.rb
51
+ - lib/prawn_html/callbacks/highlight.rb
52
+ - lib/prawn_html/callbacks/strike_through.rb
53
+ - lib/prawn_html/context.rb
54
+ - lib/prawn_html/document_renderer.rb
55
+ - lib/prawn_html/html_handler.rb
56
+ - lib/prawn_html/tags/a.rb
57
+ - lib/prawn_html/tags/b.rb
58
+ - lib/prawn_html/tags/base.rb
59
+ - lib/prawn_html/tags/body.rb
60
+ - lib/prawn_html/tags/br.rb
61
+ - lib/prawn_html/tags/del.rb
62
+ - lib/prawn_html/tags/div.rb
63
+ - lib/prawn_html/tags/h.rb
64
+ - lib/prawn_html/tags/hr.rb
65
+ - lib/prawn_html/tags/i.rb
66
+ - lib/prawn_html/tags/img.rb
67
+ - lib/prawn_html/tags/li.rb
68
+ - lib/prawn_html/tags/mark.rb
69
+ - lib/prawn_html/tags/p.rb
70
+ - lib/prawn_html/tags/small.rb
71
+ - lib/prawn_html/tags/span.rb
72
+ - lib/prawn_html/tags/u.rb
73
+ - lib/prawn_html/tags/ul.rb
74
+ - lib/prawn_html/version.rb
75
+ homepage: https://github.com/blocknotes/prawn-html
76
+ licenses:
77
+ - MIT
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 2.5.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.1.4
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Prawn PDF - HTML renderer
98
+ test_files: []