Skip to content

Commit

Permalink
Merge branch 'v0.4.0' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
blocknotes committed Aug 29, 2021
2 parents 326e330 + 4116bf3 commit 684d338
Show file tree
Hide file tree
Showing 45 changed files with 381 additions and 243 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ HTML tags:

- **a**: link
- **b**: bold
- **blockquote**: block quotation element
- **br**: new line
- **del**: strike-through
- **div**: block element
Expand Down Expand Up @@ -100,8 +101,7 @@ Some custom data attributes are used to pass options:

## Document styles

[Experimental feature] You can define document CSS rules inside an _head_ tag, but with a limited support for now.
Only single CSS selectors and basic ones are supported. Example:
You can define document CSS rules inside an _head_ tag. Example:

```html
<!DOCTYPE html>
Expand Down
4 changes: 4 additions & 0 deletions examples/misc_elements.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ <h2>Misc elements</h2>
<span>span element</span>
<span>span (2) element</span>

<blockquote>The Block Quotation element</blockquote>

<hr>

<ul>
<li>Level 1a</li>
<li>Level 1b
Expand Down
Binary file modified examples/misc_elements.pdf
Binary file not shown.
3 changes: 2 additions & 1 deletion lib/prawn-html.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,10 @@ def append_html(pdf, html)

require 'prawn_html/utils'

Dir["#{__dir__}/prawn_html/callbacks/*.rb"].sort.each { |f| require f }

require 'prawn_html/tag'
Dir["#{__dir__}/prawn_html/tags/*.rb"].sort.each { |f| require f }
Dir["#{__dir__}/prawn_html/callbacks/*.rb"].sort.each { |f| require f }

require 'prawn_html/attributes'
require 'prawn_html/context'
Expand Down
40 changes: 17 additions & 23 deletions lib/prawn_html/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ class Attributes < OpenStruct
'color' => { key: :color, set: :convert_color },
'font-family' => { key: :font, set: :unquote },
'font-size' => { key: :size, set: :convert_size },
'font-style' => { key: :styles, set: :append_symbol },
'font-weight' => { key: :styles, set: :append_symbol },
'font-style' => { key: :styles, set: :append_styles },
'font-weight' => { key: :styles, set: :append_styles },
'href' => { key: :link, set: :copy_value },
'letter-spacing' => { key: :character_spacing, set: :convert_float },
'list-style-type' => { key: :list_style_type, set: :unquote },
'text-decoration' => { key: :styles, set: :append_symbol },
'text-decoration' => { key: :styles, set: :append_styles },
# tag opening styles
'break-before' => { key: :break_before, set: :convert_symbol },
'margin-top' => { key: :margin_top, set: :convert_size },
Expand All @@ -50,10 +50,6 @@ class Attributes < OpenStruct
def initialize(attributes = {})
super
@styles = {} # result styles
return unless style

styles_hash = Attributes.parse_styles(style)
process_styles(styles_hash)
end

# Processes the data attributes
Expand All @@ -66,21 +62,12 @@ def data
end
end

# Merge already parsed styles
#
# @param parsed_styles [Hash] hash of parsed styles
def merge_styles!(parsed_styles)
@styles.merge!(parsed_styles)
end

# Processes the styles attributes
# Merge text styles
#
# @param styles_hash [Hash] hash of styles attributes
def process_styles(styles_hash)
styles_hash.each do |key, value|
apply_rule!(@styles, STYLES_LIST[key], value)
end
@styles
# @param text_styles [String] styles to parse and process
def merge_text_styles!(text_styles)
hash_styles = Attributes.parse_styles(text_styles)
process_styles(hash_styles) unless hash_styles.empty?
end

class << self
Expand Down Expand Up @@ -114,11 +101,18 @@ def parse_styles(styles)
def apply_rule!(result, rule, value)
return unless rule

if rule[:set] == :append_symbol
(result[rule[:key]] ||= []) << Utils.convert_symbol(value)
if rule[:set] == :append_styles
(result[rule[:key]] ||= []) << Utils.normalize_style(value)
else
result[rule[:key]] = Utils.send(rule[:set], value)
end
end

def process_styles(hash_styles)
hash_styles.each do |key, value|
apply_rule!(@styles, STYLES_LIST[key], value)
end
@styles
end
end
end
4 changes: 1 addition & 3 deletions lib/prawn_html/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ def add(element)
#
# @return [String] before content string
def before_content
return '' if empty? || !last.respond_to?(:tag_styles)

last.tag_styles[:before_content].to_s
(last.respond_to?(:before_content) && last.before_content) || ''
end

# Merges the context block styles
Expand Down
28 changes: 12 additions & 16 deletions lib/prawn_html/document_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,9 @@ class DocumentRenderer
def initialize(pdf)
@buffer = []
@context = Context.new
@document_styles = {}
@pdf = pdf
end

# Evaluate the document styles and store the internally
#
# @param styles [Hash] styles hash with CSS selectors as keys and rules as values
def assign_document_styles(styles)
@document_styles.merge!(
styles.transform_values do |style_rules|
Attributes.new(style: style_rules).styles
end
)
end

# On tag close callback
#
# @param element [Tag] closing element wrapper
Expand All @@ -40,13 +28,14 @@ def on_tag_close(element)
#
# @param tag_name [String] the tag name of the opening element
# @param attributes [Hash] an hash of the element attributes
# @param element_styles [String] document styles to apply to the element
#
# @return [Tag] the opening element wrapper
def on_tag_open(tag_name, attributes)
def on_tag_open(tag_name, attributes:, element_styles: '')
tag_class = Tag.class_for(tag_name)
return unless tag_class

tag_class.new(tag_name, attributes, document_styles).tap do |element|
tag_class.new(tag_name, attributes: attributes, element_styles: element_styles).tap do |element|
setup_element(element)
end
end
Expand Down Expand Up @@ -79,7 +68,7 @@ def render

private

attr_reader :buffer, :context, :document_styles, :pdf
attr_reader :buffer, :context, :pdf

def setup_element(element)
add_space_if_needed unless render_if_needed(element)
Expand Down Expand Up @@ -115,13 +104,20 @@ def apply_tag_open_styles(element)
end

def output_content(buffer, block_styles)
buffer.each { |item| item[:callback] = item[:callback].new(pdf, item) if item[:callback] }
apply_callbacks(buffer)
left_indent = block_styles[:margin_left].to_f + block_styles[:padding_left].to_f
options = block_styles.slice(:align, :leading, :mode, :padding_left)
options[:indent_paragraphs] = left_indent if left_indent > 0
pdf.puts(buffer, options, bounding_box: bounds(block_styles))
end

def apply_callbacks(buffer)
buffer.select { |item| item[:callback] }.each do |item|
callback = Tag::CALLBACKS[item[:callback]]
item[:callback] = callback.new(pdf, item)
end
end

def bounds(block_styles)
return unless block_styles[:position] == :absolute

Expand Down
45 changes: 35 additions & 10 deletions lib/prawn_html/html_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,38 @@

module PrawnHtml
class HtmlParser
REGEXP_STYLES = /\s*([^{\s]+)\s*{\s*([^}]*?)\s*}/m.freeze

# Init the HtmlParser
#
# @param renderer [DocumentRenderer] document renderer
def initialize(renderer)
# @param ignore_content_tags [Array] array of tags (symbols) to skip their contents while preparing the PDF document
def initialize(renderer, ignore_content_tags: %i[script style])
@processing = false
@ignore = false
@ignore_content_tags = ignore_content_tags
@renderer = renderer
@styles = {}
end

# Processes HTML and renders it
#
# @param html [String] The HTML content to process
def process(html)
@processing = !html.include?('<body')
doc = Oga.parse_html(html)
traverse_nodes(doc.children)
@document = Oga.parse_html(html)
traverse_nodes(document.children)
renderer.flush
end

private

attr_reader :processing, :renderer
attr_reader :document, :ignore, :processing, :renderer, :styles

def traverse_nodes(nodes)
nodes.each do |node|
next if node.is_a?(Oga::XML::Comment)

element = node_open(node)
traverse_nodes(node.children) if node.children.any?
node_close(element) if element
Expand All @@ -37,21 +45,27 @@ def traverse_nodes(nodes)
def node_open(node)
tag = node.is_a?(Oga::XML::Element) && init_element(node)
return unless processing
return IgnoredTag.new(tag) if ignore
return renderer.on_text_node(node.text) unless tag

attributes = prepare_attributes(node)
renderer.on_tag_open(tag, attributes)
renderer.on_tag_open(tag, attributes: prepare_attributes(node), element_styles: styles[node])
end

def init_element(node)
node.name.downcase.to_sym.tap do |tag_name|
@processing = true if tag_name == :body
renderer.assign_document_styles(extract_styles(node.text)) if tag_name == :style
@ignore = true if @processing && @ignore_content_tags.include?(tag_name)
process_styles(node.text) if tag_name == :style
end
end

def extract_styles(text)
text.scan(/\s*([^{\s]+)\s*{\s*([^}]*?)\s*}/m).to_h
def process_styles(text_styles)
hash_styles = text_styles.scan(REGEXP_STYLES).to_h
hash_styles.each do |selector, rule|
document.css(selector).each do |node|
styles[node] = rule
end
end
end

def prepare_attributes(node)
Expand All @@ -61,10 +75,21 @@ def prepare_attributes(node)
end

def node_close(element)
renderer.on_tag_close(element) if @processing
if processing
renderer.on_tag_close(element) unless ignore
@ignore = false if ignore && @ignore_content_tags.include?(element.tag)
end
@processing = false if element.tag == :body
end
end

class IgnoredTag
attr_accessor :tag

def initialize(tag_name)
@tag = tag_name
end
end

HtmlHandler = HtmlParser
end
34 changes: 12 additions & 22 deletions lib/prawn_html/tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

module PrawnHtml
class Tag
TAG_CLASSES = %w[A B Body Br Del Div H Hr I Img Li Mark Ol P Small Span U Ul].freeze
CALLBACKS = {
'Highlight' => Callbacks::Highlight,
'StrikeThrough' => Callbacks::StrikeThrough
}.freeze
TAG_CLASSES = %w[A B Blockquote Body Br Del Div H Hr I Img Li Mark Ol P Small Span U Ul].freeze

attr_accessor :parent
attr_reader :attrs, :tag
Expand All @@ -11,12 +15,11 @@ class Tag
#
# @param tag [Symbol] tag name
# @param attributes [Hash] hash of element attributes
# @param document_styles [Hash] hash of document styles
def initialize(tag, attributes = {}, document_styles = {})
# @param element_styles [String] document styles tp apply to the element
def initialize(tag, attributes: {}, element_styles: '')
@tag = tag
element_styles = attributes.delete(:style)
@attrs = Attributes.new(attributes)
process_styles(document_styles, element_styles)
process_styles(element_styles, attributes['style'])
end

# Is a block tag?
Expand Down Expand Up @@ -74,23 +77,10 @@ def class_for(tag_name)

private

def evaluate_document_styles(document_styles)
selectors = [
tag.to_s,
attrs['class'] ? ".#{attrs['class']}" : nil,
attrs['id'] ? "##{attrs['id']}" : nil
].compact!
document_styles.each_with_object({}) do |(sel, attributes), res|
res.merge!(attributes) if selectors.include?(sel)
end
end

def process_styles(document_styles, element_styles)
attrs.merge_styles!(attrs.process_styles(tag_styles)) if respond_to?(:tag_styles)
doc_styles = evaluate_document_styles(document_styles)
attrs.merge_styles!(doc_styles)
el_styles = Attributes.parse_styles(element_styles)
attrs.merge_styles!(attrs.process_styles(el_styles)) if el_styles
def process_styles(element_styles, inline_styles)
attrs.merge_text_styles!(tag_styles) if respond_to?(:tag_styles)
attrs.merge_text_styles!(element_styles)
attrs.merge_text_styles!(inline_styles)
end
end
end
2 changes: 1 addition & 1 deletion lib/prawn_html/tags/a.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class A < Tag
ELEMENTS = [:a].freeze

def tag_styles
attrs.href ? { 'href' => attrs.href } : {}
"href: #{attrs.href}" if attrs.href
end
end
end
Expand Down
4 changes: 1 addition & 3 deletions lib/prawn_html/tags/b.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ class B < Tag
ELEMENTS = [:b, :strong].freeze

def tag_styles
{
'font-weight' => 'bold'
}
'font-weight: bold'
end
end
end
Expand Down
25 changes: 25 additions & 0 deletions lib/prawn_html/tags/blockquote.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module PrawnHtml
module Tags
class Blockquote < Tag
ELEMENTS = [:blockquote].freeze

MARGIN_BOTTOM = 10
MARGIN_LEFT = 25
MARGIN_TOP = 10

def block?
true
end

def tag_styles
<<~STYLES
margin-bottom: #{MARGIN_BOTTOM}px;
margin-left: #{MARGIN_LEFT}px;
margin-top: #{MARGIN_TOP}px;
STYLES
end
end
end
end
Loading

0 comments on commit 684d338

Please sign in to comment.