├── .fasterer.yml
├── .github
├── FUNDING.yml
└── workflows
│ ├── linters.yml
│ └── specs.yml
├── .gitignore
├── .reviewdog.yml
├── .rspec
├── .rubocop.yml
├── Gemfile
├── LICENSE.txt
├── README.md
├── bin
├── fasterer
├── rspec
└── rubocop
├── examples
├── elements.html
├── elements.pdf
├── examples
│ └── image.jpg
├── headings.html
├── headings.pdf
├── image.jpg
├── instance.pdf
├── random_content.html
├── random_content.pdf
├── samples.rb
├── styles.html
└── styles.pdf
├── lib
├── prawn-html.rb
└── prawn_html
│ ├── attributes.rb
│ ├── callbacks
│ ├── background.rb
│ └── strike_through.rb
│ ├── context.rb
│ ├── document_renderer.rb
│ ├── html_parser.rb
│ ├── instance.rb
│ ├── pdf_wrapper.rb
│ ├── tag.rb
│ ├── tags
│ ├── a.rb
│ ├── b.rb
│ ├── blockquote.rb
│ ├── body.rb
│ ├── br.rb
│ ├── code.rb
│ ├── del.rb
│ ├── div.rb
│ ├── h.rb
│ ├── hr.rb
│ ├── i.rb
│ ├── img.rb
│ ├── li.rb
│ ├── mark.rb
│ ├── ol.rb
│ ├── p.rb
│ ├── pre.rb
│ ├── small.rb
│ ├── span.rb
│ ├── sub.rb
│ ├── sup.rb
│ ├── u.rb
│ └── ul.rb
│ ├── utils.rb
│ └── version.rb
├── prawn-html.gemspec
└── spec
├── features
├── instance_spec.rb
└── samples_spec.rb
├── integrations
├── blocks_spec.rb
├── headings_spec.rb
├── lists_spec.rb
├── misc_spec.rb
├── styles_spec.rb
└── tags
│ └── br_spec.rb
├── spec_helper.rb
├── support
├── common_checks.rb
├── shared_contexts.rb
└── test_utils.rb
└── units
├── prawn_html
├── attributes_spec.rb
├── context_spec.rb
├── document_renderer_spec.rb
├── html_parser_spec.rb
├── instance_spec.rb
├── pdf_wrapper_spec.rb
├── tag_spec.rb
├── tags
│ ├── a_spec.rb
│ ├── b_spec.rb
│ ├── blockquote_spec.rb
│ ├── body_spec.rb
│ ├── br_spec.rb
│ ├── code_spec.rb
│ ├── del_spec.rb
│ ├── div_spec.rb
│ ├── h_spec.rb
│ ├── hr_spec.rb
│ ├── i_spec.rb
│ ├── img_spec.rb
│ ├── li_spec.rb
│ ├── mark_spec.rb
│ ├── ol_spec.rb
│ ├── p_spec.rb
│ ├── pre_spec.rb
│ ├── small_spec.rb
│ ├── span_spec.rb
│ ├── sub_spec.rb
│ ├── sup_spec.rb
│ ├── u_spec.rb
│ └── ul_spec.rb
└── utils_spec.rb
├── prawn_html_spec.rb
└── styles_spec.rb
/.fasterer.yml:
--------------------------------------------------------------------------------
1 | ---
2 | exclude_paths:
3 | - bin/*
4 | - vendor/**/*
5 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [blocknotes]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/workflows/linters.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Linters
3 |
4 | on:
5 | push:
6 | branches:
7 | - main
8 | pull_request:
9 |
10 | jobs:
11 | reviewdog:
12 | name: reviewdog
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Check out code
17 | uses: actions/checkout@v2
18 |
19 | - name: Set up Ruby
20 | uses: ruby/setup-ruby@v1
21 | with:
22 | ruby-version: '2.7'
23 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
24 |
25 | - uses: reviewdog/action-setup@v1
26 | with:
27 | reviewdog_version: latest
28 |
29 | - name: Run reviewdog
30 | env:
31 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 | run: |
33 | reviewdog -fail-on-error -reporter=github-pr-review -runners=fasterer,rubocop
34 |
35 | # NOTE: check with: reviewdog -fail-on-error -reporter=github-pr-review -runners=fasterer -diff="git diff" -tee
36 |
--------------------------------------------------------------------------------
/.github/workflows/specs.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Specs
3 |
4 | on:
5 | push:
6 | branches: [main]
7 | pull_request:
8 | branches: [main]
9 |
10 | jobs:
11 | tests:
12 | runs-on: ubuntu-latest
13 |
14 | strategy:
15 | matrix:
16 | ruby: ['2.5', '2.6', '2.7', '3.0']
17 |
18 | steps:
19 | - name: Checkout repository
20 | uses: actions/checkout@v2
21 |
22 | - name: Set up Ruby
23 | uses: ruby/setup-ruby@v1
24 | with:
25 | ruby-version: ${{ matrix.ruby }}
26 | bundler-cache: true
27 |
28 | - name: Run tests
29 | run: bundle exec rspec --profile
30 |
31 | - name: Code Climate test coverage
32 | uses: paambaati/codeclimate-action@v3.0.0
33 | env:
34 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
35 | with:
36 | coverageLocations: |
37 | ${{github.workspace}}/coverage/lcov/prawn-html.lcov:lcov
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | _misc/
2 | coverage/
3 |
4 | Gemfile.lock
5 |
6 | .rubocop-*
7 | .vscode
8 |
--------------------------------------------------------------------------------
/.reviewdog.yml:
--------------------------------------------------------------------------------
1 | ---
2 | runner:
3 | fasterer:
4 | cmd: bin/fasterer
5 | level: info
6 | rubocop:
7 | cmd: bin/rubocop
8 | level: info
9 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | ---
2 | inherit_from:
3 | - https://relaxed.ruby.style/rubocop.yml
4 |
5 | require:
6 | - rubocop-packaging
7 | - rubocop-performance
8 | - rubocop-rspec
9 |
10 | AllCops:
11 | TargetRubyVersion: 2.5
12 | Exclude:
13 | - _misc/*
14 | - bin/*
15 | - vendor/**/*
16 | NewCops: enable
17 |
18 | Lint/UnusedMethodArgument:
19 | Exclude:
20 | - lib/prawn_html/utils.rb
21 |
22 | Naming/FileName:
23 | Exclude:
24 | - lib/prawn-html.rb
25 |
26 | Naming/MethodParameterName:
27 | Exclude:
28 | - lib/prawn_html/pdf_wrapper.rb
29 |
30 | RSpec/ExampleLength:
31 | # default: 5
32 | Max: 15
33 |
34 | RSpec/MultipleMemoizedHelpers:
35 | # default: 5
36 | Max: 6
37 |
38 | RSpec/SubjectStub:
39 | Exclude:
40 | - spec/units/prawn_html/attributes_spec.rb
41 |
42 | Style/OpenStructUse:
43 | Exclude:
44 | - lib/prawn_html/attributes.rb
45 | - spec/units/prawn_html/attributes_spec.rb
46 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | gemspec
6 |
7 | group :development, :test do
8 | # Testing
9 | gem 'pdf-inspector', require: 'pdf/inspector'
10 | gem 'rspec'
11 | gem 'simplecov', require: false
12 | gem 'simplecov-lcov', require: false
13 |
14 | # Linters
15 | gem 'fasterer'
16 | gem 'rubocop'
17 | gem 'rubocop-packaging'
18 | gem 'rubocop-performance'
19 | gem 'rubocop-rspec'
20 |
21 | # Tools
22 | gem 'pry'
23 | end
24 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
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.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Prawn HTML
2 | [](https://rubygems.org/gems/prawn-html)
3 | [](https://rubygems.org/gems/prawn-html)
4 | [](https://codeclimate.com/github/blocknotes/prawn-html/maintainability)
5 | [](https://github.com/blocknotes/prawn-html/actions/workflows/linters.yml)
6 | [](https://github.com/blocknotes/prawn-html/actions/workflows/specs.yml)
7 |
8 | HTML to PDF renderer using [Prawn PDF](https://github.com/prawnpdf/prawn).
9 |
10 | Features:
11 | - support a [good set](#supported-tags--attributes) of HTML tags and CSS properties;
12 | - handle [document styles](#document-styles);
13 | - custom [data attributes](#data-attributes) for Prawn PDF features;
14 | - no extra settings: it just parses an input HTML and outputs to a Prawn PDF document.
15 |
16 | **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* or *PDFKit*.
17 |
18 | > [prawn-styled-text](https://github.com/blocknotes/prawn-styled-text) rewritten from scratch, finally!
19 |
20 | Please :star: if you like it.
21 |
22 | ## Install
23 |
24 | - Add to your Gemfile: `gem 'prawn-html'` (and execute `bundle`)
25 | - Just call `PrawnHtml.append_html` on a `Prawn::Document` instance (see the examples)
26 |
27 | ## Examples
28 |
29 | ```rb
30 | require 'prawn-html'
31 | pdf = Prawn::Document.new(page_size: 'A4')
32 | PrawnHtml.append_html(pdf, '
Just a test
')
33 | pdf.render_file('test.pdf')
34 | ```
35 |
36 | To check some examples with the PDF output see [examples](examples/) folder.
37 |
38 | Alternative form using _PrawnHtml::Instance_ to preserve the context:
39 |
40 | ```rb
41 | require 'prawn-html'
42 | pdf = Prawn::Document.new(page_size: 'A4')
43 | phtml = PrawnHtml::Instance.new(pdf)
44 | css = <<~CSS
45 | h1 { color: green }
46 | i { color: red }
47 | CSS
48 | phtml.append(css: css)
49 | phtml.append(html: 'Some HTML before
')
50 | pdf.text 'Some Prawn text'
51 | phtml.append(html: 'Some HTML after
')
52 | pdf.render_file('test.pdf')
53 | ```
54 |
55 | ## Supported tags & attributes
56 |
57 | HTML tags (using MDN definitions):
58 |
59 | - **a**: the Anchor element
60 | - **b**: the Bring Attention To element
61 | - **blockquote**: the Block Quotation element
62 | - **br**: the Line Break element
63 | - **code**: the Inline Code element
64 | - **del**: the Deleted Text element
65 | - **div**: the Content Division element
66 | - **em**: the Emphasis element
67 | - **h1** - **h6**: the HTML Section Heading elements
68 | - **hr**: the Thematic Break (Horizontal Rule) element
69 | - **i**: the Idiomatic Text element
70 | - **ins**: the added text element
71 | - **img**: the Image Embed element
72 | - **li**: the list item element
73 | - **mark**: the Mark Text element
74 | - **ol**: the Ordered List element
75 | - **p**: the Paragraph element
76 | - **pre**: the Preformatted Text element
77 | - **s**: the strike-through text element
78 | - **small**: the side comment element
79 | - **span**: the generic inline element
80 | - **strong**: the Strong Importance element
81 | - **sub**: the Subscript element
82 | - **sup**: the Superscript element
83 | - **u**: the Unarticulated Annotation (Underline) element
84 | - **ul**: the Unordered List element
85 |
86 | CSS attributes (dimensional units are ignored and considered in pixel):
87 |
88 | - **background**: for *mark* tag (3/6 hex digits or RGB or color name), ex. `style="background: #FECD08"`
89 | - **break-after**: go to a new page after some elements, ex. `style="break-after: auto"`
90 | - **break-before**: go to a new page before some elements, ex. `style="break-before: auto"`
91 | - **color**: (3/6 hex digits or RGB or color name) ex. `style="color: #FB1"`
92 | - **font-family**: font must be registered, quotes are optional, ex. `style="font-family: Courier"`
93 | - **font-size**: ex. `style="font-size: 20px"`
94 | - **font-style**: values: *:italic*, ex. `style="font-style: italic"`
95 | - **font-weight**: values: *:bold*, ex. `style="font-weight: bold"`
96 | - **height**: for *img* tag, ex. `
`
97 | - **href**: for *a* tag, ex. `Google`
98 | - **left**: see *position (absolute)*
99 | - **letter-spacing**: ex. `style="letter-spacing: 1.5"`
100 | - **line-height**: ex. `style="line-height: 10px"`
101 | - **list-style-type**: for *ul*, a string, ex. `style="list-style-type: '- '"`
102 | - **margin-bottom**: ex. `style="margin-bottom: 10px"`
103 | - **margin-left**: ex. `style="margin-left: 15px"`
104 | - **margin-top**: ex. `style="margin-top: 20px"`
105 | - **position**: `absolute`, ex. `style="position: absolute; left: 20px; top: 100px"`
106 | - **src**: for *img* tag, ex. `
`
107 | - **text-align**: `left` | `center` | `right` | `justify`, ex. `style="text-align: center"`
108 | - **text-decoration**: `underline`, ex. `style="text-decoration: underline"`
109 | - **top**: see *position (absolute)*
110 | - **width**: for *img* tag, support also percentage, ex. `
`
111 |
112 | The above attributes supports the `initial` value to reset them to their original value.
113 |
114 | For colors, the supported formats are:
115 | - 3 hex digits, ex. `color: #FB1`;
116 | - 6 hex digits, ex. `color: #abcdef`;
117 | - RGB, ex. `color: RGB(64, 0, 128)`;
118 | - color name, ex. `color: yellow`.
119 |
120 | ## Data attributes
121 |
122 | Some custom data attributes are used to pass options:
123 |
124 | - **dash**: for *hr* tag, accepts an integer or a list of integers), ex. `data-data="2, 4, 3"`
125 | - **mode**: allow to specify the text mode (stroke|fill||fill_stroke), ex. `data-mode="stroke"`
126 |
127 | ## Document styles
128 |
129 | You can define document CSS rules inside an _head_ tag. Example:
130 |
131 | ```html
132 |
133 |
134 |
135 | A test
136 |
144 |
145 |
146 |
147 | Div content
148 | Span content
149 |
150 |
151 |
152 | ```
153 |
154 | ## Additional notes
155 |
156 | ### Rails: generate PDF on the fly
157 |
158 | Sample controller's action to create a PDF from Rails:
159 |
160 | ```rb
161 | class SomeController < ApplicationController
162 | def sample_action
163 | respond_to do |format|
164 | format.pdf do
165 | pdf = Prawn::Document.new
166 | PrawnHtml.append_html(pdf, 'Just a test
')
167 | send_data(pdf.render, filename: 'sample.pdf', type: 'application/pdf')
168 | end
169 | end
170 | end
171 | end
172 | ```
173 |
174 | More details in this blogpost: [generate PDF from HTML](https://www.blocknot.es/2021-08-20-rails-generate-pdf-from-html/)
175 |
176 | ## Do you like it? Star it!
177 |
178 | If you use this component just star it. A developer is more motivated to improve a project when there is some interest.
179 |
180 | Or consider offering me a coffee, it's a small thing but it is greatly appreciated: [about me](https://www.blocknot.es/about-me).
181 |
182 | ## Contributors
183 |
184 | - [Mattia Roccoberton](https://www.blocknot.es): author
185 |
186 | ## License
187 |
188 | The gem is available as open-source under the terms of the [MIT](LICENSE.txt).
189 |
--------------------------------------------------------------------------------
/bin/fasterer:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'fasterer' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("../bundle", __FILE__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("fasterer", "fasterer")
30 |
--------------------------------------------------------------------------------
/bin/rspec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'rspec' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("../bundle", __FILE__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("rspec-core", "rspec")
30 |
--------------------------------------------------------------------------------
/bin/rubocop:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'rubocop' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("../bundle", __FILE__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("rubocop", "rubocop")
30 |
--------------------------------------------------------------------------------
/examples/elements.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Supported elements
5 |
6 |
7 | tag A: link
8 | tag B: bold
9 | tag Blockquote: block quotation element
10 | tag Br: new line
11 | tag Code: inline code element
12 | tag Del: strike-through
13 | tag Div: block element
14 | tag Em: italic
15 | tag H1: heading h1
16 | tag H2: heading h2
17 | tag H3: heading h3
18 | tag H4: heading h4
19 | tag H5: heading h5
20 | tag H6: heading h6
21 | tag Hr: horizontal line
22 | tag I: italic
23 | tag Ins: underline
24 | tag Img:
image
25 | tag Mark: highlight
26 | tag Ol:
27 | - ordered list
28 | - tag Li: list item (ordered)
29 |
30 | tag P: block element
31 | tag Pre: preformatted text element
32 | tag S: strike-through
33 | tag Small: smaller text
34 | tag Span: inline element
35 | tag Strong: bold
36 | tag Sub: subscript element
37 | tag Sup: superscript element
38 | tag U: underline
39 | tag Ul:
40 | - unordered list
41 | - tag Li: list item (unordered)
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/examples/elements.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocknotes/prawn-html/4b339e6cc7cd79934af7e26adfe41a34280b2a26/examples/elements.pdf
--------------------------------------------------------------------------------
/examples/examples/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocknotes/prawn-html/4b339e6cc7cd79934af7e26adfe41a34280b2a26/examples/examples/image.jpg
--------------------------------------------------------------------------------
/examples/headings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Headings
5 |
6 |
7 |
8 |
h1 element
9 |
h2 element
10 |
h3 element
11 |
h4 element
12 |
h5 element
13 |
h6 element
14 |
15 |
16 |
17 |
Lorem ipsum dolor sit amet consectetur adipisicing elit...
18 |
h1 element
19 |
20 |
... laboriosam rerum sed veritatis, quisquam aliquid impedit...
21 |
h2 element
22 |
23 |
... architecto sunt fugiat magni molestias iste, nam quidem...
24 |
h3 element
25 |
26 |
... praesentium quaerat, eius eveniet minima facilis quos.
27 |
h4 element
28 |
29 |
Lorem ipsum dolor sit amet consectetur adipisicing elit...
30 |
h5 element
31 |
32 |
... suscipit saepe magni quasi voluptatum cupiditate, mollitia ipsum, qui rerum sint natus...
33 |
h6 element
34 |
35 |
... itaque perspiciatis. Delectus vero eveniet consectetur impedit necessitatibus animi dolore!
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/examples/headings.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocknotes/prawn-html/4b339e6cc7cd79934af7e26adfe41a34280b2a26/examples/headings.pdf
--------------------------------------------------------------------------------
/examples/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocknotes/prawn-html/4b339e6cc7cd79934af7e26adfe41a34280b2a26/examples/image.jpg
--------------------------------------------------------------------------------
/examples/instance.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocknotes/prawn-html/4b339e6cc7cd79934af7e26adfe41a34280b2a26/examples/instance.pdf
--------------------------------------------------------------------------------
/examples/random_content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Random content
5 |
6 |
7 | Random content (with various elements and styles)
8 |
9 | Test inline elements:
10 | this is italic, underline and bold, bold italic
11 |
12 |
13 |
14 |
15 |
16 | div element
17 | div (2) element
18 | span element
19 | span (2) element
20 |
21 | The Block Quotation element
22 |
23 |
24 |
25 | text-align: left
26 | text-align: center
27 | text-align: right
28 | text-align: justify
29 |
30 |
31 |
32 | margin-top
33 | margin-left
34 | margin-bottom
35 |
36 |
37 |
38 | Begin of list
39 |
40 | - Level 1a
41 | - Level 1b
42 |
43 | - Level 2a
44 | - Level 2b
45 |
46 | - Level 3a
47 | - Level 3b
48 | - Level 3c
49 |
50 |
51 | - Level 2c
52 |
53 |
54 | - Level 1c
55 |
56 | End of list
57 |
58 |
59 |
60 |
61 | - Li 1
62 | -
63 | Li 2
64 |
65 | - Li 2 - 1
66 | - Text that is bold and italic also.
67 | - Li 2 - 3
68 |
69 |
70 | - Li 3
71 |
72 |
73 |
74 |
75 | Some HTML entities: € × ÷ ½ « » ©
76 | A paragraph
77 | Another paragraph with highlight words and deleted words.
78 |
79 |
80 |
81 | A span with a color: this is courier red 20, this is green bold italic, this is character spacing 2.8, this is a Google link
82 |
83 |
84 | Line1
Line2
Line3
85 |
86 | Word1
87 | Word2
88 |
89 | Word3
90 |
91 |
92 | Line4
93 |
94 | More text
95 | line-height test... Lorem ipsum dolor sit amet,
96 | consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
97 | aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
98 | Duis aute irure dolor in reprehenderit in voluptate velit.
99 |
100 |
101 |
102 | This is an inline code
element.
103 | this is pre element
104 | last line.
105 |
106 |
107 | An image:
108 |

109 |
110 | Some other paragraph
111 |
112 |
113 | Position: absolute
114 |
115 |
116 | New page
117 |
118 | End of document
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/examples/random_content.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocknotes/prawn-html/4b339e6cc7cd79934af7e26adfe41a34280b2a26/examples/random_content.pdf
--------------------------------------------------------------------------------
/examples/samples.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | $LOAD_PATH << '../lib'
4 |
5 | require 'prawn'
6 | require 'prawn-html'
7 | require 'oga'
8 | require 'pry'
9 |
10 | Dir[File.expand_path('*.html', __dir__)].sort.each do |file|
11 | html = File.read(file)
12 | pdf = Prawn::Document.new(page_size: 'A4', page_layout: :portrait)
13 | PrawnHtml.append_html(pdf, html)
14 | out = file.gsub(/\.html\Z/, '.pdf')
15 | pdf.render_file(out)
16 | end
17 |
--------------------------------------------------------------------------------
/examples/styles.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Styles
5 |
18 |
19 |
20 | CSS rules
21 | a div
22 |
23 |
div with id
24 |
div with class
25 |
26 | another div
27 | a p
28 |
29 | Padding and margin
30 |
31 | padding top, bottom and left (outer)
32 |
33 | padding top, bottom and left (inner)
34 |
35 |
36 |
37 | Some content
38 |
39 | margin top, bottom and left (outer)
40 |
41 | margin top, bottom and left (inner)
42 |
43 |
44 | Some content
45 |
46 |
47 |
--------------------------------------------------------------------------------
/examples/styles.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocknotes/prawn-html/4b339e6cc7cd79934af7e26adfe41a34280b2a26/examples/styles.pdf
--------------------------------------------------------------------------------
/lib/prawn-html.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | ADJUST_LEADING = { nil => 0.18, 'Courier' => -0.07, 'Helvetica' => -0.17, 'Times-Roman' => 0.03 }.freeze
5 | PX = 0.6 # conversion constant for pixel sixes
6 |
7 | COLORS = {
8 | 'aliceblue' => 'f0f8ff',
9 | 'antiquewhite' => 'faebd7',
10 | 'aqua' => '00ffff',
11 | 'aquamarine' => '7fffd4',
12 | 'azure' => 'f0ffff',
13 | 'beige' => 'f5f5dc',
14 | 'bisque' => 'ffe4c4',
15 | 'black' => '000000',
16 | 'blanchedalmond' => 'ffebcd',
17 | 'blue' => '0000ff',
18 | 'blueviolet' => '8a2be2',
19 | 'brown' => 'a52a2a',
20 | 'burlywood' => 'deb887',
21 | 'cadetblue' => '5f9ea0',
22 | 'chartreuse' => '7fff00',
23 | 'chocolate' => 'd2691e',
24 | 'coral' => 'ff7f50',
25 | 'cornflowerblue' => '6495ed',
26 | 'cornsilk' => 'fff8dc',
27 | 'crimson' => 'dc143c',
28 | 'cyan' => '00ffff',
29 | 'darkblue' => '00008b',
30 | 'darkcyan' => '008b8b',
31 | 'darkgoldenrod' => 'b8860b',
32 | 'darkgray' => 'a9a9a9',
33 | 'darkgreen' => '006400',
34 | 'darkgrey' => 'a9a9a9',
35 | 'darkkhaki' => 'bdb76b',
36 | 'darkmagenta' => '8b008b',
37 | 'darkolivegreen' => '556b2f',
38 | 'darkorange' => 'ff8c00',
39 | 'darkorchid' => '9932cc',
40 | 'darkred' => '8b0000',
41 | 'darksalmon' => 'e9967a',
42 | 'darkseagreen' => '8fbc8f',
43 | 'darkslateblue' => '483d8b',
44 | 'darkslategray' => '2f4f4f',
45 | 'darkslategrey' => '2f4f4f',
46 | 'darkturquoise' => '00ced1',
47 | 'darkviolet' => '9400d3',
48 | 'deeppink' => 'ff1493',
49 | 'deepskyblue' => '00bfff',
50 | 'dimgray' => '696969',
51 | 'dimgrey' => '696969',
52 | 'dodgerblue' => '1e90ff',
53 | 'firebrick' => 'b22222',
54 | 'floralwhite' => 'fffaf0',
55 | 'forestgreen' => '228b22',
56 | 'fuchsia' => 'ff00ff',
57 | 'gainsboro' => 'dcdcdc',
58 | 'ghostwhite' => 'f8f8ff',
59 | 'gold' => 'ffd700',
60 | 'goldenrod' => 'daa520',
61 | 'gray' => '808080',
62 | 'green' => '008000',
63 | 'greenyellow' => 'adff2f',
64 | 'grey' => '808080',
65 | 'honeydew' => 'f0fff0',
66 | 'hotpink' => 'ff69b4',
67 | 'indianred' => 'cd5c5c',
68 | 'indigo' => '4b0082',
69 | 'ivory' => 'fffff0',
70 | 'khaki' => 'f0e68c',
71 | 'lavender' => 'e6e6fa',
72 | 'lavenderblush' => 'fff0f5',
73 | 'lawngreen' => '7cfc00',
74 | 'lemonchiffon' => 'fffacd',
75 | 'lightblue' => 'add8e6',
76 | 'lightcoral' => 'f08080',
77 | 'lightcyan' => 'e0ffff',
78 | 'lightgoldenrodyellow' => 'fafad2',
79 | 'lightgray' => 'd3d3d3',
80 | 'lightgreen' => '90ee90',
81 | 'lightgrey' => 'd3d3d3',
82 | 'lightpink' => 'ffb6c1',
83 | 'lightsalmon' => 'ffa07a',
84 | 'lightseagreen' => '20b2aa',
85 | 'lightskyblue' => '87cefa',
86 | 'lightslategray' => '778899',
87 | 'lightslategrey' => '778899',
88 | 'lightsteelblue' => 'b0c4de',
89 | 'lightyellow' => 'ffffe0',
90 | 'lime' => '00ff00',
91 | 'limegreen' => '32cd32',
92 | 'linen' => 'faf0e6',
93 | 'magenta' => 'ff00ff',
94 | 'maroon' => '800000',
95 | 'mediumaquamarine' => '66cdaa',
96 | 'mediumblue' => '0000cd',
97 | 'mediumorchid' => 'ba55d3',
98 | 'mediumpurple' => '9370db',
99 | 'mediumseagreen' => '3cb371',
100 | 'mediumslateblue' => '7b68ee',
101 | 'mediumspringgreen' => '00fa9a',
102 | 'mediumturquoise' => '48d1cc',
103 | 'mediumvioletred' => 'c71585',
104 | 'midnightblue' => '191970',
105 | 'mintcream' => 'f5fffa',
106 | 'mistyrose' => 'ffe4e1',
107 | 'moccasin' => 'ffe4b5',
108 | 'navajowhite' => 'ffdead',
109 | 'navy' => '000080',
110 | 'oldlace' => 'fdf5e6',
111 | 'olive' => '808000',
112 | 'olivedrab' => '6b8e23',
113 | 'orange' => 'ffa500',
114 | 'orangered' => 'ff4500',
115 | 'orchid' => 'da70d6',
116 | 'palegoldenrod' => 'eee8aa',
117 | 'palegreen' => '98fb98',
118 | 'paleturquoise' => 'afeeee',
119 | 'palevioletred' => 'db7093',
120 | 'papayawhip' => 'ffefd5',
121 | 'peachpuff' => 'ffdab9',
122 | 'peru' => 'cd853f',
123 | 'pink' => 'ffc0cb',
124 | 'plum' => 'dda0dd',
125 | 'powderblue' => 'b0e0e6',
126 | 'purple' => '800080',
127 | 'rebeccapurple' => '663399',
128 | 'red' => 'ff0000',
129 | 'rosybrown' => 'bc8f8f',
130 | 'royalblue' => '4169e1',
131 | 'saddlebrown' => '8b4513',
132 | 'salmon' => 'fa8072',
133 | 'sandybrown' => 'f4a460',
134 | 'seagreen' => '2e8b57',
135 | 'seashell' => 'fff5ee',
136 | 'sienna' => 'a0522d',
137 | 'silver' => 'c0c0c0',
138 | 'skyblue' => '87ceeb',
139 | 'slateblue' => '6a5acd',
140 | 'slategray' => '708090',
141 | 'slategrey' => '708090',
142 | 'snow' => 'fffafa',
143 | 'springgreen' => '00ff7f',
144 | 'steelblue' => '4682b4',
145 | 'tan' => 'd2b48c',
146 | 'teal' => '008080',
147 | 'thistle' => 'd8bfd8',
148 | 'tomato' => 'ff6347',
149 | 'turquoise' => '40e0d0',
150 | 'violet' => 'ee82ee',
151 | 'wheat' => 'f5deb3',
152 | 'white' => 'ffffff',
153 | 'whitesmoke' => 'f5f5f5',
154 | 'yellow' => 'ffff00',
155 | 'yellowgreen' => '9acd32'
156 | }.freeze
157 |
158 | def append_html(pdf, html)
159 | PrawnHtml::Instance.new(pdf).append(html: html)
160 | end
161 |
162 | module_function :append_html
163 | end
164 |
165 | require 'prawn'
166 |
167 | require 'prawn_html/utils'
168 |
169 | Dir["#{__dir__}/prawn_html/callbacks/*.rb"].sort.each { |f| require f }
170 |
171 | require 'prawn_html/tag'
172 | Dir["#{__dir__}/prawn_html/tags/*.rb"].sort.each { |f| require f }
173 |
174 | require 'prawn_html/attributes'
175 | require 'prawn_html/context'
176 | require 'prawn_html/pdf_wrapper'
177 | require 'prawn_html/document_renderer'
178 | require 'prawn_html/html_parser'
179 | require 'prawn_html/instance'
180 |
--------------------------------------------------------------------------------
/lib/prawn_html/attributes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'ostruct'
4 | require 'set'
5 |
6 | module PrawnHtml
7 | class Attributes < OpenStruct
8 | attr_reader :initial, :styles
9 |
10 | STYLES_APPLY = {
11 | block: %i[align bottom leading left margin_left padding_left position right top],
12 | tag_close: %i[margin_bottom padding_bottom break_after],
13 | tag_open: %i[margin_top padding_top break_before],
14 | text_node: %i[callback character_spacing color font link list_style_type size styles white_space]
15 | }.freeze
16 |
17 | STYLES_LIST = {
18 | # text node styles
19 | 'background' => { key: :callback, set: :callback_background },
20 | 'color' => { key: :color, set: :convert_color },
21 | 'font-family' => { key: :font, set: :filter_font_family },
22 | 'font-size' => { key: :size, set: :convert_size },
23 | 'font-style' => { key: :styles, set: :append_styles, values: %i[italic] },
24 | 'font-weight' => { key: :styles, set: :append_styles, values: %i[bold] },
25 | 'href' => { key: :link, set: :copy_value },
26 | 'letter-spacing' => { key: :character_spacing, set: :convert_float },
27 | 'list-style-type' => { key: :list_style_type, set: :unquote },
28 | 'text-decoration' => { key: :styles, set: :append_styles, values: %i[underline] },
29 | 'vertical-align' => { key: :styles, set: :append_styles, values: %i[subscript superscript] },
30 | 'white-space' => { key: :white_space, set: :convert_symbol },
31 | # tag opening styles
32 | 'break-before' => { key: :break_before, set: :convert_symbol },
33 | 'margin-top' => { key: :margin_top, set: :convert_size },
34 | 'padding-top' => { key: :padding_top, set: :convert_size },
35 | # tag closing styles
36 | 'break-after' => { key: :break_after, set: :convert_symbol },
37 | 'margin-bottom' => { key: :margin_bottom, set: :convert_size },
38 | 'padding-bottom' => { key: :padding_bottom, set: :convert_size },
39 | # block styles
40 | 'bottom' => { key: :bottom, set: :convert_size, options: :height },
41 | 'left' => { key: :left, set: :convert_size, options: :width },
42 | 'line-height' => { key: :leading, set: :convert_size },
43 | 'margin-left' => { key: :margin_left, set: :convert_size },
44 | 'padding-left' => { key: :padding_left, set: :convert_size },
45 | 'position' => { key: :position, set: :convert_symbol },
46 | 'right' => { key: :right, set: :convert_size, options: :width },
47 | 'text-align' => { key: :align, set: :convert_symbol },
48 | 'top' => { key: :top, set: :convert_size, options: :height },
49 | # special styles
50 | 'text-decoration-line-through' => { key: :callback, set: :callback_strike_through }
51 | }.freeze
52 |
53 | STYLES_MERGE = %i[margin_left padding_left].freeze
54 |
55 | # Init the Attributes
56 | def initialize(attributes = {})
57 | super
58 | @styles = {} # result styles
59 | @initial = Set.new
60 | end
61 |
62 | # Processes the data attributes
63 | #
64 | # @return [Hash] hash of data attributes with 'data-' prefix removed and stripped values
65 | def data
66 | to_h.each_with_object({}) do |(key, value), res|
67 | data_key = key.match /\Adata-(.+)/
68 | res[data_key[1]] = value.strip if data_key
69 | end
70 | end
71 |
72 | # Merge text styles
73 | #
74 | # @param text_styles [String] styles to parse and process
75 | # @param options [Hash] options (container width/height/etc.)
76 | def merge_text_styles!(text_styles, options: {})
77 | hash_styles = Attributes.parse_styles(text_styles)
78 | process_styles(hash_styles, options: options) unless hash_styles.empty?
79 | end
80 |
81 | # Remove an attribute value from the context styles
82 | #
83 | # @param context_styles [Hash] hash of the context styles that will be updated
84 | # @param rule [Hash] rule from the STYLES_LIST to lookup in the context style for value removal
85 | def remove_value(context_styles, rule)
86 | if rule[:set] == :append_styles
87 | context_styles[rule[:key]] -= rule[:values] if context_styles[:styles]
88 | else
89 | default = Context::DEFAULT_STYLES[rule[:key]]
90 | default ? (context_styles[rule[:key]] = default) : context_styles.delete(rule[:key])
91 | end
92 | end
93 |
94 | # Update context styles applying the initial rules (if set)
95 | #
96 | # @param context_styles [Hash] hash of the context styles that will be updated
97 | #
98 | # @return [Hash] the update context styles
99 | def update_styles(context_styles)
100 | initial.each do |rule|
101 | next unless rule
102 |
103 | remove_value(context_styles, rule)
104 | end
105 | context_styles
106 | end
107 |
108 | class << self
109 | # Merges attributes
110 | #
111 | # @param attributes [Hash] target attributes hash
112 | # @param key [Symbol] key
113 | # @param value
114 | #
115 | # @return [Hash] the updated hash of attributes
116 | def merge_attr!(attributes, key, value)
117 | return unless key
118 | return (attributes[key] = value) unless Attributes::STYLES_MERGE.include?(key)
119 |
120 | attributes[key] ||= 0
121 | attributes[key] += value
122 | end
123 |
124 | # Parses a string of styles
125 | #
126 | # @param styles [String] styles to parse
127 | #
128 | # @return [Hash] hash of styles
129 | def parse_styles(styles)
130 | (styles || '').scan(/\s*([^:;]+)\s*:\s*([^;]+)\s*/).to_h
131 | end
132 | end
133 |
134 | private
135 |
136 | def process_styles(hash_styles, options:)
137 | hash_styles.each do |key, value|
138 | rule = evaluate_rule(key, value)
139 | next unless rule
140 |
141 | apply_rule!(merged_styles: @styles, rule: rule, value: value, options: options)
142 | end
143 | @styles
144 | end
145 |
146 | def evaluate_rule(rule_key, attr_value)
147 | key = nil
148 | key = 'text-decoration-line-through' if rule_key == 'text-decoration' && attr_value == 'line-through'
149 | key ||= rule_key
150 | STYLES_LIST[key]
151 | end
152 |
153 | def apply_rule!(merged_styles:, rule:, value:, options:)
154 | return (@initial << rule) if value == 'initial'
155 |
156 | if rule[:set] == :append_styles
157 | val = Utils.normalize_style(value, rule[:values])
158 | (merged_styles[rule[:key]] ||= []) << val if val
159 | else
160 | opts = rule[:options] ? options[rule[:options]] : nil
161 | val = Utils.send(rule[:set], value, options: opts)
162 | merged_styles[rule[:key]] = val if val
163 | end
164 | end
165 | end
166 | end
167 |
--------------------------------------------------------------------------------
/lib/prawn_html/callbacks/background.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Callbacks
5 | class Background
6 | DEF_HIGHLIGHT = 'ffff00'
7 |
8 | def initialize(pdf, color = nil)
9 | @pdf = pdf
10 | @color = color || DEF_HIGHLIGHT
11 | end
12 |
13 | def render_behind(fragment)
14 | top, left = fragment.top_left
15 | @pdf.draw_rectangle(x: left, y: top, width: fragment.width, height: fragment.height, color: @color)
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/prawn_html/callbacks/strike_through.rb:
--------------------------------------------------------------------------------
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 | x1 = fragment.left
12 | x2 = fragment.right
13 | y = (fragment.top + fragment.bottom) / 2
14 | @pdf.underline(x1: x1, x2: x2, y: y)
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/prawn_html/context.rb:
--------------------------------------------------------------------------------
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
101 |
--------------------------------------------------------------------------------
/lib/prawn_html/document_renderer.rb:
--------------------------------------------------------------------------------
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 | return if context.previous_tag&.block? && content.match?(/\A\s*\Z/)
58 |
59 | text = prepare_text(content)
60 | buffer << context.merged_styles.merge(text: text) unless text.empty?
61 | context.last_text_node = true
62 | nil
63 | end
64 |
65 | # Render the buffer content to the PDF document
66 | def render
67 | return if buffer.empty?
68 |
69 | output_content(buffer.dup, context.block_styles)
70 | buffer.clear
71 | @last_margin = 0
72 | end
73 |
74 | alias_method :flush, :render
75 |
76 | private
77 |
78 | attr_reader :buffer, :context, :last_margin, :pdf
79 |
80 | def setup_element(element, element_styles:)
81 | render_if_needed(element)
82 | context.add(element)
83 | element.process_styles(element_styles: element_styles)
84 | apply_tag_open_styles(element)
85 | element.custom_render(pdf, context) if element.respond_to?(:custom_render)
86 | end
87 |
88 | def render_if_needed(element)
89 | render_needed = element&.block? && buffer.any? && buffer.last != NEW_LINE
90 | return false unless render_needed
91 |
92 | render
93 | true
94 | end
95 |
96 | def apply_tag_close_styles(element)
97 | tag_styles = element.tag_close_styles
98 | @last_margin = tag_styles[:margin_bottom].to_f
99 | pdf.advance_cursor(last_margin + tag_styles[:padding_bottom].to_f)
100 | pdf.start_new_page if tag_styles[:break_after]
101 | end
102 |
103 | def apply_tag_open_styles(element)
104 | tag_styles = element.tag_open_styles
105 | move_down = (tag_styles[:margin_top].to_f - last_margin) + tag_styles[:padding_top].to_f
106 | pdf.advance_cursor(move_down) if move_down > 0
107 | pdf.start_new_page if tag_styles[:break_before]
108 | end
109 |
110 | def prepare_text(content)
111 | text = @before_content.any? ? ::Oga::HTML::Entities.decode(@before_content.join) : ''
112 | @before_content.clear
113 |
114 | return (@last_text = text + content) if context.white_space_pre?
115 |
116 | content = content.lstrip if @last_text[-1] == ' ' || @last_tag_open
117 | text += content.tr("\n", ' ').squeeze(' ')
118 | @last_text = text
119 | end
120 |
121 | def output_content(buffer, block_styles)
122 | apply_callbacks(buffer)
123 | left_indent = block_styles[:margin_left].to_f + block_styles[:padding_left].to_f
124 | options = block_styles.slice(:align, :indent_paragraphs, :leading, :mode, :padding_left)
125 | options[:leading] = adjust_leading(buffer, options[:leading])
126 | pdf.puts(buffer, options, bounding_box: bounds(buffer, options, block_styles), left_indent: left_indent)
127 | end
128 |
129 | def apply_callbacks(buffer)
130 | buffer.select { |item| item[:callback] }.each do |item|
131 | callback, arg = item[:callback]
132 | callback_class = Tag::CALLBACKS[callback]
133 | item[:callback] = callback_class.new(pdf, arg)
134 | end
135 | end
136 |
137 | def adjust_leading(buffer, leading)
138 | return leading if leading
139 |
140 | leadings = buffer.map do |item|
141 | (item[:size] || Context::DEFAULT_STYLES[:size]) * (ADJUST_LEADING[item[:font]] || ADJUST_LEADING[nil])
142 | end
143 | leadings.max.round(4)
144 | end
145 |
146 | def bounds(buffer, options, block_styles)
147 | return unless block_styles[:position] == :absolute
148 |
149 | x = if block_styles.include?(:right)
150 | x1 = pdf.calc_buffer_width(buffer) + block_styles[:right]
151 | x1 < pdf.page_width ? (pdf.page_width - x1) : 0
152 | else
153 | block_styles[:left] || 0
154 | end
155 | y = if block_styles.include?(:bottom)
156 | pdf.calc_buffer_height(buffer, options) + block_styles[:bottom]
157 | else
158 | pdf.page_height - (block_styles[:top] || 0)
159 | end
160 |
161 | [[x, y], { width: pdf.page_width - x }]
162 | end
163 | end
164 | end
165 |
--------------------------------------------------------------------------------
/lib/prawn_html/html_parser.rb:
--------------------------------------------------------------------------------
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?(' Callbacks::Background,
9 | 'StrikeThrough' => Callbacks::StrikeThrough
10 | }.freeze
11 |
12 | TAG_CLASSES = %w[A B Blockquote Body Br Code Del Div H Hr I Img Li Mark Ol P Pre Small Span Sub Sup U Ul].freeze
13 |
14 | def_delegators :@attrs, :styles, :update_styles
15 |
16 | attr_accessor :parent
17 | attr_reader :attrs, :tag
18 |
19 | # Init the Tag
20 | #
21 | # @param tag [Symbol] tag name
22 | # @param attributes [Hash] hash of element attributes
23 | # @param options [Hash] options (container width/height/etc.)
24 | def initialize(tag, attributes: {}, options: {})
25 | @tag = tag
26 | @options = options
27 | @attrs = Attributes.new(attributes)
28 | end
29 |
30 | # Is a block tag?
31 | #
32 | # @return [Boolean] true if the type of the tag is block, false otherwise
33 | def block?
34 | false
35 | end
36 |
37 | # Styles to apply to the block
38 | #
39 | # @return [Hash] hash of styles to apply
40 | def block_styles
41 | block_styles = styles.slice(*Attributes::STYLES_APPLY[:block])
42 | block_styles[:mode] = attrs.data['mode'].to_sym if attrs.data.include?('mode')
43 | block_styles
44 | end
45 |
46 | # Process tag styles
47 | #
48 | # @param element_styles [String] extra styles to apply to the element
49 | def process_styles(element_styles: nil)
50 | attrs.merge_text_styles!(tag_styles, options: options) if respond_to?(:tag_styles)
51 | attrs.merge_text_styles!(element_styles, options: options) if element_styles
52 | attrs.merge_text_styles!(attrs.style, options: options)
53 | attrs.merge_text_styles!(extra_styles, options: options) if respond_to?(:extra_styles)
54 | end
55 |
56 | # Styles to apply on tag closing
57 | #
58 | # @return [Hash] hash of styles to apply
59 | def tag_close_styles
60 | styles.slice(*Attributes::STYLES_APPLY[:tag_close])
61 | end
62 |
63 | # Styles to apply on tag opening
64 | #
65 | # @return [Hash] hash of styles to apply
66 | def tag_open_styles
67 | styles.slice(*Attributes::STYLES_APPLY[:tag_open])
68 | end
69 |
70 | class << self
71 | # Evaluate the Tag class from a tag name
72 | #
73 | # @params tag_name [Symbol] the tag name
74 | #
75 | # @return [Tag] the class for the tag if available or nil
76 | def class_for(tag_name)
77 | @tag_classes ||= TAG_CLASSES.each_with_object({}) do |tag_class, res|
78 | klass = const_get("PrawnHtml::Tags::#{tag_class}")
79 | k = [klass] * klass::ELEMENTS.size
80 | res.merge!(klass::ELEMENTS.zip(k).to_h)
81 | end
82 | @tag_classes[tag_name]
83 | end
84 | end
85 |
86 | private
87 |
88 | attr_reader :options
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/a.rb:
--------------------------------------------------------------------------------
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
21 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/b.rb:
--------------------------------------------------------------------------------
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
14 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/blockquote.rb:
--------------------------------------------------------------------------------
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
26 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/body.rb:
--------------------------------------------------------------------------------
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
14 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/br.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Br < Tag
6 | ELEMENTS = [:br].freeze
7 |
8 | BR_SPACING = Utils.convert_size('17')
9 |
10 | def block?
11 | true
12 | end
13 |
14 | def custom_render(pdf, context)
15 | return if context.last_text_node || !context.previous_tag.is_a?(Br)
16 |
17 | pdf.advance_cursor(BR_SPACING)
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/code.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Code < Tag
6 | ELEMENTS = [:code].freeze
7 |
8 | def tag_styles
9 | 'font-family: Courier'
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/del.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Del < Tag
6 | ELEMENTS = [:del, :s].freeze
7 |
8 | def tag_styles
9 | 'text-decoration: line-through'
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/div.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Div < Tag
6 | ELEMENTS = [:div].freeze
7 |
8 | def block?
9 | true
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/h.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class H < Tag
6 | ELEMENTS = [:h1, :h2, :h3, :h4, :h5, :h6].freeze
7 |
8 | MARGINS_TOP = {
9 | h1: 25,
10 | h2: 20.5,
11 | h3: 18,
12 | h4: 21.2,
13 | h5: 21.2,
14 | h6: 22.8
15 | }.freeze
16 |
17 | MARGINS_BOTTOM = {
18 | h1: 15.8,
19 | h2: 15.8,
20 | h3: 15.8,
21 | h4: 20,
22 | h5: 21.4,
23 | h6: 24.8
24 | }.freeze
25 |
26 | SIZES = {
27 | h1: 31.5,
28 | h2: 24,
29 | h3: 18.7,
30 | h4: 15.7,
31 | h5: 13,
32 | h6: 10.8
33 | }.freeze
34 |
35 | def block?
36 | true
37 | end
38 |
39 | def tag_styles
40 | <<~STYLES
41 | font-size: #{SIZES[tag]}px;
42 | font-weight: bold;
43 | margin-bottom: #{MARGINS_BOTTOM[tag]}px;
44 | margin-top: #{MARGINS_TOP[tag]}px;
45 | STYLES
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/hr.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Hr < Tag
6 | ELEMENTS = [:hr].freeze
7 |
8 | MARGIN_BOTTOM = 12
9 | MARGIN_TOP = 6
10 |
11 | def block?
12 | true
13 | end
14 |
15 | def custom_render(pdf, _context)
16 | dash = attrs.data.include?('dash') ? parse_dash_value(attrs.data['dash']) : nil
17 | pdf.horizontal_rule(color: attrs.styles[:color], dash: dash)
18 | end
19 |
20 | def tag_styles
21 | <<~STYLES
22 | margin-bottom: #{MARGIN_BOTTOM}px;
23 | margin-top: #{MARGIN_TOP}px;
24 | STYLES
25 | end
26 |
27 | private
28 |
29 | def parse_dash_value(dash_string)
30 | if dash_string.match? /\A\d+\Z/
31 | dash_string.to_i
32 | else
33 | dash_array = dash_string.split(',')
34 | dash_array.map(&:to_i) if dash_array.any?
35 | end
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/i.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class I < Tag
6 | ELEMENTS = [:i, :em].freeze
7 |
8 | def tag_styles
9 | 'font-style: italic'
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/img.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Img < Tag
6 | ELEMENTS = [:img].freeze
7 |
8 | def block?
9 | true
10 | end
11 |
12 | def custom_render(pdf, context)
13 | parsed_styles = Attributes.parse_styles(attrs.style)
14 | block_styles = context.block_styles
15 | evaluated_styles = adjust_styles(pdf, block_styles.merge(parsed_styles))
16 | pdf.image(@attrs.src, evaluated_styles)
17 | end
18 |
19 | private
20 |
21 | def adjust_styles(pdf, img_styles)
22 | {}.tap do |result|
23 | w, h = img_styles['width'], img_styles['height']
24 | result[:width] = Utils.convert_size(w, options: pdf.page_width) if w
25 | result[:height] = Utils.convert_size(h, options: pdf.page_height) if h
26 | result[:position] = img_styles[:align] if %i[left center right].include?(img_styles[:align])
27 | end
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/li.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Li < Tag
6 | ELEMENTS = [:li].freeze
7 |
8 | INDENT_OL = -12
9 | INDENT_UL = -6
10 |
11 | def block?
12 | true
13 | end
14 |
15 | def before_content
16 | return if @before_content_once
17 |
18 | @before_content_once = @counter ? "#{@counter}. " : "#{@symbol} "
19 | end
20 |
21 | def block_styles
22 | super.tap do |bs|
23 | bs[:indent_paragraphs] = @indent
24 | end
25 | end
26 |
27 | def on_context_add(_context)
28 | case parent.class.to_s
29 | when 'PrawnHtml::Tags::Ol'
30 | @indent = INDENT_OL
31 | @counter = (parent.counter += 1)
32 | when 'PrawnHtml::Tags::Ul'
33 | @indent = INDENT_UL
34 | @symbol = parent.styles[:list_style_type] || '•'
35 | end
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/mark.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Mark < Tag
6 | ELEMENTS = [:mark].freeze
7 |
8 | def tag_styles
9 | 'background: #ff0'
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/ol.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Ol < Tag
6 | ELEMENTS = [:ol].freeze
7 |
8 | MARGIN_TOP = 15
9 | MARGIN_LEFT = 40
10 | MARGIN_BOTTOM = 15
11 |
12 | attr_accessor :counter
13 |
14 | def initialize(tag, attributes: {}, options: {})
15 | super
16 | @counter = 0
17 | @first_level = false
18 | end
19 |
20 | def block?
21 | true
22 | end
23 |
24 | def on_context_add(context)
25 | return if context.map(&:tag).count { |el| el == :ol } > 1
26 |
27 | @first_level = true
28 | end
29 |
30 | def tag_styles
31 | if @first_level
32 | <<~STYLES
33 | margin-top: #{MARGIN_TOP}px;
34 | margin-left: #{MARGIN_LEFT}px;
35 | margin-bottom: #{MARGIN_BOTTOM}px;
36 | STYLES
37 | else
38 | "margin-left: #{MARGIN_LEFT}px"
39 | end
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/p.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class P < Tag
6 | ELEMENTS = [:p].freeze
7 |
8 | MARGIN_BOTTOM = 12.5
9 | MARGIN_TOP = 12.5
10 |
11 | def block?
12 | true
13 | end
14 |
15 | def tag_styles
16 | <<~STYLES
17 | margin-bottom: #{MARGIN_BOTTOM}px;
18 | margin-top: #{MARGIN_TOP}px;
19 | STYLES
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/pre.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Pre < Tag
6 | ELEMENTS = [:pre].freeze
7 |
8 | MARGIN_BOTTOM = 14
9 | MARGIN_TOP = 14
10 |
11 | def block?
12 | true
13 | end
14 |
15 | def tag_styles
16 | <<~STYLES
17 | font-family: Courier;
18 | margin-bottom: #{MARGIN_BOTTOM}px;
19 | margin-top: #{MARGIN_TOP}px;
20 | white-space: pre;
21 | STYLES
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/small.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Small < Tag
6 | ELEMENTS = [:small].freeze
7 |
8 | def update_styles(context_styles)
9 | size = (context_styles[:size] || Context::DEFAULT_STYLES[:size]) * 0.85
10 | context_styles[:size] = size
11 | super(context_styles)
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/span.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Span < Tag
6 | ELEMENTS = [:span].freeze
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/sub.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Sub < Tag
6 | ELEMENTS = [:sub].freeze
7 |
8 | def tag_styles
9 | 'vertical-align: sub'
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/sup.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Sup < Tag
6 | ELEMENTS = [:sup].freeze
7 |
8 | def tag_styles
9 | 'vertical-align: super'
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/u.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class U < Tag
6 | ELEMENTS = [:ins, :u].freeze
7 |
8 | def tag_styles
9 | 'text-decoration: underline'
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/prawn_html/tags/ul.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Tags
5 | class Ul < Tag
6 | ELEMENTS = [:ul].freeze
7 |
8 | MARGIN_TOP = 15
9 | MARGIN_LEFT = 40
10 | MARGIN_BOTTOM = 15
11 |
12 | def initialize(tag, attributes: {}, options: {})
13 | super
14 | @first_level = false
15 | end
16 |
17 | def block?
18 | true
19 | end
20 |
21 | def on_context_add(context)
22 | return if context.map(&:tag).count { |el| el == :ul } > 1
23 |
24 | @first_level = true
25 | end
26 |
27 | def tag_styles
28 | if @first_level
29 | <<~STYLES
30 | margin-top: #{MARGIN_TOP}px;
31 | margin-left: #{MARGIN_LEFT}px;
32 | margin-bottom: #{MARGIN_BOTTOM}px;
33 | STYLES
34 | else
35 | "margin-left: #{MARGIN_LEFT}px"
36 | end
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/prawn_html/utils.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml
4 | module Utils
5 | NORMALIZE_STYLES = {
6 | 'bold' => :bold,
7 | 'italic' => :italic,
8 | 'sub' => :subscript,
9 | 'super' => :superscript,
10 | 'underline' => :underline
11 | }.freeze
12 |
13 | # Setup a background callback
14 | #
15 | # @param value [String] HTML string color
16 | #
17 | # @return [Array] callback name and argument value
18 | def callback_background(value, options: nil)
19 | ['Background', convert_color(value, options: options)]
20 | end
21 |
22 | # Setup a strike through callback
23 | #
24 | # @param value [String] unused
25 | #
26 | # @return [Array] callback name and argument value
27 | def callback_strike_through(value, options: nil)
28 | ['StrikeThrough', nil]
29 | end
30 |
31 | # Converts a color string
32 | #
33 | # Supported formats:
34 | # - 3 hex digits, ex. `color: #FB1`;
35 | # - 6 hex digits, ex. `color: #abcdef`;
36 | # - RGB, ex. `color: RGB(64, 0, 128)`;
37 | # - color name, ex. `color: red`.
38 | #
39 | # @param value [String] HTML string color
40 | #
41 | # @return [String] adjusted string color or nil if value is invalid
42 | def convert_color(value, options: nil)
43 | val = value.to_s.strip.downcase
44 | return Regexp.last_match[1] if val.match /\A#([a-f0-9]{6})\Z/ # rubocop:disable Performance/RedundantMatch
45 |
46 | if val.match /\A#([a-f0-9]{3})\Z/ # rubocop:disable Performance/RedundantMatch
47 | r, g, b = Regexp.last_match[1].chars
48 | return (r * 2) + (g * 2) + (b * 2)
49 | end
50 | if val.match /\Argb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\Z/ # rubocop:disable Performance/RedundantMatch
51 | r, g, b = Regexp.last_match[1..3].map { |v| v.to_i.to_s(16) }
52 | return "#{r.rjust(2, '0')}#{g.rjust(2, '0')}#{b.rjust(2, '0')}"
53 | end
54 |
55 | COLORS[val]
56 | end
57 |
58 | # Converts a decimal number string
59 | #
60 | # @param value [String] string decimal
61 | #
62 | # @return [Float] converted and rounded float number
63 | def convert_float(value, options: nil)
64 | val = value&.gsub(/[^0-9.]/, '') || ''
65 | val.to_f.round(4)
66 | end
67 |
68 | # Converts a size string
69 | #
70 | # @param value [String] size string
71 | # @param options [Numeric] container size
72 | #
73 | # @return [Float] converted and rounded size
74 | def convert_size(value, options: nil)
75 | val = value&.gsub(/[^0-9.]/, '') || ''
76 | val =
77 | if options && value&.include?('%')
78 | val.to_f * options * 0.01
79 | else
80 | val.to_f * PrawnHtml::PX
81 | end
82 | val.round(4)
83 | end
84 |
85 | # Converts a string to symbol
86 | #
87 | # @param value [String] string
88 | #
89 | # @return [Symbol] symbol
90 | def convert_symbol(value, options: nil)
91 | value.to_sym if value && !value.match?(/\A\s*\Z/)
92 | end
93 |
94 | # Copy a value without conversion
95 | #
96 | # @param value
97 | #
98 | # @return value
99 | def copy_value(value, options: nil)
100 | value
101 | end
102 |
103 | # Filter font family
104 | #
105 | # @param value [String] string value
106 | #
107 | # @return [Symbol] unquoted font family or nil if the input value is 'inherit'
108 | def filter_font_family(value, options: nil)
109 | result = unquote(value, options: options)
110 | result == 'inherit' ? nil : result
111 | end
112 |
113 | # Normalize a style value
114 | #
115 | # @param value [String] string value
116 | # @param accepted_values [Array] allowlist of valid values (symbols)
117 | #
118 | # @return [Symbol] style value or nil
119 | def normalize_style(value, accepted_values)
120 | val = value&.strip&.downcase
121 | ret = NORMALIZE_STYLES[val]
122 | accepted_values.include?(ret) ? ret : nil
123 | end
124 |
125 | # Unquotes a string
126 | #
127 | # @param value [String] string
128 | #
129 | # @return [String] string without quotes at the beginning/ending
130 | def unquote(value, options: nil)
131 | (value&.strip || +'').tap do |val|
132 | val.gsub!(/\A['"]|["']\Z/, '')
133 | end
134 | end
135 |
136 | module_function :callback_background, :callback_strike_through, :convert_color, :convert_float, :convert_size,
137 | :convert_symbol, :copy_value, :filter_font_family, :normalize_style, :unquote
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/lib/prawn_html/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PrawnHtml # :nodoc:
4 | VERSION = '0.7.1'
5 | end
6 |
--------------------------------------------------------------------------------
/prawn-html.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | lib = File.expand_path('lib', __dir__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require 'prawn_html/version'
6 |
7 | Gem::Specification.new do |spec|
8 | spec.name = 'prawn-html'
9 | spec.version = PrawnHtml::VERSION
10 | spec.summary = 'Prawn PDF - HTML renderer'
11 | spec.description = 'HTML to PDF with Prawn PDF'
12 |
13 | spec.required_ruby_version = '>= 2.5.0'
14 |
15 | spec.license = 'MIT'
16 | spec.authors = ['Mattia Roccoberton']
17 | spec.email = 'mat@blocknot.es'
18 | spec.homepage = 'https://github.com/blocknotes/prawn-html'
19 |
20 | spec.metadata['homepage_uri'] = spec.homepage
21 | spec.metadata['source_code_uri'] = spec.homepage
22 | spec.metadata['rubygems_mfa_required'] = 'true'
23 |
24 | spec.files = Dir['lib/**/*', 'LICENSE.txt', 'README.md']
25 | spec.require_paths = ['lib']
26 |
27 | spec.add_runtime_dependency 'oga', '~> 3.3'
28 | spec.add_runtime_dependency 'prawn', '~> 2.4'
29 | end
30 |
--------------------------------------------------------------------------------
/spec/features/instance_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Instance' do
4 | it 'preserves the styles in the instance' do
5 | pdf = Prawn::Document.new(page_size: 'A4')
6 | phtml = PrawnHtml::Instance.new(pdf)
7 | css = <<~CSS
8 | h1 { color: green }
9 | i { color: red }
10 | CSS
11 | phtml.append(css: css)
12 | phtml.append(html: 'Some HTML before
')
13 | pdf.text 'Some Prawn text'
14 | phtml.append(html: 'Some HTML after
')
15 |
16 | output_file = File.expand_path('../../examples/instance.pdf', __dir__)
17 | pdf.render_file(output_file) unless File.exist?(output_file)
18 |
19 | expected_pdf = File.read(output_file)
20 | expect(Zlib.crc32(pdf.render)).to eq Zlib.crc32(expected_pdf)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/features/samples_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Samples' do
4 | Dir[File.expand_path('../../examples/*.html', __dir__)].sort.each do |file|
5 | it "renders the expected output for #{File.basename(file)}", :aggregate_failures do
6 | html = File.read(file)
7 | pdf = Prawn::Document.new(page_size: 'A4', page_layout: :portrait)
8 | PrawnHtml.append_html(pdf, html)
9 | expected_pdf = File.read(file.gsub(/\.html\Z/, '.pdf'))
10 | expect(Zlib.crc32(pdf.render)).to eq Zlib.crc32(expected_pdf)
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/integrations/blocks_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Blocks' do
4 | let(:pdf) { Prawn::Document.new(page_size: 'A4', page_layout: :portrait) }
5 | let(:size) { PrawnHtml::Context::DEFAULT_STYLES[:size] }
6 |
7 | before do
8 | PrawnHtml.append_html(pdf, html)
9 | end
10 |
11 | context 'with some content in an element div' do
12 | let(:html) { 'Some content in a element div
' }
13 |
14 | let(:expected_content) { ['Some content in a element div'] }
15 | let(:expected_positions) do
16 | [[
17 | pdf.page.margins[:left],
18 | (pdf.y - TestUtils.default_font.ascender).round(4)
19 | ]]
20 | end
21 | let(:expected_font_settings) { [{ name: TestUtils.default_font_family, size: size }] }
22 |
23 | include_examples 'checks contents, positions and font settings'
24 | end
25 |
26 | context 'with some content in an element p' do
27 | let(:html) { 'Some content in a element p
' }
28 |
29 | let(:expected_content) { ['Some content in a element p'] }
30 | let(:expected_positions) do
31 | margin = PrawnHtml::Utils.convert_size(PrawnHtml::Tags::P::MARGIN_TOP.to_s)
32 | y = pdf.y - TestUtils.default_font.ascender - margin
33 | [[pdf.page.margins[:left], y.round(4)]]
34 | end
35 | let(:expected_font_settings) { [{ name: TestUtils.default_font_family, size: size }] }
36 |
37 | include_examples 'checks contents, positions and font settings'
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/integrations/headings_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Headings' do
4 | let(:pdf) { Prawn::Document.new(page_size: 'A4', page_layout: :portrait) }
5 |
6 | before do
7 | PrawnHtml.append_html(pdf, html)
8 | end
9 |
10 | def base_position(pdf, font_size, margin_top: 16)
11 | font = Prawn::Document.new(page_size: 'A4', page_layout: :portrait).font('Helvetica-Bold', size: font_size)
12 | [pdf.page.margins[:left], pdf.y - font.ascender - margin_top]
13 | end
14 |
15 | context 'with some content in an element h1' do
16 | let(:html) { 'Some content in a element h1
' }
17 |
18 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h1].to_s) }
19 | let(:expected_content) { ['Some content in a element h1'] }
20 | let(:expected_positions) do
21 | margin = PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::MARGINS_TOP[:h1].to_s)
22 | y = pdf.y - TestUtils.font_ascender(font_size: size) - margin
23 | [[pdf.page.margins[:left], y.round(4)]]
24 | end
25 | let(:expected_font_settings) { [{ name: :'Helvetica-Bold', size: size }] }
26 |
27 | include_examples 'checks contents, positions and font settings'
28 | end
29 |
30 | context 'with some content in an element h2' do
31 | let(:html) { 'Some content in a element h2
' }
32 |
33 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h2].to_s) }
34 | let(:expected_content) { ['Some content in a element h2'] }
35 | let(:expected_positions) do
36 | margin = PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::MARGINS_TOP[:h2].to_s)
37 | y = pdf.y - TestUtils.font_ascender(font_size: size) - margin
38 | [[pdf.page.margins[:left], y.round(4)]]
39 | end
40 | let(:expected_font_settings) { [{ name: :'Helvetica-Bold', size: size }] }
41 |
42 | include_examples 'checks contents, positions and font settings'
43 | end
44 |
45 | context 'with some content in an element h3' do
46 | let(:html) { 'Some content in a element h3
' }
47 |
48 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h3].to_s) }
49 | let(:expected_content) { ['Some content in a element h3'] }
50 | let(:expected_positions) do
51 | margin = PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::MARGINS_TOP[:h3].to_s)
52 | y = pdf.y - TestUtils.font_ascender(font_size: size) - margin
53 | [[pdf.page.margins[:left], y.round(4)]]
54 | end
55 | let(:expected_font_settings) { [{ name: :'Helvetica-Bold', size: size }] }
56 |
57 | include_examples 'checks contents, positions and font settings'
58 | end
59 |
60 | context 'with some content in an element h4' do
61 | let(:html) { 'Some content in a element h4
' }
62 |
63 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h4].to_s) }
64 | let(:expected_content) { ['Some content in a element h4'] }
65 | let(:expected_positions) do
66 | margin = PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::MARGINS_TOP[:h4].to_s)
67 | y = pdf.y - TestUtils.font_ascender(font_size: size) - margin
68 | [[pdf.page.margins[:left], y.round(4)]]
69 | end
70 | let(:expected_font_settings) { [{ name: :'Helvetica-Bold', size: size }] }
71 |
72 | include_examples 'checks contents, positions and font settings'
73 | end
74 |
75 | context 'with some content in an element h5' do
76 | let(:html) { 'Some content in a element h5
' }
77 |
78 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h5].to_s) }
79 | let(:expected_content) { ['Some content in a element h5'] }
80 | let(:expected_positions) do
81 | ascender = TestUtils.font_ascender(font_family: 'Helvetica-Bold', font_size: size)
82 | margin = PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::MARGINS_TOP[:h5].to_s)
83 | y = pdf.y - ascender - margin
84 | [[pdf.page.margins[:left], y.round(4)]]
85 | end
86 | let(:expected_font_settings) { [{ name: :'Helvetica-Bold', size: size }] }
87 |
88 | include_examples 'checks contents, positions and font settings'
89 | end
90 |
91 | context 'with some content in an element h6' do
92 | let(:html) { 'Some content in a element h6
' }
93 |
94 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h6].to_s) }
95 | let(:expected_content) { ['Some content in a element h6'] }
96 | let(:expected_positions) do
97 | ascender = TestUtils.font_ascender(font_family: 'Helvetica-Bold', font_size: size)
98 | margin = PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::MARGINS_TOP[:h6].to_s)
99 | y = pdf.y - ascender - margin
100 | [[pdf.page.margins[:left], y.round(4)]]
101 | end
102 | let(:expected_font_settings) { [{ name: :'Helvetica-Bold', size: size }] }
103 |
104 | include_examples 'checks contents, positions and font settings'
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/spec/integrations/lists_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Lists' do
4 | let(:pdf) { Prawn::Document.new(page_size: 'A4', page_layout: :portrait) }
5 |
6 | before do
7 | PrawnHtml.append_html(pdf, html)
8 | end
9 |
10 | context 'with an empty ul list' do
11 | let(:html) do
12 | <<~HTML
13 |
15 | HTML
16 | end
17 |
18 | it 'renders no strings', :aggregate_failures do
19 | text_analysis = PDF::Inspector::Text.analyze(pdf.render)
20 |
21 | expect(text_analysis.strings).to be_empty
22 | expect(text_analysis.font_settings).to be_empty
23 | expect(text_analysis.positions).to be_empty
24 | end
25 | end
26 |
27 | context 'with an ul list' do
28 | let(:html) do
29 | <<~HTML
30 |
31 | - First item
32 | - Second item
33 | - Third item
34 |
35 | HTML
36 | end
37 |
38 | it 'renders the list of elements', :aggregate_failures do
39 | text_analysis = PDF::Inspector::Text.analyze(pdf.render)
40 |
41 | expected_array = [{ name: TestUtils.default_font_family, size: TestUtils.default_font_size }] * 3
42 |
43 | expect(text_analysis.strings).to match_array(['• First item', '• Second item', '• Third item'])
44 | expect(text_analysis.font_settings).to match_array(expected_array)
45 |
46 | font = TestUtils.default_font
47 | margin_left = PrawnHtml::Utils.convert_size(PrawnHtml::Tags::Ul::MARGIN_LEFT.to_s)
48 | x = pdf.page.margins[:left] + margin_left + PrawnHtml::Tags::Li::INDENT_UL
49 | y = pdf.y - font.ascender - PrawnHtml::Utils.convert_size(PrawnHtml::Tags::Ul::MARGIN_TOP.to_s)
50 |
51 | expected_array = [
52 | [x, y.round(5)],
53 | [x, (y - font.height - TestUtils.adjust_leading).round(5)],
54 | [x, (y - (font.height * 2) - (TestUtils.adjust_leading * 2)).round(5)]
55 | ]
56 |
57 | expect(text_analysis.positions).to match_array(expected_array)
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/spec/integrations/misc_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Misc' do
4 | let(:pdf) { Prawn::Document.new(page_size: 'A4', page_layout: :portrait) }
5 |
6 | before do
7 | PrawnHtml.append_html(pdf, html)
8 | end
9 |
10 | shared_examples 'a specific element with some test content' do
11 | let(:html) { [expected_content[0], test_content, expected_content[2]].join }
12 |
13 | let(:expected_content) { ['Some', 'test', 'content'] }
14 | let(:expected_positions) do
15 | x1 = pdf.page.margins[:left]
16 | x2 = x1 + TestUtils.font_string_width(pdf, expected_content.first)
17 | x3 = x2 + TestUtils.font_string_width(pdf, test_content, inline_format: true)
18 | y = pdf.y - TestUtils.default_font.ascender
19 | [[x1.round(4), y.round(4)], [x2.round(4), y.round(4)], [x3.round(4), y.round(4)]]
20 | end
21 | let(:expected_font_settings) do
22 | [
23 | { name: :Helvetica, size: TestUtils.default_font_size },
24 | { name: expected_font_family.to_sym, size: TestUtils.default_font_size },
25 | { name: :Helvetica, size: TestUtils.default_font_size }
26 | ]
27 | end
28 |
29 | include_examples 'checks contents, positions and font settings'
30 | end
31 |
32 | context 'with some content in an element b' do
33 | it_behaves_like 'a specific element with some test content' do
34 | let(:test_content) { 'test' }
35 | let(:expected_font_family) { 'Helvetica-Bold' }
36 | end
37 | end
38 |
39 | context 'with some content in an element em' do
40 | it_behaves_like 'a specific element with some test content' do
41 | let(:test_content) { 'test' }
42 | let(:expected_font_family) { 'Helvetica-Oblique' }
43 | end
44 | end
45 |
46 | context 'with some content in an element i' do
47 | it_behaves_like 'a specific element with some test content' do
48 | let(:test_content) { 'test' }
49 | let(:expected_font_family) { 'Helvetica-Oblique' }
50 | end
51 | end
52 |
53 | context 'with some content in an element strong' do
54 | it_behaves_like 'a specific element with some test content' do
55 | let(:test_content) { 'test' }
56 | let(:expected_font_family) { 'Helvetica-Bold' }
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/spec/integrations/styles_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Styles' do
4 | let(:pdf) { Prawn::Document.new(page_size: 'A4', page_layout: :portrait) }
5 |
6 | before do
7 | PrawnHtml.append_html(pdf, html)
8 | end
9 |
10 | describe 'attribute text-align' do
11 | context 'with some content left aligned' do
12 | let(:html) { 'Some content
' }
13 |
14 | let(:expected_content) { ['Some content'] }
15 | let(:expected_positions) do
16 | [[
17 | pdf.page.margins[:left],
18 | (pdf.y - TestUtils.default_font.ascender).round(4)
19 | ]]
20 | end
21 |
22 | include_examples 'checks contents and positions'
23 | end
24 |
25 | context 'with some content center aligned' do
26 | let(:html) { 'Some content
' }
27 |
28 | let(:content_width) { TestUtils.font_string_width(pdf, expected_content.first) }
29 | let(:expected_content) { ['Some content'] }
30 | let(:expected_positions) do
31 | [[
32 | (pdf.page.margins[:left] + ((pdf.bounds.width - content_width) / 2)).round(4),
33 | (pdf.y - TestUtils.default_font.ascender).round(4)
34 | ]]
35 | end
36 |
37 | include_examples 'checks contents and positions'
38 | end
39 |
40 | context 'with some content right aligned' do
41 | let(:html) { 'Some content
' }
42 |
43 | let(:content_width) { TestUtils.font_string_width(pdf, expected_content.first) }
44 | let(:expected_content) { ['Some content'] }
45 | let(:expected_positions) do
46 | x = pdf.page.margins[:left] + pdf.bounds.width - content_width
47 | [[
48 | x.round(4),
49 | (pdf.y - TestUtils.default_font.ascender).round(4)
50 | ]]
51 | end
52 |
53 | include_examples 'checks contents and positions'
54 | end
55 | end
56 |
57 | describe 'attribute break-after' do
58 | let(:html) { 'Some content
' }
59 |
60 | it 'creates a new page' do
61 | pdf.render
62 | expect(pdf.page_count).to eq 2
63 | end
64 | end
65 |
66 | describe 'attribute break-before' do
67 | let(:html) { 'Some content
' }
68 |
69 | it 'creates a new page' do
70 | pdf.render
71 | expect(pdf.page_count).to eq 2
72 | end
73 | end
74 |
75 | describe 'attribute font-family' do
76 | context 'with some content with Courier font' do
77 | let(:html) { 'Some content
' }
78 | let(:size) { TestUtils.default_font_size }
79 |
80 | let(:expected_content) { ['Some content'] }
81 | let(:expected_positions) do
82 | [[
83 | pdf.page.margins[:left],
84 | (pdf.y - TestUtils.font_ascender(font_family: 'Courier', font_size: size)).round(4)
85 | ]]
86 | end
87 | let(:expected_font_settings) { [{ name: :Courier, size: size }] }
88 |
89 | include_examples 'checks contents, positions and font settings'
90 | end
91 | end
92 |
93 | describe 'attribute font-size' do
94 | context 'with some content with a font size of 20px' do
95 | let(:html) { 'Some content
' }
96 | let(:size) { PrawnHtml::Utils.convert_size('20px') }
97 |
98 | let(:expected_content) { ['Some content'] }
99 | let(:expected_positions) do
100 | [[
101 | pdf.page.margins[:left],
102 | (pdf.y - TestUtils.prawn_document.font('Helvetica', size: size).ascender).round(4)
103 | ]]
104 | end
105 | let(:expected_font_settings) { [{ name: TestUtils.default_font_family, size: size }] }
106 |
107 | include_examples 'checks contents, positions and font settings'
108 | end
109 | end
110 |
111 | describe 'attribute font-style' do
112 | context 'with some content with italic style' do
113 | let(:html) { 'Some content
' }
114 | let(:size) { TestUtils.default_font_size }
115 |
116 | let(:expected_content) { ['Some content'] }
117 | let(:expected_positions) do
118 | [[
119 | pdf.page.margins[:left],
120 | (pdf.y - TestUtils.prawn_document.font('Helvetica-Oblique', size: size).ascender).round(4)
121 | ]]
122 | end
123 | let(:expected_font_settings) { [{ name: :'Helvetica-Oblique', size: size }] }
124 |
125 | include_examples 'checks contents, positions and font settings'
126 | end
127 | end
128 |
129 | describe 'attribute font-weight' do
130 | context 'with some content with bold weight' do
131 | let(:html) { 'Some content
' }
132 | let(:size) { TestUtils.default_font_size }
133 |
134 | let(:expected_content) { ['Some content'] }
135 | let(:expected_positions) do
136 | [[
137 | pdf.page.margins[:left],
138 | (pdf.y - TestUtils.prawn_document.font('Helvetica-Bold', size: size).ascender).round(4)
139 | ]]
140 | end
141 | let(:expected_font_settings) { [{ name: :'Helvetica-Bold', size: size }] }
142 |
143 | include_examples 'checks contents, positions and font settings'
144 | end
145 | end
146 |
147 | describe 'attribute letter-spacing' do
148 | let(:html) { 'aaa
bbb ccc
' }
149 |
150 | it 'renders some content with letter spacing', :aggregate_failures do
151 | text_analysis = PDF::Inspector::Text.analyze(pdf.render)
152 |
153 | expect(text_analysis.strings).to eq(['aaa', 'bbb', 'ccc'])
154 | expect(text_analysis.character_spacing).to eq(([1.5, 0.0] * 3) + ([2.0, 0.0] * 3))
155 | end
156 | end
157 |
158 | describe 'attribute margin-left' do
159 | let(:html) { 'Some content
' }
160 | let(:size) { PrawnHtml::Utils.convert_size('40px') }
161 |
162 | let(:expected_content) { ['Some content'] }
163 | let(:expected_positions) do
164 | [[
165 | pdf.page.margins[:left] + size,
166 | (pdf.y - TestUtils.default_font.ascender).round(4)
167 | ]]
168 | end
169 |
170 | include_examples 'checks contents and positions'
171 | end
172 |
173 | describe 'attribute margin-top' do
174 | let(:html) { 'Some content
' }
175 | let(:size) { PrawnHtml::Utils.convert_size('40px') }
176 |
177 | let(:expected_content) { ['Some content'] }
178 | let(:expected_positions) do
179 | [[
180 | pdf.page.margins[:left],
181 | (pdf.y - TestUtils.default_font.ascender - size).round(4)
182 | ]]
183 | end
184 |
185 | include_examples 'checks contents and positions'
186 | end
187 | end
188 |
--------------------------------------------------------------------------------
/spec/integrations/tags/br_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Misc' do
4 | let(:pdf) { Prawn::Document.new(page_size: 'A4', page_layout: :portrait) }
5 | let(:text_analysis) { PDF::Inspector::Text.analyze(pdf.render) }
6 |
7 | before do
8 | PrawnHtml.append_html(pdf, html)
9 | end
10 |
11 | context 'with some br elements' do
12 | let(:html) { 'First line
Second line
Third line
Last line' }
13 |
14 | it 'renders some breaking line elements' do
15 | expected_strings = ['First line', 'Second line', 'Third line', 'Last line']
16 | expect(text_analysis.strings).to match_array(expected_strings)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'simplecov'
4 | require 'simplecov-lcov'
5 |
6 | SimpleCov::Formatter::LcovFormatter.config do |c|
7 | c.report_with_single_file = true
8 | # c.single_report_path = ENV['LCOV_PATH'] if ENV['LCOV_PATH'].present?
9 | end
10 | simplecov_formatters = [SimpleCov::Formatter::LcovFormatter, SimpleCov.formatter]
11 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(simplecov_formatters)
12 | SimpleCov.start do
13 | add_filter '/spec/'
14 | end
15 |
16 | require 'ostruct'
17 | require 'prawn-html'
18 | require 'pdf/inspector'
19 | require 'pry'
20 |
21 | Dir["#{__dir__}/support/**/*.rb"].sort.each { |f| require f }
22 |
23 | Prawn::Fonts::AFM.hide_m17n_warning = true
24 |
25 | RSpec.configure do |config|
26 | config.color = true
27 | config.tty = true
28 |
29 | config.expect_with :rspec do |expectations|
30 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
31 | end
32 |
33 | config.mock_with :rspec do |mocks|
34 | mocks.verify_partial_doubles = true
35 | end
36 |
37 | config.shared_context_metadata_behavior = :apply_to_host_groups
38 | end
39 |
--------------------------------------------------------------------------------
/spec/support/common_checks.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples 'checks contents and positions' do
4 | it 'matches the expected contents and positions', :aggregate_failures do
5 | text_analysis = PDF::Inspector::Text.analyze(pdf.render)
6 | positions = text_analysis.positions.map { |item| [item[0].round(4), item[1].round(4)] }
7 | expect(text_analysis.strings).to eq expected_content
8 | expect(positions).to eq expected_positions
9 | end
10 | end
11 |
12 | RSpec.shared_examples 'checks contents, positions and font settings' do
13 | it 'matches the expected contents, positions and font settings', :aggregate_failures do
14 | text_analysis = PDF::Inspector::Text.analyze(pdf.render)
15 | positions = text_analysis.positions.map { |item| [item[0].round(4), item[1].round(4)] }
16 | expect(text_analysis.strings).to eq expected_content
17 | expect(text_analysis.font_settings).to eq expected_font_settings
18 | expect(positions).to eq expected_positions
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/support/shared_contexts.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_context 'with pdf wrapper' do
4 | let(:pdf) do
5 | methods = { advance_cursor: true, puts: true, horizontal_rule: true }
6 | instance_double(PrawnHtml::PdfWrapper, methods)
7 | end
8 |
9 | def append_html_to_pdf(html)
10 | allow(pdf).to receive_messages(page_width: 540, page_height: 720)
11 | allow(PrawnHtml::PdfWrapper).to receive(:new).and_return(pdf)
12 | pdf_document = Prawn::Document.new(page_size: 'A4', page_layout: :portrait)
13 | PrawnHtml.append_html(pdf_document, html)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/support/test_utils.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module TestUtils
4 | extend self
5 |
6 | def adjust_leading(size = PrawnHtml::Context::DEFAULT_STYLES[:size], font = nil)
7 | (size * PrawnHtml::ADJUST_LEADING[font]).round(4)
8 | end
9 |
10 | def default_font
11 | prawn_document.font('Helvetica', size: default_font_size)
12 | end
13 |
14 | def default_font_family
15 | default_font.family.to_sym
16 | end
17 |
18 | def default_font_size
19 | PrawnHtml::Context::DEFAULT_STYLES[:size]
20 | end
21 |
22 | def font_ascender(font_family: 'Helvetica', font_size: default_font_size)
23 | prawn_document.font(font_family, size: font_size).ascender
24 | end
25 |
26 | def font_string_width(pdf_doc, string, font_family: 'Helvetica', font_size: default_font_size, inline_format: false)
27 | width = 0
28 | pdf_doc.font(font_family, size: font_size) { width = pdf_doc.width_of(string, inline_format: inline_format) }
29 | width
30 | end
31 |
32 | def prawn_document
33 | ::Prawn::Document.new(page_size: 'A4', page_layout: :portrait)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/attributes_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Attributes do
4 | subject(:attributes) { described_class.new(attributes_hash) }
5 |
6 | let(:attributes_hash) { { attr1: 'value 1', attr2: 'value 2' } }
7 |
8 | it { expect(described_class).to be < OpenStruct }
9 |
10 | describe '#initialize' do
11 | it 'returns an empty styles hash' do
12 | expect(attributes.styles).to eq({})
13 | end
14 | end
15 |
16 | describe '#data' do
17 | subject(:data) { attributes.data }
18 |
19 | context 'with some data attributes (data-dash: 5 and data-something-else: "some value")' do
20 | let(:attributes_hash) { { 'data-dash': '5', 'data-something-else': '"some value"' } }
21 |
22 | it { is_expected.to match('dash' => '5', 'something-else' => '"some value"') }
23 | end
24 | end
25 |
26 | describe '#merge_text_styles!' do
27 | subject(:merge_text_styles!) { attributes.merge_text_styles!(text_styles, options: options) }
28 |
29 | let(:options) { {} }
30 |
31 | before do
32 | allow(PrawnHtml::Utils).to receive(:send).and_call_original
33 | end
34 |
35 | context 'with an empty hash' do
36 | let(:text_styles) { '' }
37 |
38 | it "doesn't merge new styles" do
39 | expect(attributes.styles).to eq({})
40 | end
41 | end
42 |
43 | context 'with some styles' do
44 | let(:text_styles) do
45 | <<~STYLES
46 | font-family: 'Times-Roman';
47 | font-size: 16px;
48 | font-weight: bold;
49 | margin-bottom: 22px;
50 | STYLES
51 | end
52 |
53 | it 'receives the expected messages', :aggregate_failures do
54 | merge_text_styles!
55 |
56 | expect(PrawnHtml::Utils).to have_received(:send).with(:filter_font_family, "'Times-Roman'", options: nil)
57 | expect(PrawnHtml::Utils).to have_received(:send).with(:convert_size, '16px', options: nil)
58 | expect(PrawnHtml::Utils).to have_received(:send).with(:convert_size, '22px', options: nil)
59 | end
60 | end
61 |
62 | context 'with some options' do
63 | let(:options) { { width: 540, height: 720 } }
64 | let(:text_styles) { 'top: 50%' }
65 |
66 | it 'receives the expected convert messages', :aggregate_failures do
67 | merge_text_styles!
68 |
69 | expect(PrawnHtml::Utils).to have_received(:send).with(:convert_size, '50%', options: 720)
70 | end
71 | end
72 | end
73 |
74 | describe '#remove_value' do
75 | subject(:remove_value) { attributes.remove_value(context_styles, rule) }
76 |
77 | let(:context_styles) { { size: 9.6, styles: %i[bold italic] } }
78 |
79 | context 'with a missing rule' do
80 | let(:rule) { { key: :color, set: :convert_color } }
81 |
82 | it "doesn't change the context styles" do
83 | expect { remove_value }.not_to change(context_styles, :values)
84 | end
85 | end
86 |
87 | context 'with an applied rule' do
88 | let(:rule) { { key: :styles, set: :append_styles, values: %i[bold] } }
89 |
90 | it "changes the context styles" do
91 | expect { remove_value }.to change(context_styles, :values).from([9.6, %i[bold italic]]).to([9.6, %i[italic]])
92 | end
93 | end
94 | end
95 |
96 | describe '#update_styles' do
97 | subject(:update_styles) { attributes.update_styles(context_styles) }
98 |
99 | let(:context_styles) { { size: 9.6 } }
100 | let(:rule) { { key: :styles, set: :append_styles, values: %i[bold] } }
101 |
102 | before do
103 | allow(attributes).to receive_messages(initial: Set.new([rule]), remove_value: nil)
104 | end
105 |
106 | it 'asks to the attributes to remove the value from the context styles that match the specified rule' do
107 | update_styles
108 | expect(attributes).to have_received(:remove_value).with(context_styles, rule)
109 | end
110 | end
111 |
112 | describe '.merge_attr!' do
113 | context 'with an empty key' do
114 | let(:key) { nil }
115 | let(:value) { '123' }
116 |
117 | it "doesn't update the hash" do
118 | hash = { some_key: 'some value' }
119 | described_class.merge_attr!(hash, key, value)
120 |
121 | expect(hash).to eq(some_key: 'some value')
122 | end
123 | end
124 |
125 | context 'with a mergeable key (ex. :margin_left)' do
126 | let(:key) { :margin_left }
127 | let(:value) { 10 }
128 |
129 | it 'updates the hash increasing the target attribute', :aggregate_failures do
130 | hash = { some_key: 'some value' }
131 | described_class.merge_attr!(hash, key, value)
132 | expect(hash).to eq(margin_left: value, some_key: 'some value')
133 |
134 | described_class.merge_attr!(hash, key, 5)
135 | expect(hash).to eq(margin_left: 15, some_key: 'some value')
136 | end
137 | end
138 |
139 | context 'with a non-mergeable key (ex. :another_key)' do
140 | let(:key) { :another_key }
141 | let(:value) { 10 }
142 |
143 | it 'replaces the target attribute value in the hash' do
144 | hash = { another_key: 5, some_key: 'some value' }
145 | described_class.merge_attr!(hash, key, value)
146 |
147 | expect(hash).to eq(another_key: 10, some_key: 'some value')
148 | end
149 | end
150 | end
151 |
152 | describe '.parse_styles' do
153 | subject(:parse_styles) { described_class.parse_styles(styles) }
154 |
155 | context 'with nil styles' do
156 | let(:styles) { nil }
157 |
158 | it { is_expected.to eq({}) }
159 | end
160 |
161 | context 'with an invalid styles string' do
162 | let(:styles) { 'a a a' }
163 |
164 | it { is_expected.to eq({}) }
165 | end
166 |
167 | context 'with a valid styles string' do
168 | let(:styles) { 'padding-left: 10px; padding-top: 20px; padding-bottom: 30px' }
169 |
170 | it 'parses the styles string and return an hash' do
171 | expected_hash = {
172 | 'padding-bottom' => '30px',
173 | 'padding-left' => '10px',
174 | 'padding-top' => '20px'
175 | }
176 |
177 | expect(parse_styles).to match(expected_hash)
178 | end
179 | end
180 | end
181 | end
182 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/context_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Context do
4 | subject(:context) { described_class.new }
5 |
6 | it { expect(described_class).to be < Array }
7 |
8 | describe '#initialize' do
9 | it 'last_text_node is set to false' do
10 | expect(context.last_text_node).to be_falsey
11 | end
12 | end
13 |
14 | describe '#add' do
15 | let(:some_tag_class) do
16 | Class.new(PrawnHtml::Tag) do
17 | def on_context_add(context)
18 | # extra init after context insertion
19 | end
20 | end
21 | end
22 | let(:tag1) { instance_double(PrawnHtml::Tag, 'parent=': true) }
23 | let(:tag2) { instance_double(some_tag_class, 'parent=': true, on_context_add: true) }
24 |
25 | it 'adds the elements', :aggregate_failures do
26 | expect { context.add(tag1) }.to change(context, :size).from(0).to(1)
27 | expect(tag1).to have_received('parent=').with(nil)
28 |
29 | expect { context.add(tag2) }.to change(context, :size).from(1).to(2)
30 | expect(tag2).to have_received(:'parent=').with(tag1)
31 | expect(tag2).to have_received(:on_context_add).with(context)
32 | end
33 |
34 | it 'returns the context itself' do
35 | expect(context.add(tag1)).to eq(context)
36 | end
37 | end
38 |
39 | describe '#before_content' do
40 | subject(:before_content) { context.before_content }
41 |
42 | context 'with no elements' do
43 | it { is_expected.to eq '' }
44 | end
45 |
46 | context 'with the last element that has no tag_styles' do
47 | before do
48 | context << PrawnHtml::Tag.new(:some_tag)
49 | end
50 |
51 | it { is_expected.to eq '' }
52 | end
53 |
54 | context 'with the last element that has some tag_styles' do
55 | let(:some_tag_class) do
56 | Class.new(PrawnHtml::Tag) do
57 | def before_content
58 | 'Some before content'
59 | end
60 | end
61 | end
62 |
63 | before do
64 | context << some_tag_class.new(:some_tag)
65 | end
66 |
67 | it { is_expected.to eq 'Some before content' }
68 | end
69 | end
70 |
71 | describe '#block_styles' do
72 | subject(:block_styles) { context.block_styles }
73 |
74 | context 'with no elements' do
75 | it { is_expected.to eq({}) }
76 | end
77 |
78 | context 'with some elements' do
79 | let(:tag1) { instance_double(PrawnHtml::Tag, block_styles: { margin_left: 11.11 }) }
80 | let(:tag2) { instance_double(PrawnHtml::Tag, block_styles: { margin_left: 22.22 }) }
81 |
82 | before do
83 | allow(PrawnHtml::Attributes).to receive(:merge_attr!).and_call_original
84 | context << tag1 << tag2
85 | end
86 |
87 | it 'merges the block styles of the elements', :aggregate_failures do
88 | expect(block_styles).to eq(margin_left: 33.33)
89 | expect(PrawnHtml::Attributes).to have_received(:merge_attr!).twice
90 | end
91 | end
92 | end
93 |
94 | describe '#remove_last' do
95 | subject(:remove_last) { context.remove_last }
96 |
97 | let(:some_tag_class) do
98 | Class.new(PrawnHtml::Tag) do
99 | def on_context_remove(context)
100 | # callback after removal
101 | end
102 | end
103 | end
104 | let(:tag) { instance_double(some_tag_class, on_context_remove: true, :parent= => true, tag: :some_tag) }
105 |
106 | before do
107 | context.add(tag)
108 | end
109 |
110 | it 'removes the last element from the context', :aggregate_failures do
111 | expect { remove_last }.to(
112 | change(context, :size).from(1).to(0).and(
113 | change(context, :previous_tag).from(nil).to(tag)
114 | )
115 | )
116 | expect(tag).to have_received(:on_context_remove)
117 | end
118 | end
119 |
120 | describe '#merged_styles' do
121 | subject(:merged_styles) { context.merged_styles }
122 |
123 | context 'with no elements' do
124 | it { is_expected.to eq(size: PrawnHtml::Context::DEFAULT_STYLES[:size]) }
125 | end
126 |
127 | context 'with some elements' do
128 | let(:tag1) { instance_double(PrawnHtml::Tag, styles: { color: 'fb1', size: 12.34 }, update_styles: nil) }
129 | let(:tag2) { instance_double(PrawnHtml::Tag, styles: { color: 'abc' }, update_styles: nil) }
130 |
131 | before do
132 | context << tag1 << tag2
133 | end
134 |
135 | it 'merges the styles of the elements' do
136 | expect(merged_styles).to match(color: 'abc', size: 12.34)
137 | end
138 | end
139 |
140 | context 'with an element with update_styles' do
141 | let(:some_tag_class) do
142 | Class.new(PrawnHtml::Tag) do
143 | def update_styles(res)
144 | res[:some_style] = :some_value
145 | end
146 | end
147 | end
148 | let(:tag1) { instance_double(PrawnHtml::Tag, styles: { color: 'fb1', size: 12.34 }, update_styles: nil) }
149 | let(:tag2) { some_tag_class.new(:some_tag) }
150 |
151 | before do
152 | allow(tag2).to receive(:update_styles).and_call_original
153 | context << tag1 << tag2
154 | end
155 |
156 | it 'sends the context styles to the update_styles method', :aggregate_failures do
157 | expect(merged_styles).to match(color: 'fb1', size: 12.34, some_style: :some_value)
158 | expect(tag2).to have_received(:update_styles)
159 | end
160 | end
161 | end
162 |
163 | describe '#white_space_pre?' do
164 | subject(:white_space_pre?) { context.white_space_pre? }
165 |
166 | before do
167 | context << instance_double(PrawnHtml::Tag, styles: {})
168 | end
169 |
170 | it { is_expected.to be_falsey }
171 |
172 | context 'when the last element has white-space property set to pre' do
173 | before do
174 | context << instance_double(PrawnHtml::Tag, styles: { white_space: :pre })
175 | end
176 |
177 | it { is_expected.to be_truthy }
178 | end
179 | end
180 | end
181 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/document_renderer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::DocumentRenderer do
4 | subject(:document_renderer) { described_class.new(pdf) }
5 |
6 | let(:context) { PrawnHtml::Context.new }
7 | let(:pdf) { PrawnHtml::PdfWrapper.new(pdf_doc) }
8 | let(:pdf_doc) { Prawn::Document.new }
9 |
10 | before do
11 | allow(PrawnHtml::Context).to receive(:new).and_return(context)
12 | end
13 |
14 | describe '#on_tag_close' do
15 | subject(:on_tag_close) { document_renderer.on_tag_close(element) }
16 |
17 | let(:element) { PrawnHtml::Tags::Div.new(:div) }
18 |
19 | before do
20 | allow(context).to receive(:remove_last)
21 | allow(element).to receive(:tag_close_styles).and_call_original
22 | end
23 |
24 | it 'handles tag closing', :aggregate_failures do
25 | on_tag_close
26 | expect(element).to have_received(:tag_close_styles)
27 | expect(context).to have_received(:remove_last)
28 | end
29 | end
30 |
31 | describe '#on_tag_open' do
32 | subject(:on_tag_open) do
33 | document_renderer.on_tag_open(tag, attributes: attributes, element_styles: element_styles)
34 | end
35 |
36 | let(:attributes) { { 'class' => 'green' } }
37 | let(:element_styles) { 'color: red' }
38 |
39 | context 'with a div tag' do
40 | let(:tag) { :div }
41 |
42 | before do
43 | allow(context).to receive(:add)
44 | end
45 |
46 | it { is_expected.to be_kind_of PrawnHtml::Tags::Div }
47 |
48 | it 'adds the element to the context' do
49 | on_tag_open
50 | expect(context).to have_received(:add)
51 | end
52 | end
53 |
54 | context 'with an unknown tag' do
55 | let(:tag) { :unknown_tag }
56 |
57 | it { is_expected.to be_nil }
58 | end
59 | end
60 |
61 | describe '#on_text_node' do
62 | subject(:on_text_node) { document_renderer.on_text_node(content) }
63 |
64 | let(:content) { 'some content' }
65 |
66 | it { is_expected.to be_nil }
67 | end
68 |
69 | describe '#render' do
70 | subject(:render) { document_renderer.render }
71 |
72 | before do
73 | allow(pdf).to receive_messages(puts: true)
74 | end
75 |
76 | it "renders nothing when the buffer's content is empty" do
77 | render
78 | expect(pdf).not_to have_received(:puts)
79 | end
80 |
81 | context 'with some content in the buffer' do
82 | before do
83 | document_renderer.on_text_node('Some content')
84 | end
85 |
86 | it "renders the current buffer's content" do
87 | render
88 | expect(pdf).to have_received(:puts)
89 | end
90 | end
91 |
92 | context 'with position absolute, top and left properties' do
93 | before do
94 | document_renderer.on_tag_open(:div, attributes: { 'style' => 'position: absolute; top: 10px; left: 50px' })
95 | document_renderer.on_text_node('Some content')
96 | end
97 |
98 | it "renders the current buffer's content in a bounded box" do
99 | render
100 | expect(pdf).to have_received(:puts)
101 | end
102 | end
103 |
104 | context 'with position absolute, bottom and right properties' do
105 | before do
106 | document_renderer.on_tag_open(:div, attributes: { 'style' => 'position: absolute; bottom: 10px; right: 80px' })
107 | document_renderer.on_text_node('Some content')
108 | end
109 |
110 | it "renders the current buffer's content in a bounded box" do
111 | render
112 | expect(pdf).to have_received(:puts)
113 | end
114 | end
115 | end
116 |
117 | describe 'alias method: flush' do
118 | it { is_expected.to respond_to(:flush) }
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/html_parser_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::HtmlParser do
4 | subject(:html_parser) { described_class.new(renderer) }
5 |
6 | let(:renderer) { instance_double(PrawnHtml::DocumentRenderer, flush: true, on_text_node: nil) }
7 |
8 | describe 'REGEXP_STYLES' do
9 | subject(:regexp) { text.scan(described_class::REGEXP_STYLES) }
10 |
11 | context 'with a single rule and a single property: "i{color:red}"' do
12 | let(:text) { 'i{color:red}' }
13 |
14 | it { is_expected.to match_array [['i', 'color:red']] }
15 | end
16 |
17 | context 'with a single rule and more properties: "i { color: red; font-style: italic; font-weight: bold }"' do
18 | let(:text) { 'i { color: red; font-style: italic; font-weight: bold }' }
19 |
20 | it { is_expected.to match_array [['i', 'color: red; font-style: italic; font-weight: bold']] }
21 | end
22 |
23 | context 'with some rules' do
24 | let(:text) do
25 | <<~CSS
26 | i {
27 | color: red;
28 | font-style: italic; font-weight: bold
29 | }
30 | b { font-size: 20px; }
31 | u {
32 | color: RGB(128, 128, 128);
33 | }
34 | CSS
35 | end
36 |
37 | it { is_expected.to match_array [["i", "color: red;\n font-style: italic; font-weight: bold"], ["b", "font-size: 20px;"], ["u", "color: RGB(128, 128, 128);"]] }
38 | end
39 | end
40 |
41 | describe '#parse_styles' do
42 | subject(:parse_styles) { html_parser.parse_styles(css) }
43 |
44 | let(:css) { 'h1 { color: red }' }
45 |
46 | it { is_expected.to eq('h1' => 'color: red') }
47 | end
48 |
49 | describe '#process' do
50 | subject(:process) { html_parser.process(html) }
51 |
52 | let(:html) { 'some text' }
53 |
54 | before do
55 | allow(Oga).to receive(:parse_html).and_call_original
56 | end
57 |
58 | it 'calls Oga parse html', :aggregate_failures do
59 | process
60 | expect(Oga).to have_received(:parse_html).with(html)
61 | expect(renderer).to have_received(:on_text_node).with(html)
62 | end
63 | end
64 |
65 | describe 'ignore some elements' do
66 | let(:process) { html_parser.process(html) }
67 |
68 | before do
69 | allow(Oga).to receive(:parse_html).and_call_original
70 | allow(PrawnHtml::IgnoredTag).to receive(:new).and_call_original
71 | allow(renderer).to receive(:on_tag_open)
72 | end
73 |
74 | context 'with an HTML comment' do
75 | let(:html) { 'before comment bold --> italicafter' }
76 |
77 | it 'skips the HTML comments', :aggregate_failures do
78 | process
79 | expect(renderer).to have_received(:on_text_node).with('before').ordered
80 | expect(renderer).not_to have_received(:on_text_node).with('bold')
81 | expect(renderer).to have_received(:on_text_node).with('italic').ordered
82 | expect(renderer).to have_received(:on_text_node).with('after').ordered
83 | end
84 | end
85 |
86 | context 'with a script tag' do
87 | let(:html) { 'beforeafter' }
88 |
89 | it 'skips the content of the script tag', :aggregate_failures do
90 | process
91 | expect(renderer).to have_received(:on_text_node).with('before').ordered
92 | expect(PrawnHtml::IgnoredTag).to have_received(:new).with(:script).ordered
93 | expect(renderer).not_to have_received(:on_text_node).with('a script')
94 | expect(renderer).to have_received(:on_text_node).with('after').ordered
95 | end
96 | end
97 |
98 | context 'with a style tag' do
99 | let(:html) { 'beforeafter' }
100 |
101 | before do
102 | allow(renderer).to receive(:on_tag_open)
103 | end
104 |
105 | it 'skips the content of style tag but parses the styles', :aggregate_failures do
106 | process
107 | expect(renderer).to have_received(:on_text_node).with('before').ordered
108 | expect(PrawnHtml::IgnoredTag).to have_received(:new).with(:style).ordered
109 | expect(renderer).not_to have_received(:on_text_node).with('some styles')
110 | expect(renderer).to have_received(:on_text_node).with('after').ordered
111 | end
112 | end
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/instance_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Instance do
4 | describe 'initialize' do
5 | subject(:instance) { described_class.new(pdf) }
6 |
7 | let(:pdf) { instance_double(Prawn::Document) }
8 |
9 | before do
10 | allow(PrawnHtml::PdfWrapper).to receive(:new)
11 | allow(PrawnHtml::DocumentRenderer).to receive(:new)
12 | allow(PrawnHtml::HtmlParser).to receive(:new)
13 | end
14 |
15 | it 'initializes the required entities', :aggregate_failures do
16 | instance
17 | expect(PrawnHtml::PdfWrapper).to have_received(:new)
18 | expect(PrawnHtml::DocumentRenderer).to have_received(:new)
19 | expect(PrawnHtml::HtmlParser).to have_received(:new)
20 | end
21 | end
22 |
23 | describe '#append' do
24 | subject(:append) { described_class.new(pdf).append(html: html) }
25 |
26 | let(:html) { 'Some HTML
' }
27 | let(:html_parser) { instance_double(PrawnHtml::HtmlParser, process: nil) }
28 | let(:pdf) { instance_double(Prawn::Document) }
29 |
30 | before do
31 | allow(PrawnHtml::HtmlParser).to receive(:new).and_return(html_parser)
32 | end
33 |
34 | it 'sends a process message to the html parser' do
35 | append
36 | expect(html_parser).to have_received(:process).with(html)
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/pdf_wrapper_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::PdfWrapper do
4 | subject(:pdf_wrapper) { described_class.new(pdf) }
5 |
6 | let(:pdf) { instance_double(Prawn::Document) }
7 |
8 | describe 'delegated methods' do
9 | %i[start_new_page].each do |method_name|
10 | context "with #{method_name} method" do
11 | before do
12 | allow(pdf).to receive(method_name)
13 | end
14 |
15 | it 'delegates the method call to the pdf instance' do
16 | pdf_wrapper.send(method_name)
17 | expect(pdf).to have_received(method_name)
18 | end
19 | end
20 | end
21 | end
22 |
23 | describe '#advance_cursor' do
24 | subject(:advance_cursor) { pdf_wrapper.advance_cursor(quantity) }
25 |
26 | before do
27 | allow(pdf).to receive(:move_down)
28 | end
29 |
30 | context 'with some quantity' do
31 | let(:quantity) { 20 }
32 |
33 | it 'delegates to PDF move_down' do
34 | advance_cursor
35 | expect(pdf).to have_received(:move_down).with(20)
36 | end
37 | end
38 |
39 | context 'with zero quantity' do
40 | let(:quantity) { 0 }
41 |
42 | it "doesn't move down" do
43 | advance_cursor
44 | expect(pdf).not_to have_received(:move_down).with(20)
45 | end
46 | end
47 | end
48 |
49 | describe '#calc_buffer_height' do
50 | subject(:calc_buffer_height) { pdf_wrapper.calc_buffer_height(buffer, options) }
51 |
52 | let(:buffer) { [{ text: 'some content' }] }
53 | let(:options) { {} }
54 |
55 | before do
56 | allow(pdf).to receive(:height_of_formatted)
57 | end
58 |
59 | it 'calls the PDF height_of_formatted method' do
60 | calc_buffer_height
61 | expect(pdf).to have_received(:height_of_formatted).with(buffer, options)
62 | end
63 | end
64 |
65 | describe '#calc_buffer_width' do
66 | subject(:calc_buffer_width) { pdf_wrapper.calc_buffer_width(buffer) }
67 |
68 | let(:buffer) { [{ text: 'some content', font: 'Courier', size: 12 }] }
69 |
70 | before do
71 | allow(pdf).to receive(:font).and_yield
72 | allow(pdf).to receive(:width_of).and_return(0)
73 | end
74 |
75 | it 'calls the PDF width_of method' do
76 | calc_buffer_width
77 | expect(pdf).to have_received(:width_of) # .with(buffer, inline_format: true)
78 | end
79 | end
80 |
81 | describe '#draw_rectangle' do
82 | subject(:draw_rectangle) { pdf_wrapper.draw_rectangle(x: 50, y: 80, width: 200, height: 150, color: 'ffbb111') }
83 |
84 | before do
85 | allow(pdf).to receive_messages(fill_color: 'aabbbcc', :fill_color= => true, fill_rectangle: true)
86 | end
87 |
88 | it 'calls the PDF fill_rectangle method', :aggregate_failures do
89 | draw_rectangle
90 | expect(pdf).to have_received(:fill_color=).with('ffbb111').ordered
91 | expect(pdf).to have_received(:fill_rectangle).with([80, 50], 200, 150).ordered
92 | expect(pdf).to have_received(:fill_color=).with('aabbbcc').ordered
93 | end
94 | end
95 |
96 | describe '#horizontal_rule' do
97 | subject(:horizontal_rule) { pdf_wrapper.horizontal_rule(color: color, dash: dash) }
98 |
99 | let(:color) { 'ffbb11' }
100 | let(:dash) { 5 }
101 |
102 | before do
103 | methods = { dash: nil, stroke_color: 'abcdef', :stroke_color= => nil, stroke_horizontal_rule: nil, undash: nil }
104 | allow(pdf).to receive_messages(methods)
105 | end
106 |
107 | it 'calls the PDF stroke_horizontal_rule method', :aggregate_failures do
108 | horizontal_rule
109 | expect(pdf).to have_received(:stroke_color).ordered
110 | expect(pdf).to have_received(:dash).with(5).ordered
111 | expect(pdf).to have_received(:stroke_color=).with('ffbb11').ordered
112 | expect(pdf).to have_received(:stroke_horizontal_rule).ordered
113 | expect(pdf).to have_received(:stroke_color=).with('abcdef').ordered
114 | expect(pdf).to have_received(:undash).ordered
115 | end
116 | end
117 |
118 | describe '#image' do
119 | subject(:image) { pdf_wrapper.image(src, options) }
120 |
121 | let(:src) { 'some_image_path' }
122 | let(:options) { {} }
123 |
124 | before do
125 | allow(pdf).to receive(:image)
126 | end
127 |
128 | it 'calls the PDF image method' do
129 | image
130 | expect(pdf).to have_received(:image).with(src, options)
131 | end
132 | end
133 |
134 | describe '#puts' do
135 | subject(:puts) { pdf_wrapper.puts(buffer, options, bounding_box: bounding_box) }
136 |
137 | let(:bounding_box) { nil }
138 | let(:buffer) { [] }
139 | let(:options) { {} }
140 |
141 | before do
142 | allow(pdf).to receive(:formatted_text)
143 | end
144 |
145 | it 'calls the PDF formatted_text method' do
146 | puts
147 | expect(pdf).to have_received(:formatted_text).with(buffer, options)
148 | end
149 | end
150 |
151 | describe '#underline' do
152 | subject(:underline) { pdf_wrapper.underline(x1: x1, x2: x2, y: y) }
153 |
154 | let(:x1) { 20 }
155 | let(:x2) { 50 }
156 | let(:y) { 40 }
157 |
158 | before do
159 | allow(pdf).to receive(:stroke).and_yield
160 | allow(pdf).to receive(:line)
161 | end
162 |
163 | it 'calls the PDF formatted_text method', :aggregate_failures do
164 | underline
165 | expect(pdf).to have_received(:stroke).ordered
166 | expect(pdf).to have_received(:line).with([20, 40], [50, 40]).ordered
167 | end
168 | end
169 | end
170 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tag_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tag do
4 | subject(:tag) { described_class.new(:some_tag, attributes: attributes) }
5 |
6 | let(:attributes) { { 'style' => 'color: #0088ff', 'some_attr' => 'some value' } }
7 |
8 | describe '#initialize' do
9 | before do
10 | allow(PrawnHtml::Attributes).to receive(:new).and_call_original
11 | end
12 |
13 | it 'instantiates a new Attributes object', :aggregate_failures do
14 | tag
15 | expect(PrawnHtml::Attributes).to have_received(:new).with(attributes)
16 | expect(tag.attrs).to be_kind_of(PrawnHtml::Attributes)
17 | end
18 |
19 | it 'sets the tag' do
20 | expect(tag.tag).to eq(:some_tag)
21 | end
22 | end
23 |
24 | describe '#block?' do
25 | subject(:block?) { tag.block? }
26 |
27 | it { is_expected.to be_falsey }
28 | end
29 |
30 | describe '#block_styles' do
31 | subject(:block_styles) { tag.block_styles }
32 |
33 | it { is_expected.to eq({}) }
34 |
35 | context 'with some data attributes' do
36 | let(:attributes) { { 'data-mode' => 'mode1' } }
37 |
38 | it 'delegates to the tag attributes' do
39 | expect(block_styles).to eq(mode: :mode1)
40 | end
41 | end
42 | end
43 |
44 | describe '#process_styles' do
45 | subject(:process_styles) { tag.process_styles(element_styles: element_styles) }
46 |
47 | let(:element_styles) { nil }
48 |
49 | before do
50 | allow(tag.attrs).to receive(:merge_text_styles!)
51 | end
52 |
53 | it 'merges the inline styles' do
54 | process_styles
55 | expect(tag.attrs).to have_received(:merge_text_styles!).with('color: #0088ff', options: {})
56 | end
57 |
58 | context 'with some additional styles' do
59 | let(:some_tag_class) do
60 | Class.new(described_class) do
61 | def extra_styles
62 | 'color: green; text-decoration: underline'
63 | end
64 |
65 | def tag_styles
66 | 'color: yellow; font-style: italic'
67 | end
68 | end
69 | end
70 |
71 | let(:element_styles) { 'color: red; font-weight: bold' }
72 | let(:tag) { some_tag_class.new(:some_tag, attributes: attributes) }
73 |
74 | it 'merges the tag styles', :aggregate_failures do
75 | process_styles
76 |
77 | expected_styles = 'color: yellow; font-style: italic'
78 | expect(tag.attrs).to have_received(:merge_text_styles!).with(expected_styles, options: {}).ordered
79 | expect(tag.attrs).to have_received(:merge_text_styles!).with(element_styles, options: {}).ordered
80 | expect(tag.attrs).to have_received(:merge_text_styles!).with('color: #0088ff', options: {}).ordered
81 | expected_styles = 'color: green; text-decoration: underline'
82 | expect(tag.attrs).to have_received(:merge_text_styles!).with(expected_styles, options: {}).ordered
83 | end
84 | end
85 | end
86 |
87 | describe '#styles' do
88 | subject(:styles) { tag.styles }
89 |
90 | before do
91 | tag.process_styles
92 | end
93 |
94 | it { is_expected.to eq(color: '0088ff') }
95 | end
96 |
97 | describe '#tag_close_styles' do
98 | subject(:tag_close_styles) { tag.tag_close_styles }
99 |
100 | it { is_expected.to eq({}) }
101 | end
102 |
103 | describe '#tag_open_styles' do
104 | subject(:tag_open_styles) { tag.tag_open_styles }
105 |
106 | it { is_expected.to eq({}) }
107 | end
108 |
109 | describe '#update_styles' do
110 | subject(:update_styles) { tag.update_styles(context_styles) }
111 |
112 | let(:context_styles) { { size: 9.6 } }
113 |
114 | before do
115 | allow(tag.attrs).to receive(:update_styles)
116 | end
117 |
118 | it 'asks to the attributes to update the context styles' do
119 | update_styles
120 | expect(tag.attrs).to have_received(:update_styles).with(context_styles)
121 | end
122 | end
123 |
124 | describe '.class_for' do
125 | subject(:class_for) { described_class.class_for(tag_name) }
126 |
127 | context 'with an unknown tag name' do
128 | let(:tag_name) { :some_tag }
129 |
130 | it { is_expected.to be_nil }
131 | end
132 |
133 | context 'with an h6 tag' do
134 | let(:tag_name) { :h6 }
135 |
136 | it { is_expected.to eq(PrawnHtml::Tags::H) }
137 | end
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/a_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::A do
4 | subject(:a) { described_class.new(:a, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | context 'without attributes' do
9 | before do
10 | a.process_styles
11 | end
12 |
13 | it "styles doesn't include the link property" do
14 | expect(a.styles).to eq(color: 'ffbb11', styles: [:underline])
15 | end
16 | end
17 |
18 | context 'with an href attribute' do
19 | subject(:a) do
20 | described_class.new(:a, attributes: { 'href' => 'https://www.google.it', 'style' => 'font-weight: bold' })
21 | end
22 |
23 | before do
24 | a.process_styles
25 | end
26 |
27 | it 'includes the link property in the styles' do
28 | expect(a.styles).to match(color: '0000ee', link: 'https://www.google.it', styles: [:underline, :bold])
29 | end
30 | end
31 |
32 | describe 'tag rendering' do
33 | include_context 'with pdf wrapper'
34 |
35 | let(:html) { 'A link' }
36 |
37 | before { append_html_to_pdf(html) }
38 |
39 | it 'sends the expected buffer elements to the pdf', :aggregate_failures do
40 | expected_buffer = [{ size: TestUtils.default_font_size, text: 'A link', link: 'https://www.google.it', color: '0000ee', styles: [:underline] }]
41 | expected_options = { leading: TestUtils.adjust_leading }
42 | expected_extra = { bounding_box: nil, left_indent: 0 }
43 |
44 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/b_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::B do
4 | subject(:b) { described_class.new(:b, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | context 'without attributes' do
9 | before do
10 | b.process_styles
11 | end
12 |
13 | it 'returns the expected styles for b tag' do
14 | expect(b.styles).to match(color: 'ffbb11', styles: [:bold])
15 | end
16 | end
17 |
18 | describe 'tag rendering' do
19 | include_context 'with pdf wrapper'
20 |
21 | before { append_html_to_pdf(html) }
22 |
23 | context 'with a B tag' do
24 | let(:html) { 'Some sample content' }
25 |
26 | it 'sends the expected buffer elements to the pdf' do
27 | expect(pdf).to have_received(:puts).with(
28 | [{ size: TestUtils.default_font_size, styles: [:bold], text: "Some sample content" }],
29 | { leading: TestUtils.adjust_leading },
30 | { bounding_box: nil, left_indent: 0 }
31 | )
32 | end
33 | end
34 |
35 | context 'with a Strong tag' do
36 | let(:html) { 'Some sample content' }
37 |
38 | it 'sends the expected buffer elements to the pdf' do
39 | expect(pdf).to have_received(:puts).with(
40 | [{ size: TestUtils.default_font_size, styles: [:bold], text: "Some sample content" }],
41 | { leading: TestUtils.adjust_leading },
42 | { bounding_box: nil, left_indent: 0 }
43 | )
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/blockquote_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Blockquote do
4 | subject(:blockquote) { described_class.new(:blockquote, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | context 'without attributes' do
9 | before do
10 | blockquote.process_styles
11 | end
12 |
13 | it 'returns the expected styles for blockquote tag' do
14 | expected_styles = {
15 | color: 'ffbb11',
16 | margin_bottom: PrawnHtml::Utils.convert_size(described_class::MARGIN_BOTTOM.to_s),
17 | margin_left: PrawnHtml::Utils.convert_size(described_class::MARGIN_LEFT.to_s),
18 | margin_top: PrawnHtml::Utils.convert_size(described_class::MARGIN_TOP.to_s)
19 | }
20 | expect(blockquote.styles).to match(expected_styles)
21 | end
22 | end
23 |
24 | describe '#block?' do
25 | subject(:block?) { blockquote.block? }
26 |
27 | it { is_expected.to be_truthy }
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/body_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Body do
4 | subject(:body) { described_class.new(:body) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#block?' do
9 | subject(:block?) { body.block? }
10 |
11 | it { is_expected.to be_truthy }
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/br_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Br do
4 | subject(:br) { described_class.new(:br) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#block?' do
9 | subject(:block?) { br.block? }
10 |
11 | it { is_expected.to be_truthy }
12 | end
13 |
14 | describe '#custom_render' do
15 | subject(:custom_render) { br.custom_render(pdf, context) }
16 |
17 | let(:context) { instance_double(PrawnHtml::Context, last_text_node: false, previous_tag: nil) }
18 | let(:pdf) { instance_double(PrawnHtml::PdfWrapper, advance_cursor: true) }
19 |
20 | it "doesn't call advance_cursor on the pdf wrapper" do
21 | custom_render
22 | expect(pdf).not_to have_received(:advance_cursor)
23 | end
24 |
25 | context 'when the last node in the context is another br' do
26 | let(:context) { instance_double(PrawnHtml::Context, last_text_node: false, previous_tag: prev_tag) }
27 | let(:prev_tag) { PrawnHtml::Tags::Br.new(:br) } # rubocop:disable RSpec/DescribedClass
28 |
29 | it 'calls advance_cursor on the pdf wrapper' do
30 | custom_render
31 | expect(pdf).to have_received(:advance_cursor)
32 | end
33 | end
34 |
35 | context 'when the last node in the context is of type text' do
36 | let(:context) { instance_double(PrawnHtml::Context, last_text_node: true) }
37 |
38 | it "doesn't call advance_cursor on the pdf wrapper" do
39 | custom_render
40 | expect(pdf).not_to have_received(:advance_cursor)
41 | end
42 | end
43 | end
44 |
45 | describe 'tag rendering' do
46 | include_context 'with pdf wrapper'
47 |
48 | let(:html) { 'First line
Second line
Third line
Last line' }
49 |
50 | before { append_html_to_pdf(html) }
51 |
52 | it 'sends the expected buffer elements to the pdf', :aggregate_failures do
53 | expected_options = { leading: TestUtils.adjust_leading }
54 | expected_extra = { bounding_box: nil, left_indent: 0 }
55 |
56 | expected_text = { text: "First line", size: TestUtils.default_font_size }
57 | expect(pdf).to have_received(:puts).with([expected_text], expected_options, expected_extra).ordered
58 |
59 | expected_text = { text: "Second line", size: TestUtils.default_font_size }
60 | expect(pdf).to have_received(:puts).with([expected_text], expected_options, expected_extra).ordered
61 |
62 | expected_text = { text: "Third line", size: TestUtils.default_font_size }
63 | expect(pdf).to have_received(:puts).with([expected_text], expected_options, expected_extra).ordered
64 |
65 | expect(pdf).to have_received(:advance_cursor).with(described_class::BR_SPACING).ordered
66 |
67 | expected_text = { text: "Last line", size: TestUtils.default_font_size }
68 | expect(pdf).to have_received(:puts).with([expected_text], expected_options, expected_extra).ordered
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/code_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Code do
4 | subject(:code) { described_class.new(:code, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | context 'without attributes' do
9 | before do
10 | code.process_styles
11 | end
12 |
13 | it 'returns the expected styles for code tag' do
14 | expect(code.styles).to match(color: 'ffbb11', font: 'Courier')
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/del_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Del do
4 | subject(:del) { described_class.new(:del, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#styles' do
9 | subject(:styles) { del.styles }
10 |
11 | before do
12 | del.process_styles
13 | end
14 |
15 | it 'merges the callback property into styles' do
16 | expect(styles).to match(color: 'ffbb11', callback: ['StrikeThrough', nil])
17 | end
18 | end
19 |
20 | describe 'tag rendering' do
21 | include_context 'with pdf wrapper'
22 |
23 | before { append_html_to_pdf(html) }
24 |
25 | context 'with a Del tag' do
26 | let(:html) { 'Some sample content' }
27 |
28 | it 'sends the expected buffer elements to the pdf' do
29 | expect(pdf).to have_received(:puts).with(
30 | [{ callback: anything, size: TestUtils.default_font_size, text: "Some sample content" }],
31 | { leading: TestUtils.adjust_leading },
32 | { bounding_box: nil, left_indent: 0 }
33 | )
34 | end
35 | end
36 |
37 | context 'with a S tag' do
38 | let(:html) { 'Some sample content' }
39 |
40 | it 'sends the expected buffer elements to the pdf' do
41 | expect(pdf).to have_received(:puts).with(
42 | [{ callback: anything, size: TestUtils.default_font_size, text: "Some sample content" }],
43 | { leading: TestUtils.adjust_leading },
44 | { bounding_box: nil, left_indent: 0 }
45 | )
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/div_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Div do
4 | subject(:div) { described_class.new(:div) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#block?' do
9 | subject(:block?) { div.block? }
10 |
11 | it { is_expected.to be_truthy }
12 | end
13 |
14 | describe 'tag rendering' do
15 | include_context 'with pdf wrapper'
16 |
17 | let(:html) { 'Some sample content
' }
18 |
19 | before { append_html_to_pdf(html) }
20 |
21 | it 'sends the expected buffer elements to the pdf' do
22 | expect(pdf).to have_received(:puts).with(
23 | [{ size: TestUtils.default_font_size, text: "Some sample content" }],
24 | { leading: TestUtils.adjust_leading },
25 | { bounding_box: nil, left_indent: 0 }
26 | )
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/h_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::H do
4 | subject(:h1) { described_class.new(:h1, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#block?' do
9 | subject(:block?) { h1.block? }
10 |
11 | it { is_expected.to be_truthy }
12 | end
13 |
14 | context 'without attributes' do
15 | before do
16 | h1.process_styles
17 | end
18 |
19 | it 'returns the expected styles for h1 tag' do
20 | expected_styles = {
21 | color: 'ffbb11',
22 | margin_bottom: PrawnHtml::Utils.convert_size(described_class::MARGINS_BOTTOM[:h1].to_s),
23 | margin_top: PrawnHtml::Utils.convert_size(described_class::MARGINS_TOP[:h1].to_s),
24 | size: PrawnHtml::Utils.convert_size(described_class::SIZES[:h1].to_s),
25 | styles: [:bold]
26 | }
27 | expect(h1.styles).to match(expected_styles)
28 | end
29 | end
30 |
31 | describe 'tag rendering' do
32 | include_context 'with pdf wrapper'
33 |
34 | let(:expected_buffer) { [{ size: size, styles: [:bold], text: 'Some sample content' }] }
35 | let(:expected_options) { { leading: TestUtils.adjust_leading(size) } }
36 | let(:expected_extra) { { bounding_box: nil, left_indent: 0 } }
37 |
38 | before { append_html_to_pdf(html) }
39 |
40 | context 'with some content in an element h1' do
41 | let(:html) { 'Some sample content
' }
42 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h1].to_s) }
43 |
44 | it 'sends the expected buffer elements to the pdf' do
45 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
46 | end
47 | end
48 |
49 | context 'with some content in an element h2' do
50 | let(:html) { 'Some sample content
' }
51 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h2].to_s) }
52 |
53 | it 'sends the expected buffer elements to Prawn pdf' do
54 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
55 | end
56 | end
57 |
58 | context 'with some content in an element h3' do
59 | let(:html) { 'Some sample content
' }
60 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h3].to_s) }
61 |
62 | it 'sends the expected buffer elements to Prawn pdf' do
63 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
64 | end
65 | end
66 |
67 | context 'with some content in an element h4' do
68 | let(:html) { 'Some sample content
' }
69 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h4].to_s) }
70 |
71 | it 'sends the expected buffer elements to Prawn pdf' do
72 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
73 | end
74 | end
75 |
76 | context 'with some content in an element h5' do
77 | let(:html) { 'Some sample content
' }
78 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h5].to_s) }
79 |
80 | it 'sends the expected buffer elements to Prawn pdf' do
81 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
82 | end
83 | end
84 |
85 | context 'with some content in an element h6' do
86 | let(:html) { 'Some sample content
' }
87 | let(:size) { PrawnHtml::Utils.convert_size(PrawnHtml::Tags::H::SIZES[:h6].to_s) }
88 |
89 | it 'sends the expected buffer elements to Prawn pdf' do
90 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
91 | end
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/hr_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Hr do
4 | subject(:hr) { described_class.new(:hr, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | let(:pdf) { instance_double(PrawnHtml::PdfWrapper, horizontal_rule: true) }
7 |
8 | it { expect(described_class).to be < PrawnHtml::Tag }
9 |
10 | describe '#block?' do
11 | subject(:block?) { hr.block? }
12 |
13 | it { is_expected.to be_truthy }
14 | end
15 |
16 | describe '#custom_render' do
17 | subject(:custom_render) { hr.custom_render(pdf, context) }
18 |
19 | before do
20 | hr.process_styles
21 | end
22 |
23 | let(:context) { nil }
24 |
25 | it 'calls horizontal_rule on the pdf wrapper' do
26 | custom_render
27 | expect(pdf).to have_received(:horizontal_rule).with(color: 'ffbb11', dash: nil)
28 | end
29 |
30 | context 'with a dash number set' do
31 | subject(:hr) { described_class.new(:hr, attributes: { 'data-dash' => '5' }) }
32 |
33 | it 'calls the dash methods around stroke', :aggregate_failures do
34 | custom_render
35 | expect(pdf).to have_received(:horizontal_rule).with(color: nil, dash: 5)
36 | end
37 | end
38 |
39 | context 'with a dash array set' do
40 | subject(:hr) { described_class.new(:hr, attributes: { 'data-dash' => '1, 2, 3' }) }
41 |
42 | it 'calls the dash methods around stroke', :aggregate_failures do
43 | custom_render
44 | expect(pdf).to have_received(:horizontal_rule).with(color: nil, dash: [1, 2, 3])
45 | end
46 | end
47 |
48 | context 'with a color set via style attributes' do
49 | subject(:hr) { described_class.new(:hr, attributes: { 'style' => 'color: red' }) }
50 |
51 | it 'calls the color methods around stroke', :aggregate_failures do
52 | custom_render
53 | expect(pdf).to have_received(:horizontal_rule).with(color: 'ff0000', dash: nil)
54 | end
55 | end
56 | end
57 |
58 | context 'without attributes' do
59 | before do
60 | hr.process_styles
61 | end
62 |
63 | it 'returns the expected styles for hr tag' do
64 | expected_styles = {
65 | color: 'ffbb11',
66 | margin_bottom: PrawnHtml::Utils.convert_size(described_class::MARGIN_BOTTOM.to_s),
67 | margin_top: PrawnHtml::Utils.convert_size(described_class::MARGIN_TOP.to_s)
68 | }
69 | expect(hr.styles).to match(expected_styles)
70 | end
71 | end
72 |
73 | describe 'tag rendering' do
74 | include_context 'with pdf wrapper'
75 |
76 | let(:html) { 'Some content
More content' }
77 |
78 | before { append_html_to_pdf(html) }
79 |
80 | it 'sends the expected buffer elements to the pdf', :aggregate_failures do
81 | expect(pdf).to have_received(:puts).with(
82 | [{ size: TestUtils.default_font_size, text: "Some content" }],
83 | { leading: TestUtils.adjust_leading },
84 | { bounding_box: nil, left_indent: 0 }
85 | )
86 | expect(pdf).to have_received(:puts).with(
87 | [{ size: TestUtils.default_font_size, text: "More content" }],
88 | { leading: TestUtils.adjust_leading },
89 | { bounding_box: nil, left_indent: 0 }
90 | )
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/i_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::I do
4 | subject(:i) { described_class.new(:i, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | context 'without attributes' do
9 | before do
10 | i.process_styles
11 | end
12 |
13 | it 'returns the expected styles for i tag' do
14 | expect(i.styles).to match(color: 'ffbb11', styles: [:italic])
15 | end
16 | end
17 |
18 | describe 'tag rendering' do
19 | include_context 'with pdf wrapper'
20 |
21 | before { append_html_to_pdf(html) }
22 |
23 | context 'with an I tag' do
24 | let(:html) { 'Some sample content' }
25 |
26 | it 'sends the expected buffer elements to the pdf' do
27 | expect(pdf).to have_received(:puts).with(
28 | [{ size: TestUtils.default_font_size, styles: [:italic], text: "Some sample content" }],
29 | { leading: TestUtils.adjust_leading },
30 | { bounding_box: nil, left_indent: 0 }
31 | )
32 | end
33 | end
34 |
35 | context 'with an Em tag' do
36 | let(:html) { 'Some sample content' }
37 |
38 | it 'sends the expected buffer elements to the pdf' do
39 | expect(pdf).to have_received(:puts).with(
40 | [{ size: TestUtils.default_font_size, styles: [:italic], text: "Some sample content" }],
41 | { leading: TestUtils.adjust_leading },
42 | { bounding_box: nil, left_indent: 0 }
43 | )
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/img_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Img do
4 | subject(:img) { described_class.new(:img, attributes: { 'src' => 'some_image_url' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#block?' do
9 | subject(:block?) { img.block? }
10 |
11 | it { is_expected.to be_truthy }
12 | end
13 |
14 | describe '#custom_render' do
15 | subject(:custom_render) { img.custom_render(pdf, context) }
16 |
17 | let(:context) { instance_double(PrawnHtml::Context, block_styles: {}) }
18 | let(:pdf) { instance_double(PrawnHtml::PdfWrapper, image: true) }
19 |
20 | it 'calls image on the pdf instance', :aggregate_failures do
21 | custom_render
22 | expect(context).to have_received(:block_styles)
23 | expect(pdf).to have_received(:image).with('some_image_url', {})
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/li_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Li do
4 | subject(:li) { described_class.new(:li, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#block?' do
9 | subject(:block?) { li.block? }
10 |
11 | it { is_expected.to be_truthy }
12 | end
13 |
14 | describe '#before_content' do
15 | subject(:before_content) { li.before_content }
16 |
17 | let(:context) { instance_double(PrawnHtml::Context) }
18 | let(:parent) { PrawnHtml::Tags::Ul.new(:ul) }
19 |
20 | before do
21 | li.parent = parent
22 | li.on_context_add(context)
23 | end
24 |
25 | it 'merges the before_content property into before_content' do
26 | expect(before_content).to eq('• ')
27 | end
28 | end
29 |
30 | describe 'when the parent is an ol element' do
31 | subject(:before_content) { li.before_content }
32 |
33 | let(:context) { instance_double(PrawnHtml::Context) }
34 | let(:parent) { PrawnHtml::Tags::Ol.new(:ol) }
35 |
36 | before do
37 | li.parent = parent
38 | li.on_context_add(context)
39 | end
40 |
41 | it 'sets the counter in before content' do
42 | expect(before_content).to eq('1. ')
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/mark_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Mark do
4 | subject(:mark) { described_class.new(:mark, attributes: { 'style' => 'color: #ffbb11' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#styles' do
9 | subject(:styles) { mark.styles }
10 |
11 | before do
12 | mark.process_styles
13 | end
14 |
15 | it 'merges the callback property into styles' do
16 | expect(styles).to match(color: 'ffbb11', callback: ['Background', 'ffff00'])
17 | end
18 | end
19 |
20 | describe 'tag rendering' do
21 | include_context 'with pdf wrapper'
22 |
23 | let(:html) { 'Some sample content' }
24 |
25 | before { append_html_to_pdf(html) }
26 |
27 | it 'sends the expected buffer elements to the pdf' do
28 | expect(pdf).to have_received(:puts).with(
29 | [{ callback: anything, size: TestUtils.default_font_size, text: "Some sample content" }],
30 | { leading: TestUtils.adjust_leading },
31 | { bounding_box: nil, left_indent: 0 }
32 | )
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/ol_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Ol do
4 | subject(:ol) { described_class.new(:ol, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | it 'sets the counter to zero' do
9 | expect(ol.counter).to be_zero
10 | end
11 |
12 | context 'without attributes' do
13 | before do
14 | ol.process_styles
15 | end
16 |
17 | it 'returns the expected styles for ol tag' do
18 | expected_styles = {
19 | color: 'ffbb11',
20 | margin_left: PrawnHtml::Utils.convert_size(described_class::MARGIN_LEFT.to_s),
21 | }
22 | expect(ol.styles).to match(expected_styles)
23 | end
24 | end
25 |
26 | describe '#block?' do
27 | subject(:block?) { ol.block? }
28 |
29 | it { is_expected.to be_truthy }
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/p_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::P do
4 | subject(:p) { described_class.new(:p, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#block?' do
9 | subject(:block?) { p.block? }
10 |
11 | it { is_expected.to be_truthy }
12 | end
13 |
14 | context 'without attributes' do
15 | before do
16 | p.process_styles
17 | end
18 |
19 | it 'returns the expected styles for p tag' do
20 | expected_styles = {
21 | color: 'ffbb11',
22 | margin_bottom: PrawnHtml::Utils.convert_size(described_class::MARGIN_BOTTOM.to_s),
23 | margin_top: PrawnHtml::Utils.convert_size(described_class::MARGIN_TOP.to_s)
24 | }
25 | expect(p.styles).to match(expected_styles)
26 | end
27 | end
28 |
29 | describe 'tag rendering' do
30 | include_context 'with pdf wrapper'
31 |
32 | let(:html) { 'Some sample content
' }
33 |
34 | before { append_html_to_pdf(html) }
35 |
36 | it 'sends the expected buffer elements to the pdf' do
37 | expect(pdf).to have_received(:puts).with(
38 | [{ size: TestUtils.default_font_size, text: "Some sample content" }],
39 | { leading: TestUtils.adjust_leading },
40 | { bounding_box: nil, left_indent: 0 }
41 | )
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/pre_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Pre do
4 | subject(:pre) { described_class.new(:pre, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | context 'without attributes' do
9 | before do
10 | pre.process_styles
11 | end
12 |
13 | it 'returns the expected styles for pre tag' do
14 | expected_styles = {
15 | color: 'ffbb11',
16 | margin_bottom: PrawnHtml::Utils.convert_size(described_class::MARGIN_BOTTOM.to_s),
17 | margin_top: PrawnHtml::Utils.convert_size(described_class::MARGIN_TOP.to_s),
18 | font: 'Courier',
19 | white_space: :pre
20 | }
21 | expect(pre.styles).to match(expected_styles)
22 | end
23 | end
24 |
25 | describe '#block?' do
26 | subject(:block?) { pre.block? }
27 |
28 | it { is_expected.to be_truthy }
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/small_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Small do
4 | subject(:small) { described_class.new(:small, attributes: { 'style' => 'color: ffbb11' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#update_styles' do
9 | subject(:update_styles) { small.update_styles(styles) }
10 |
11 | let(:styles) { { some_attr: 'some_value' } }
12 |
13 | it 'updates the argument styles reducing the default font size' do
14 | expect(update_styles).to eq(some_attr: 'some_value', size: 8.16)
15 | end
16 |
17 | context 'with a parent font size' do
18 | let(:styles) { { some_attr: 'some_value', size: 20 } }
19 |
20 | it 'updates the argument styles reducing the current font size' do
21 | expect(update_styles).to eq(some_attr: 'some_value', size: 17.0)
22 | end
23 | end
24 | end
25 |
26 | describe 'tag rendering' do
27 | include_context 'with pdf wrapper'
28 |
29 | let(:html) { 'Some sample content' }
30 | let(:size) { PrawnHtml::Context::DEFAULT_STYLES[:size] * 0.85 }
31 |
32 | before { append_html_to_pdf(html) }
33 |
34 | it 'sends the expected buffer elements to the pdf' do
35 | expect(pdf).to have_received(:puts).with(
36 | [{ size: size, text: "Some sample content" }],
37 | { leading: TestUtils.adjust_leading(size) },
38 | { bounding_box: nil, left_indent: 0 }
39 | )
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/span_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Span do
4 | it { expect(described_class).to be < PrawnHtml::Tag }
5 |
6 | describe 'tag rendering' do
7 | include_context 'with pdf wrapper'
8 |
9 | let(:html) { 'Some sample content' }
10 |
11 | before { append_html_to_pdf(html) }
12 |
13 | it 'sends the expected buffer elements to the pdf' do
14 | expect(pdf).to have_received(:puts).with(
15 | [{ size: TestUtils.default_font_size, text: "Some sample content" }],
16 | { leading: TestUtils.adjust_leading },
17 | { bounding_box: nil, left_indent: 0 }
18 | )
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/sub_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Sub do
4 | subject(:sub) { described_class.new(:sub, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | context 'without attributes' do
9 | before do
10 | sub.process_styles
11 | end
12 |
13 | it 'returns the expected styles for sub tag' do
14 | expect(sub.styles).to match(color: 'ffbb11', styles: [:subscript])
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/sup_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Sup do
4 | subject(:sup) { described_class.new(:sup, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | context 'without attributes' do
9 | before do
10 | sup.process_styles
11 | end
12 |
13 | it 'returns the expected styles for sup tag' do
14 | expect(sup.styles).to match(color: 'ffbb11', styles: [:superscript])
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/u_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::U do
4 | subject(:u) { described_class.new(:u, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | context 'without attributes' do
9 | before do
10 | u.process_styles
11 | end
12 |
13 | it 'returns the expected styles for u tag' do
14 | expect(u.styles).to match(color: 'ffbb11', styles: [:underline])
15 | end
16 | end
17 |
18 | describe 'tag rendering' do
19 | include_context 'with pdf wrapper'
20 |
21 | before { append_html_to_pdf(html) }
22 |
23 | context 'with a U tag' do
24 | let(:html) { 'Some sample content' }
25 |
26 | it 'sends the expected buffer elements to the pdf' do
27 | expect(pdf).to have_received(:puts).with(
28 | [{ size: TestUtils.default_font_size, styles: [:underline], text: "Some sample content" }],
29 | { leading: TestUtils.adjust_leading },
30 | { bounding_box: nil, left_indent: 0 }
31 | )
32 | end
33 | end
34 |
35 | context 'with a Ins tag' do
36 | let(:html) { 'Some sample content' }
37 |
38 | it 'sends the expected buffer elements to the pdf' do
39 | expect(pdf).to have_received(:puts).with(
40 | [{ size: TestUtils.default_font_size, styles: [:underline], text: "Some sample content" }],
41 | { leading: TestUtils.adjust_leading },
42 | { bounding_box: nil, left_indent: 0 }
43 | )
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/tags/ul_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Tags::Ul do
4 | subject(:ul) { described_class.new(:ul, attributes: { 'style' => 'color: #fb1' }) }
5 |
6 | it { expect(described_class).to be < PrawnHtml::Tag }
7 |
8 | describe '#block?' do
9 | subject(:block?) { ul.block? }
10 |
11 | it { is_expected.to be_truthy }
12 | end
13 |
14 | context 'without attributes' do
15 | before do
16 | ul.process_styles
17 | end
18 |
19 | it 'returns the expected styles for ul tag' do
20 | expected_styles = {
21 | color: 'ffbb11',
22 | margin_left: PrawnHtml::Utils.convert_size(described_class::MARGIN_LEFT.to_s),
23 | }
24 | expect(ul.styles).to match(expected_styles)
25 | end
26 | end
27 |
28 | describe 'tag rendering' do
29 | include_context 'with pdf wrapper'
30 |
31 | let(:html) do
32 | <<~HTML
33 |
34 | - First item
35 | - Second item
36 | - Third item
37 |
38 | HTML
39 | end
40 | let(:size) { TestUtils.default_font_size }
41 | let(:margin_left) do
42 | PrawnHtml::Utils.convert_size(PrawnHtml::Tags::Ul::MARGIN_LEFT.to_s)
43 | end
44 |
45 | before { append_html_to_pdf(html) }
46 |
47 | it 'sends the expected buffer elements to the pdf', :aggregate_failures do
48 | expect(pdf).to have_received(:puts).with(
49 | [{ size: size, text: "• First item" }],
50 | { indent_paragraphs: PrawnHtml::Tags::Li::INDENT_UL, leading: TestUtils.adjust_leading },
51 | { bounding_box: nil, left_indent: margin_left }
52 | )
53 | expect(pdf).to have_received(:puts).with(
54 | [{ size: size, text: "• Second item" }],
55 | { indent_paragraphs: PrawnHtml::Tags::Li::INDENT_UL, leading: TestUtils.adjust_leading },
56 | { bounding_box: nil, left_indent: margin_left }
57 | )
58 | expect(pdf).to have_received(:puts).with(
59 | [{ size: size, text: "• Third item" }],
60 | { indent_paragraphs: PrawnHtml::Tags::Li::INDENT_UL, leading: TestUtils.adjust_leading },
61 | { bounding_box: nil, left_indent: margin_left }
62 | )
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/spec/units/prawn_html/utils_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml::Utils do
4 | describe '.callback_background' do
5 | subject(:callback_background) { described_class.callback_background(value) }
6 |
7 | context 'with a nil value' do
8 | let(:value) { nil }
9 |
10 | it { is_expected.to eq ['Background', nil] }
11 | end
12 |
13 | context 'with a color value' do
14 | let(:value) { 'red' }
15 |
16 | it { is_expected.to eq ['Background', 'ff0000'] }
17 | end
18 | end
19 |
20 | describe '.callback_strike_through' do
21 | subject(:callback_strike_through) { described_class.callback_strike_through(nil) }
22 |
23 | it { is_expected.to eq ['StrikeThrough', nil] }
24 | end
25 |
26 | describe '.convert_color' do
27 | subject(:convert_color) { described_class.convert_color(value) }
28 |
29 | context 'with a nil value' do
30 | let(:value) { nil }
31 |
32 | it { is_expected.to be_nil }
33 | end
34 |
35 | context 'with an invalid value' do
36 | let(:value) { ' unknown color ' }
37 |
38 | it { is_expected.to be_nil }
39 | end
40 |
41 | context 'with a 3 characters string value (ex. "#A80")' do
42 | let(:value) { ' #A80' }
43 |
44 | it { is_expected.to eq 'aa8800' }
45 | end
46 |
47 | context 'with a 6 characters string value (ex. "#12aB0f")' do
48 | let(:value) { '#12aB0f ' }
49 |
50 | it { is_expected.to eq '12ab0f' }
51 | end
52 |
53 | context 'with an RGB value (ex. "RGB(192, 0, 255)")' do
54 | let(:value) { 'RGB(192, 0, 255)' }
55 |
56 | it { is_expected.to eq 'c000ff' }
57 | end
58 |
59 | context 'with a color name (ex. "rebeccapurple")' do
60 | let(:value) { ' rebeccapurple ' }
61 |
62 | it { is_expected.to eq '663399' }
63 | end
64 | end
65 |
66 | describe '.convert_float' do
67 | subject(:convert_float) { described_class.convert_float(value) }
68 |
69 | context 'with a nil value' do
70 | let(:value) { nil }
71 |
72 | it { is_expected.to eq 0.0 }
73 | end
74 |
75 | context 'with a blank string value' do
76 | let(:value) { '' }
77 |
78 | it { is_expected.to eq 0.0 }
79 | end
80 |
81 | context 'with a decimal value (ex. "10.12345")' do
82 | let(:value) { '10.12345' }
83 |
84 | it { is_expected.to eq 10.1235 }
85 | end
86 |
87 | context 'with an integer value (ex. "12345")' do
88 | let(:value) { '12345' }
89 |
90 | it { is_expected.to eq 12_345.0 }
91 | end
92 | end
93 |
94 | describe '.convert_size' do
95 | subject(:convert_size) { described_class.convert_size(value) }
96 |
97 | context 'with a nil value' do
98 | let(:value) { nil }
99 |
100 | it { is_expected.to eq 0.0 }
101 | end
102 |
103 | context 'with a blank string value' do
104 | let(:value) { '' }
105 |
106 | it { is_expected.to eq 0.0 }
107 | end
108 |
109 | context 'with a pixel value (ex. "10.125")' do
110 | let(:value) { '10.125' }
111 |
112 | it { is_expected.to eq 6.075 }
113 | end
114 |
115 | context 'with a percentage value and a container size (ex. "50%" and 100.242424)' do
116 | subject(:convert_size) { described_class.convert_size(value, options: 100.242424) }
117 |
118 | let(:value) { '50%' }
119 |
120 | it { is_expected.to eq 50.1212 }
121 | end
122 | end
123 |
124 | describe '.convert_symbol' do
125 | subject(:convert_symbol) { described_class.convert_symbol(value) }
126 |
127 | context 'with a nil value' do
128 | let(:value) { nil }
129 |
130 | it { is_expected.to be_nil }
131 | end
132 |
133 | context 'with a blank string value' do
134 | let(:value) { '' }
135 |
136 | it { is_expected.to be_nil }
137 | end
138 |
139 | context 'with a string value (ex. "some_string")' do
140 | let(:value) { 'some_string' }
141 |
142 | it { is_expected.to eq :some_string }
143 | end
144 | end
145 |
146 | describe '.copy_value' do
147 | subject(:copy_value) { described_class.copy_value(value) }
148 |
149 | context 'with any value (ex. "some_string")' do
150 | let(:value) { 'some_string' }
151 |
152 | it { is_expected.to eq 'some_string' }
153 | end
154 | end
155 |
156 | describe '.filter_font_family' do
157 | subject(:filter_font_family) { described_class.filter_font_family(value) }
158 |
159 | context 'with any string (ex. "some_string")' do
160 | let(:value) { 'some_string' }
161 |
162 | it { is_expected.to eq 'some_string' }
163 | end
164 |
165 | context 'with a string with some spaces (ex. " some_string ")' do
166 | let(:value) { ' some_string ' }
167 |
168 | it { is_expected.to eq 'some_string' }
169 | end
170 |
171 | context 'with "inherit" value' do
172 | let(:value) { 'inherit' }
173 |
174 | it { is_expected.to be_nil }
175 | end
176 | end
177 |
178 | describe '.normalize_style' do
179 | subject(:normalize_style) { described_class.normalize_style(value, accepted_values) }
180 |
181 | let(:accepted_values) { [:bold, :italic] }
182 |
183 | context 'with an invalid value (ex. "some_string")' do
184 | let(:value) { 'some_string' }
185 |
186 | it { is_expected.to be_nil }
187 | end
188 |
189 | context 'with a valid value (ex. " bOlD ")' do
190 | let(:value) { ' bOlD ' }
191 |
192 | it { is_expected.to eq :bold }
193 | end
194 | end
195 |
196 | describe '.unquote' do
197 | subject(:unquote) { described_class.unquote(value) }
198 |
199 | context 'with a nil value' do
200 | let(:value) { nil }
201 |
202 | it { is_expected.to eq '' }
203 | end
204 |
205 | context 'with a blank string value' do
206 | let(:value) { '' }
207 |
208 | it { is_expected.to eq '' }
209 | end
210 |
211 | context 'with a string with some spaces (ex. " a string ")' do
212 | let(:value) { ' a string ' }
213 |
214 | it { is_expected.to eq 'a string' }
215 | end
216 |
217 | context 'with a string with some single quotes (ex. " \' a \' string \' ")' do
218 | let(:value) { " ' a ' string ' " }
219 |
220 | it { is_expected.to eq " a ' string " }
221 | end
222 |
223 | context 'with a string with some double quotes (ex. " \" a \" string \" ")' do
224 | let(:value) { ' " a " string " ' }
225 |
226 | it { is_expected.to eq ' a " string ' }
227 | end
228 | end
229 | end
230 |
--------------------------------------------------------------------------------
/spec/units/prawn_html_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe PrawnHtml do
4 | describe '.append_html' do
5 | subject(:append_html) { described_class.append_html(pdf, html) }
6 |
7 | let(:pdf) { instance_double(Prawn::Document) }
8 | let(:html) { 'some html
' }
9 | let(:pdf_wrapper) { instance_double(PrawnHtml::PdfWrapper) }
10 | let(:html_parser) { instance_double(PrawnHtml::HtmlParser, process: true) }
11 | let(:renderer) { instance_double(PrawnHtml::DocumentRenderer) }
12 |
13 | before do
14 | allow(PrawnHtml::PdfWrapper).to receive(:new).and_return(pdf_wrapper)
15 | allow(PrawnHtml::DocumentRenderer).to receive(:new).and_return(renderer)
16 | allow(PrawnHtml::HtmlParser).to receive(:new).and_return(html_parser)
17 | end
18 |
19 | it 'creates an instance of PrawnHtml::HtmlParser and call process', :aggregate_failures do
20 | append_html
21 | expect(PrawnHtml::PdfWrapper).to have_received(:new).with(pdf)
22 | expect(PrawnHtml::DocumentRenderer).to have_received(:new).with(pdf_wrapper)
23 | expect(PrawnHtml::HtmlParser).to have_received(:new).with(renderer)
24 | expect(html_parser).to have_received(:process).with(html)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/units/styles_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe 'Styles' do
4 | let(:pdf) { instance_double(PrawnHtml::PdfWrapper, advance_cursor: true, puts: true) }
5 |
6 | before do
7 | allow(pdf).to receive_messages(page_width: 540, page_height: 720)
8 | allow(PrawnHtml::PdfWrapper).to receive(:new).and_return(pdf)
9 | pdf_document = Prawn::Document.new(page_size: 'A4', page_layout: :portrait)
10 | PrawnHtml.append_html(pdf_document, html)
11 | end
12 |
13 | describe 'attribute color' do
14 | let(:html) { 'Some content...
' }
15 |
16 | let(:expected_buffer) { [{ color: 'ffbb11', size: TestUtils.default_font_size, text: "Some content..." }] }
17 | let(:expected_options) { { leading: TestUtils.adjust_leading } }
18 | let(:expected_extra) { { bounding_box: nil, left_indent: 0 } }
19 |
20 | it 'sends the expected buffer elements to Prawn pdf' do
21 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
22 | end
23 | end
24 |
25 | describe 'attribute font-family' do
26 | let(:html) { 'Some content...
' }
27 |
28 | let(:expected_buffer) { [{ font: 'Courier', size: TestUtils.default_font_size, text: "Some content..." }] }
29 | let(:expected_options) { { leading: TestUtils.adjust_leading(TestUtils.default_font_size, 'Courier') } }
30 | let(:expected_extra) { { bounding_box: nil, left_indent: 0 } }
31 |
32 | it 'sends the expected buffer elements to Prawn pdf' do
33 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
34 | end
35 | end
36 |
37 | describe 'attribute font-size' do
38 | let(:html) { 'Some content...
' }
39 | let(:size) { PrawnHtml::Utils.convert_size('20px') }
40 |
41 | let(:expected_buffer) { [{ size: size, text: "Some content..." }] }
42 | let(:expected_options) { { leading: TestUtils.adjust_leading(20 * PrawnHtml::PX) } }
43 | let(:expected_extra) { { bounding_box: nil, left_indent: 0 } }
44 |
45 | it 'sends the expected buffer elements to Prawn pdf' do
46 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
47 | end
48 | end
49 |
50 | describe 'attribute font-style' do
51 | let(:html) { 'Some content...
' }
52 |
53 | let(:expected_buffer) { [{ size: TestUtils.default_font_size, styles: [:italic], text: "Some content..." }] }
54 | let(:expected_options) { { leading: TestUtils.adjust_leading } }
55 | let(:expected_extra) { { bounding_box: nil, left_indent: 0 } }
56 |
57 | it 'sends the expected buffer elements to Prawn pdf' do
58 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
59 | end
60 | end
61 |
62 | describe 'attribute font-weight' do
63 | let(:html) { 'Some content...
' }
64 |
65 | let(:expected_buffer) { [{ size: TestUtils.default_font_size, styles: [:bold], text: "Some content..." }] }
66 | let(:expected_options) { { leading: TestUtils.adjust_leading } }
67 | let(:expected_extra) { { bounding_box: nil, left_indent: 0 } }
68 |
69 | it 'sends the expected buffer elements to Prawn pdf' do
70 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
71 | end
72 | end
73 |
74 | describe 'attribute letter-spacing' do
75 | let(:html) { 'aaa
bbb ccc
' }
76 | let(:size) { TestUtils.default_font_size }
77 |
78 | let(:expected_options) { { leading: TestUtils.adjust_leading } }
79 | let(:expected_extra) { { bounding_box: nil, left_indent: 0 } }
80 |
81 | it 'sends the expected buffer elements to Prawn pdf', :aggregate_failures do
82 | expect(pdf).to have_received(:puts).with(
83 | [{ character_spacing: 1.5, size: size, text: 'aaa' }], expected_options, expected_extra
84 | )
85 | expect(pdf).to have_received(:puts).with([{ size: size, text: ' bbb ' }], expected_options, expected_extra)
86 | expect(pdf).to have_received(:puts).with(
87 | [{ character_spacing: 2.0, size: size, text: 'ccc' }], expected_options, expected_extra
88 | )
89 | end
90 | end
91 |
92 | describe 'attribute line-height' do
93 | let(:html) { 'Some content...
' }
94 |
95 | it 'sends the expected buffer elements to Prawn pdf' do
96 | expect(pdf).to have_received(:puts).with(
97 | [{ size: TestUtils.default_font_size, text: "Some content..." }],
98 | { leading: (12 * PrawnHtml::PX).round(5) },
99 | { bounding_box: nil, left_indent: 0 }
100 | )
101 | end
102 | end
103 |
104 | describe 'attribute margin-left' do
105 | let(:html) { 'Some content...
' }
106 |
107 | let(:expected_buffer) { [{ size: TestUtils.default_font_size, text: "Some content..." }] }
108 | let(:expected_options) { { leading: TestUtils.adjust_leading } }
109 | let(:expected_extra) { { bounding_box: nil, left_indent: 40 * PrawnHtml::PX } }
110 |
111 | it 'sends the expected buffer elements to Prawn pdf' do
112 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
113 | end
114 | end
115 |
116 | describe 'attribute margin-top' do
117 | let(:html) { 'Some content...
' }
118 |
119 | it 'sends the expected buffer elements to Prawn pdf' do
120 | expect(pdf).to have_received(:puts).with(
121 | [{ size: TestUtils.default_font_size, text: "Some content..." }],
122 | { leading: TestUtils.adjust_leading },
123 | { bounding_box: nil, left_indent: 0 }
124 | )
125 | end
126 | end
127 |
128 | describe 'attribute text-align' do
129 | context 'with some content left aligned' do
130 | let(:html) { 'Some content...
' }
131 |
132 | let(:expected_buffer) { [{ size: TestUtils.default_font_size, text: "Some content..." }] }
133 | let(:expected_options) { { align: :left, leading: TestUtils.adjust_leading } }
134 | let(:expected_extra) { { bounding_box: nil, left_indent: 0 } }
135 |
136 | it 'sends the expected buffer elements to Prawn pdf' do
137 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
138 | end
139 | end
140 |
141 | context 'with some content center aligned' do
142 | let(:html) { 'Some content...
' }
143 |
144 | let(:expected_buffer) { [{ size: TestUtils.default_font_size, text: "Some content..." }] }
145 | let(:expected_options) { { align: :center, leading: TestUtils.adjust_leading } }
146 | let(:expected_extra) { { bounding_box: nil, left_indent: 0 } }
147 |
148 | it 'sends the expected buffer elements to Prawn pdf' do
149 | expect(pdf).to have_received(:puts).with(expected_buffer, expected_options, expected_extra)
150 | end
151 | end
152 | end
153 | end
154 |
--------------------------------------------------------------------------------