prawn-html 0.1.0

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.
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: []