# frozen_string_literal: true
require_relative 'spec_helper'
describe Asciidoctor::PDF::Converter do
describe '.register_for' do
it 'should self register to handle pdf backend' do
registered = Asciidoctor::Converter.for 'pdf'
(expect registered).to be described_class
end
it 'should convert AsciiDoc string to PDF object when backend is pdf' do
(expect Asciidoctor.convert 'hello', backend: 'pdf').to be_a Prawn::Document
end
it 'should convert AsciiDoc file to PDF file when backend is pdf' do
pdf = to_pdf Pathname.new fixture_file 'hello.adoc'
(expect Pathname.new output_file 'hello.pdf').to exist
(expect pdf.page_count).to be > 0
end
end
describe '#convert' do
it 'should not fail to convert empty string' do
(expect to_pdf '').not_to be_nil
end
it 'should not fail to convert empty file' do
pdf = to_pdf Pathname.new fixture_file 'empty.adoc'
(expect Pathname.new output_file 'empty.pdf').to exist
(expect pdf.page_count).to be > 0
end
it 'should convert file to target dir in secure mode' do
input_file = fixture_file 'secure.adoc'
target_file = output_file 'secure.pdf'
doc = Asciidoctor.convert_file input_file, backend: 'pdf', to_dir: output_dir, safe: 'secure'
(expect doc.attr 'outfile').to be_nil
pdf = PDF::Reader.new target_file
(expect pdf.pages).to have_size 2
(expect pdf.pages[0].text).to include 'Book Title'
(expect pdf.pages[1].text).to include 'Chapter'
images = get_images pdf
(expect images).to have_size 1
end
it 'should convert file to target file in secure mode' do
input_file = fixture_file 'secure.adoc'
target_file = output_file 'secure-alt.pdf'
Asciidoctor.convert_file input_file, backend: 'pdf', to_file: target_file, safe: 'secure'
(expect Pathname.new target_file).to exist
pdf = PDF::Reader.new target_file
(expect pdf.pages).to have_size 2
(expect pdf.pages[0].text).to include 'Book Title'
(expect pdf.pages[1].text).to include 'Chapter'
images = get_images pdf
(expect images).to have_size 1
end
it 'should be able to load, convert, and write in separate steps' do
input_file = fixture_file 'hello.adoc'
target_file = output_file 'hello.pdf'
doc = Asciidoctor.load_file input_file, backend: 'pdf'
doc.write doc.convert, target_file
(expect Pathname.new target_file).to exist
pdf = PDF::Reader.new target_file
(expect pdf.pages).to have_size 1
end
it 'should be able to reuse instance of converter' do
input_file = fixture_file 'book.adoc'
doc = Asciidoctor.load_file input_file, backend: 'pdf', safe: :safe, attributes: { 'reproducible' => '' }
converter = doc.converter
pdf1 = doc.convert.render
doc = Asciidoctor.load_file input_file, backend: 'pdf', safe: :safe, attributes: { 'reproducible' => '' }, converter: converter
pdf2 = doc.convert.render
(expect pdf1).to eql pdf2
end
it 'should warn if convert method is not found for node' do
(expect do
doc = Asciidoctor.load <<~'END', backend: 'pdf', safe: :safe, attributes: { 'nofooter' => '' }
before
1,2,3
after
END
doc.blocks[1].context = :chart
pdf_stream = StringIO.new
doc.write doc.convert, pdf_stream
pdf = PDF::Reader.new pdf_stream
pages = pdf.pages
(expect pages).to have_size 1
lines = pdf.pages[0].text.lines.map(&:rstrip).reject(&:empty?)
(expect lines).to eql %w(before after)
end).to log_message severity: :WARN, message: 'missing convert handler for chart node in pdf backend'
end
it 'should not warn if convert method is not found for node in scratch document' do
(expect do
doc = Asciidoctor.load <<~'END', backend: 'pdf', safe: :safe, attributes: { 'nofooter' => '' }
before
[%unbreakable]
--
1,2,3
--
after
END
doc.blocks[1].blocks[0].context = :chart
pdf_stream = StringIO.new
doc.write doc.convert, pdf_stream
pdf = PDF::Reader.new pdf_stream
pages = pdf.pages
(expect pages).to have_size 1
lines = pdf.pages[0].text.lines.map(&:rstrip).reject(&:empty?)
(expect lines).to eql %w(before after)
end).to log_message severity: :WARN, message: 'missing convert handler for chart node in pdf backend'
end
it 'should ensure data-uri attribute is set' do
doc = Asciidoctor.load <<~'END', backend: 'pdf', base_dir: fixtures_dir, safe: :safe
image::logo.png[]
END
(expect doc.attr? 'data-uri').to be true
doc.convert
(expect doc.attr? 'data-uri').to be true
end
it 'should ignore data-uri attribute entry in document' do
doc = Asciidoctor.load <<~'END', backend: 'pdf', base_dir: fixtures_dir, safe: :safe
:!data-uri:
image::logo.png[]
END
(expect doc.attr? 'data-uri').to be true
doc.convert
(expect doc.attr? 'data-uri').to be true
end
it 'should not fail to remove tmp files if already removed' do
image_data = File.read (fixture_file 'square.jpg'), mode: 'r:UTF-8'
encoded_image_data = Base64.strict_encode64 image_data
doc = Asciidoctor.load <<~END, backend: 'pdf'
:page-background-image: image:data:image/png;base64,#{encoded_image_data}[Square,fit=cover]
END
pdf_doc = doc.convert
tmp_files = (converter = doc.converter).instance_variable_get :@tmp_files
(expect tmp_files).to have_size 1
tmp_files.each {|_, path| converter.send :unlink_tmp_file, path }
doc.write pdf_doc, (pdf_io = StringIO.new)
pdf = PDF::Reader.new pdf_io
(expect get_images pdf).to have_size 1
end
it 'should not fail to remove tmp files if they are not writable' do
(expect do
image_data = File.read (fixture_file 'square.jpg'), mode: 'r:UTF-8'
encoded_image_data = Base64.strict_encode64 image_data
doc = Asciidoctor.load <<~END, backend: 'pdf'
:page-background-image: image:data:image/png;base64,#{encoded_image_data}[Square,fit=cover]
END
pdf_doc = doc.convert
tmp_files = doc.converter.instance_variable_get :@tmp_files
(expect tmp_files).to have_size 1
tmp_file_paths = tmp_files.map do |_, path|
FileUtils.mv path, (tmp_path = %(#{path}-tmp))
Dir.mkdir path
FileUtils.mv tmp_path, (File.join path, (File.basename path))
path
end
doc.write pdf_doc, (pdf_io = StringIO.new)
pdf = PDF::Reader.new pdf_io
(expect get_images pdf).to have_size 1
tmp_file_paths.each {|path| (Pathname.new path).rmtree secure: true }
end).to log_message severity: :WARN, message: '~could not delete temporary file'
end
it 'should keep tmp files if KEEP_ARTIFACTS environment variable is set' do
image_data = File.read (fixture_file 'square.jpg'), mode: 'r:UTF-8'
encoded_image_data = Base64.strict_encode64 image_data
doc = Asciidoctor.load <<~END, backend: 'pdf'
:page-background-image: image:data:image/png;base64,#{encoded_image_data}[Square,fit=cover]
END
pdf_doc = doc.convert
tmp_files = doc.converter.instance_variable_get :@tmp_files
(expect tmp_files).to have_size 1
ENV['KEEP_ARTIFACTS'] = 'true'
doc.write pdf_doc, (pdf_io = StringIO.new)
ENV.delete 'KEEP_ARTIFACTS'
pdf = PDF::Reader.new pdf_io
(expect get_images pdf).to have_size 1
(expect tmp_files).to have_size 1
tmp_files.each do |_, path|
(expect Pathname.new path).to exist
File.unlink path
end
end
context 'theme' do
it 'should apply the theme at the path specified by pdf-theme' do
with_pdf_theme_file <<~'END' do |theme_path|
base:
font-color: ff0000
END
pdf = to_pdf <<~END, analyze: true
= Document Title
:pdf-theme: #{theme_path}
red text
END
(expect pdf.find_text font_color: 'FF0000').to have_size pdf.text.size
end
end
it 'should only load theme from pdf-themesdir if pdf-theme attribute specified' do
[nil, 'default'].each do |theme|
to_pdf_opts = { analyze: true }
to_pdf_opts[:attribute_overrides] = { 'pdf-theme' => theme } if theme
pdf = to_pdf <<~END, to_pdf_opts
= Document Title
:pdf-themesdir: #{fixtures_dir}
body text
END
expected_font_color = theme ? 'AA0000' : '333333'
body_text = (pdf.find_text 'body text')[0]
(expect body_text).not_to be_nil
(expect body_text[:font_color]).to eql expected_font_color
end
end
it 'should apply the named theme specified by pdf-theme located in the specified pdf-themesdir' do
with_pdf_theme_file <<~'END' do |theme_path|
base:
font-color: ff0000
END
pdf = to_pdf <<~END, analyze: true
= Document Title
:pdf-theme: #{File.basename theme_path, '-theme.yml'}
:pdf-themesdir: #{File.dirname theme_path}
red text
END
(expect pdf.find_text font_color: 'FF0000').to have_size pdf.text.size
end
end
it 'should resolve pdf-themesdir relative to the current working directory' do
input_file = Pathname.new fixture_file 'hello-with-custom-theme.adoc'
relative_themesdir = fixture_file '.', relative: true
pdf = to_pdf input_file, analyze: true, attribute_overrides: { 'pdf-themesdir' => relative_themesdir }
(expect pdf.find_text font_name: 'Times-Roman').to have_size pdf.text.size
end
it 'should replace {docdir} token in value of pdf-themesdir' do
input_file = Pathname.new fixture_file 'hello-with-custom-theme.adoc'
pdf = to_pdf input_file, analyze: true, attribute_overrides: { 'pdf-themesdir' => '{docdir}' }
(expect pdf.find_text font_name: 'Times-Roman').to have_size pdf.text.size
end
it 'should resolve theme at root of classloader when pdf-themesdir is uri:classloader:/', if: RUBY_ENGINE == 'jruby' do
require fixture_file 'pdf-themes.jar'
pdf = to_pdf <<~'END', attribute_overrides: { 'pdf-themesdir' => 'uri:classloader:/', 'pdf-theme' => 'custom' }, analyze: true
hi there
END
text = pdf.find_unique_text 'hi there'
(expect text[:font_color]).to eql '0000FF'
end
it 'should resolve theme from folder in classloader when pdf-themesdir starts with uri:classloader:', if: RUBY_ENGINE == 'jruby' do
require fixture_file 'pdf-themes.jar'
pdf = to_pdf <<~'END', attribute_overrides: { 'pdf-themesdir' => 'uri:classloader:/pdf-themes', 'pdf-theme' => 'another-custom' }, analyze: true
hi there
END
text = pdf.find_unique_text 'hi there'
(expect text[:font_color]).to eql 'FF0000'
end
it 'should set text color to black when default-for-print theme is specified' do
pdf = to_pdf <<~END, analyze: true
= Document Title
:pdf-theme: default-for-print
black `text`
> loud quote
END
(expect pdf.find_text font_color: '000000').to have_size pdf.text.size
end
it 'should set font family to Noto Sans when default-sans themme is specified' do
pdf = to_pdf <<~END, analyze: true
= Document Title
:pdf-theme: default-sans
We don't like those _pesky_ serifs in these here parts.
END
text = pdf.text
sans_text = text.select {|it| it[:font_name].start_with? 'NotoSans' }
(expect sans_text).to have_size text.size
end
it 'should use theme passed in through :pdf_theme option' do
theme = Asciidoctor::PDF::ThemeLoader.load_theme 'custom', fixtures_dir
theme.base_font_size = 14
theme.base_font_color = '1a1a1a'
pdf = Asciidoctor.convert 'content', backend: 'pdf', pdf_theme: theme
converter_theme = pdf.instance_variable_get :@theme
(expect converter_theme.base_font_size).to eql theme.base_font_size
(expect converter_theme.base_font_color).to eql theme.base_font_color
end
it 'should set themesdir theme with __dir__ is passed via :pdf_theme option' do
theme = Asciidoctor::PDF::ThemeLoader.load_base_theme
theme.delete_field :__dir__
pdf = Asciidoctor.convert 'content', backend: 'pdf', pdf_theme: theme
(expect pdf.instance_variable_get :@themesdir).to eql Dir.pwd
end
it 'should log error if built-in theme cannot be found or loaded' do
(expect do
Asciidoctor.convert 'foo', backend: 'pdf', attributes: { 'pdf-theme' => 'foo' }
end).to log_message severity: :ERROR, message: /could not locate or load the built-in pdf theme `foo' because of .*?; reverting to default theme/
end
it 'should log error if user theme cannot be found or loaded' do
(expect do
Asciidoctor.convert 'foo', backend: 'pdf', attributes: { 'pdf-theme' => 'foo', 'pdf-themesdir' => fixtures_dir }
end).to log_message severity: :ERROR, message: /could not locate or load the pdf theme `foo' in #{Regexp.escape fixtures_dir} because of .*?; reverting to default theme/
end
it 'should log error with filename and reason if theme file cannot be parsed' do
pdf_theme = fixture_file 'tab-indentation-theme.yml'
(expect do
pdf = to_pdf 'content', attribute_overrides: { 'pdf-theme' => pdf_theme }, analyze: true
(expect pdf.pages).to have_size 1
end).to log_message severity: :ERROR, message: /because of Psych::SyntaxError \(#{Regexp.escape pdf_theme}\): found character .*that cannot start any token.*; reverting to default theme/
end
it 'should log error with filename and reason if exception is thrown during theme compilation' do
(expect do
pdf = to_pdf 'content', attribute_overrides: { 'pdf-theme' => (fixture_file 'invalid-theme.yml') }, analyze: true
(expect pdf.pages).to have_size 1
end).to log_message severity: :ERROR, message: /because of NoMethodError undefined method `start_with\?' for 10:(Fixnum|Integer); reverting to default theme/
end
it 'should not crash if theme does not specify any keys' do
pdf = to_pdf <<~'END', attribute_overrides: { 'pdf-theme' => (fixture_file 'bare-theme.yml') }, analyze: true
= Document Title
:doctype: book
This is the stark theme.
== Chapter Title
=== Section Title
.dlist
term:: desc
.ulist
* one
* two
----
key=val <1>
----
<1> A variable assignment
NOTE: That's all, folks!
END
(expect pdf.pages).to have_size 3
(expect pdf.find_text font_name: 'Helvetica', font_size: 12).not_to be_empty
(expect (pdf.find_text 'Document Title')[0]).not_to be_nil
(expect (pdf.find_text 'Chapter Title')[0]).not_to be_nil
(expect (pdf.find_text 'Section Title')[0]).not_to be_nil
(expect (pdf.find_text 'ulist')[0]).not_to be_nil
(expect (pdf.find_text 'one')[0]).not_to be_nil
end
it 'should not crash if theme does not specify any keys when converting chronicles example' do
input_path = Pathname.new example_file 'chronicles-example.adoc'
pdf = to_pdf input_path, attribute_overrides: { 'imagesdir' => '@', 'pdf-theme' => (fixture_file 'bare-theme.yml'), 'source-highlighter' => nil }
(expect pdf.pages).to have_size 14
(expect (pdf.page 1).text).to include 'Documentation Chronicles'
end
it 'should not warn when using dark theme to convert chronicles example' do
input_path = Pathname.new example_file 'chronicles-example.adoc'
pdf = to_pdf input_path, attribute_overrides: { 'imagesdir' => '@', 'pdf-theme' => 'chronicles-dark', 'source-highlighter' => nil }, analyze: true
(expect pdf.pages).to have_size 17
gs_p1 = pdf.pages[0][:raw_content]
(expect gs_p1).to start_with %(q\n/DeviceRGB cs\n0.0 0.0 0.0 scn\n0.0 0.0 595.28 841.89 re\n)
doctitle = pdf.find_text page_number: 1, string: 'Documentation Chronicles'
(expect doctitle).to have_size 1
(expect doctitle[0][:font_color]).to eql '666666'
p4_text = pdf.find_text page_number: 4
heading_text = p4_text[0]
paragraph_text = p4_text[1]
link_text = p4_text[2]
(expect heading_text[:font_color]).to eql 'CCCCCC'
(expect paragraph_text[:font_color]).to eql 'CCCCCC'
(expect link_text[:font_color]).to eql 'BD7435'
end
it 'should allow all border colors to be set using base-border-color when extending base theme' do
[
%(****\ncontent\n****),
%(====\ncontent\n====),
%([cols=2*]\n|===\n|a|b\n|c|d\n|===),
%(____\ncontent\n____),
%([verse]\n____\ncontent\n____),
%(----\ncontent\n----),
'---',
'NOTE: content',
].each do |input|
pdf = to_pdf input, pdf_theme: { extends: 'base', base_border_color: '0000EE' }, analyze: :line
(expect pdf.lines.map {|it| it[:color] }.uniq).to eql %w(0000EE)
end
end
it 'should convert background position to options' do
converter = Asciidoctor::Converter.create 'pdf'
{
'center' => { position: :center, vposition: :center },
'top' => { position: :center, vposition: :top },
'bottom' => { position: :center, vposition: :bottom },
'left' => { position: :left, vposition: :center },
'right' => { position: :right, vposition: :center },
'top left' => { position: :left, vposition: :top },
'right top' => { position: :right, vposition: :top },
'bottom left' => { position: :left, vposition: :bottom },
'right bottom' => { position: :right, vposition: :bottom },
'center right' => { position: :right, vposition: :center },
'left center' => { position: :left, vposition: :center },
'center center' => { position: :center, vposition: :center },
'bogus' => nil,
'bogus bogus' => nil,
}.each do |value, expected|
(expect converter.send :resolve_background_position, value, nil).to eql expected
end
end
it 'should expose theme as property on converter' do
doc = Asciidoctor.load 'yo', backend: :pdf
doc.convert
(expect doc.converter.theme).not_to be_nil
(expect doc.converter.theme.base_font_family).to eql 'Noto Serif'
end
end
end
describe 'helpers' do
it 'should not drop lines with unresolved attributes when apply_subs_discretely is called without options' do
input = <<~'END'
foo
{undefined}
bar
END
doc = Asciidoctor.load 'yo', backend: :pdf
converter = doc.converter
converter.load_theme doc
result = converter.apply_subs_discretely doc, input
(expect result).to eql input
end
it 'should raise exception if an unsupported unit of measurement is passed to to_pt' do
(expect do
converter = (Asciidoctor.load 'yo', backend: :pdf).converter
converter.to_pt 3, 'ft'
end).to raise_exception ArgumentError, /unknown unit of measurement: ft/
end
it 'should return previous integer as string when pred is invoked on integer string' do
{
'1' => '0',
'10' => '9',
'0' => '-1',
'-9' => '-10',
}.each do |curr, pred|
(expect curr.pred).to eql pred
end
end
it 'should not delegate to formatter when parse_text is called without options' do
doc = Asciidoctor.load 'text', backend: :pdf
converter = doc.converter
converter.init_pdf doc
result = converter.parse_text 'text'
(expect result).to eql [text: 'text']
end
it 'should not delegate to formatter with default options when parse_text is called with inline_format: true' do
doc = Asciidoctor.load 'text', backend: :pdf
converter = doc.converter
converter.init_pdf doc
result = converter.parse_text %(foo\nbar), inline_format: true
(expect result).to eql [{ text: %(foo\n) }, { text: 'bar', styles: [:bold].to_set }]
end
it 'should not delegate to formatter with specified options when parse_text is called with inline_format: Array' do
doc = Asciidoctor.load 'text', backend: :pdf
converter = doc.converter
converter.init_pdf doc
result = converter.parse_text %(foo\nbar), inline_format: [normalize: true]
(expect result).to eql [{ text: 'foo ' }, { text: 'bar', styles: [:bold].to_set }]
end
it 'should restore current column after float yields to current block' do
doc = Asciidoctor.load 'text', backend: :pdf
converter = doc.converter
actual_column = nil
last_visited_column = nil
converter.instance_exec do
init_pdf doc
start_new_page
column_box [bounds.left, cursor], width: bounds.width, columns: 2 do
float do
ink_prose 'before'
bounds.move_past_bottom
ink_prose 'after'
last_visited_column = bounds.instance_variable_get :@current_column
end
actual_column = bounds.instance_variable_get :@current_column
end
end
(expect actual_column).to eql 0
(expect last_visited_column).to eql 1
end
it 'should short-circuit formatted_text and log error if text cannot not fit on new page' do
doc = Asciidoctor.load 'text', backend: :pdf
last_page = last_page_number = nil
(expect do
doc.converter.instance_exec do
init_pdf doc
start_new_page
ink_prose 'before'
formatted_text [{ text: 'x', ascender: bounds.height, descender: font.descender }]
last_page = page
last_page_number = page_number
end
end).to log_message severity: :ERROR, message: 'cannot fit formatted text on page: x'
(expect last_page).to be_empty
(expect last_page_number).to eql 2
end
end
describe '#next_enclosed_block' do
let(:doc) { Asciidoctor.load input_source, backend: 'pdf' }
let(:converter) { doc.converter }
let(:result) { converter.next_enclosed_block start }
context 'of document' do
let(:input_source) { 'paragraph' }
let(:start) { doc }
it('should be nil') { (expect result).to be_nil }
end
context 'of last block in document' do
let(:input_source) { 'paragraph' }
let(:start) { doc.blocks[0] }
it('should be nil') { (expect result).to be_nil }
end
context 'of block followed by block' do
let :input_source do
<<~'END'
first paragraph
second paragraph
END
end
let(:start) { doc.blocks[0] }
it('should be next block') { (expect result).to eql doc.blocks[1] }
end
context 'of last block in open block followed by block' do
let :input_source do
<<~'END'
first paragraph
--
second paragraph
--
third paragraph
END
end
let(:start) { (doc.find_by context: :paragraph)[1] }
it('should be next block adjacent to open block') { (expect result).to eql doc.blocks[2] }
end
context 'of last block before parent section' do
let :input_source do
<<~'END'
== First Section
paragraph
== Second Section
END
end
let(:start) { (doc.find_by context: :paragraph)[0] }
it('should be next section') { (expect result).to eql doc.sections[1] }
end
context 'of last block before subsection' do
let :input_source do
<<~'END'
== Section
paragraph
=== Subsection
END
end
let(:start) { (doc.find_by context: :paragraph)[0] }
it('should be next section') { (expect result).to eql (doc.find_by context: :section)[-1] }
end
context 'of last block before grandparent section' do
let :input_source do
<<~'END'
== First Section
paragraph
=== Subsection
paragraph
== Last Section
END
end
let(:start) { (doc.find_by context: :paragraph)[-1] }
it('should be next section') { (expect result).to eql (doc.find_by context: :section)[-1] }
end
context 'of preamble' do
let :input_source do
<<~'END'
= Document Title
preamble
== First Section
END
end
let(:start) { (doc.find_by context: :preamble)[0] }
it('should be first section') { (expect result).to eql doc.sections[0] }
end
context 'of abstract' do
let :input_source do
<<~'END'
= Document Title
[abstract]
--
abstract
--
== First Section
END
end
let(:start) { (doc.find_by context: :open, style: 'abstract')[0] }
it('should be nil') { (expect result).to be_nil }
end
context 'of abstract followed by more preamble' do
let :input_source do
<<~'END'
= Document Title
[abstract]
--
abstract
--
more preamble
== First Section
END
end
let(:start) { (doc.find_by context: :open, style: 'abstract')[0] }
it('should be next block in preamble') { (expect result).to eql (doc.find_by context: :paragraph)[1] }
end
context 'of last block in abstract' do
let :input_source do
<<~'END'
= Document Title
[abstract]
--
abstract
--
== First Section
END
end
let(:start) { (doc.find_by context: :paragraph)[0] }
it('should be nil') { (expect result).to be_nil }
end
context 'of last block inside abstract followed by more preamble' do
let :input_source do
<<~'END'
= Document Title
[abstract]
--
abstract
--
more preamble
== First Section
END
end
let(:start) { (doc.find_by context: :paragraph)[0] }
it('should be nil') { (expect result).to be_nil }
end
context 'of last block inside delimited block' do
let :input_source do
<<~'END'
****
inside paragraph
****
outside paragraph
END
end
let(:start) { (doc.find_by context: :paragraph)[0] }
it('should be nil') { (expect result).to be_nil }
end
context 'of list followed by block' do
let :input_source do
<<~'END'
* list item
paragraph
END
end
let(:start) { doc.blocks[0] }
it('should be next block adjacent to list') { (expect result).to eql doc.blocks[1] }
end
context 'of first list item in list' do
let :input_source do
<<~'END'
* yin
* yang
END
end
let(:start) { doc.blocks[0].items[0] }
it('should be next list item') { (expect result).to eql doc.blocks[0].items[1] }
end
context 'of last list item in list' do
let :input_source do
<<~'END'
* yin
* yang
paragraph
END
end
let(:start) { doc.blocks[0].items[-1] }
it('should be nil') { (expect result).to be_nil }
end
context 'of last attached block in first item in list' do
let :input_source do
<<~'END'
* moon
+
stars
* sun
END
end
let(:start) { (doc.find_by context: :paragraph)[0] }
it('should be next list item') { (expect result).to eql doc.blocks[0].items[-1] }
end
context 'of last attached block in last item in list' do
let :input_source do
<<~'END'
* sun
* moon
+
stars
paragraph
END
end
let(:start) { (doc.find_by context: :paragraph)[0] }
it('should be nil') { (expect result).to be_nil }
end
context 'of last block in open block attached to first item in list' do
let :input_source do
<<~'END'
* moon
+
--
light side
dark side
--
* sun
END
end
let(:start) { (doc.find_by context: :paragraph)[1] }
it('should be next list item') { (expect result).to eql doc.blocks[0].items[-1] }
end
context 'of last item in nested list of first item in list' do
let :input_source do
<<~'END'
* sun
** star
* moon
END
end
let(:start) { (doc.find_by context: :list_item)[1] }
it('should be next top-level list item') { (expect result).to eql doc.blocks[0].items[-1] }
end
context 'of last item in nested list of last item in list' do
let :input_source do
<<~'END'
* moon
* sun
** star
paragraph
END
end
let(:start) { (doc.find_by context: :list_item)[2] }
it('should be nil') { (expect result).to be_nil }
end
context 'of last item in deeply nested list of first item in list' do
let :input_source do
<<~'END'
* foo
** bar
*** baz
* moon
END
end
let(:start) { (doc.find_by context: :list_item)[2] }
it('should be next top-level list item') { (expect result).to eql doc.blocks[0].items[-1] }
end
context 'of term of first item in dlist' do
let :input_source do
<<~'END'
foo:: bar
END
end
let(:start) { (doc.find_by context: :list_item)[0] }
it('should be desc of current item') { (expect result).to eql (doc.find_by context: :list_item)[1] }
end
context 'of desc text of first item in dlist' do
let :input_source do
<<~'END'
foo:: bar
yin:: yang
END
end
let(:start) { (doc.find_by context: :list_item)[1] }
it('should be term of next item') { (expect result).to eql (doc.find_by context: :list_item)[2] }
end
context 'of desc text of last item in dlist' do
let :input_source do
<<~'END'
foo:: bar
yin:: yang
paragraph
END
end
let(:start) { (doc.find_by context: :list_item)[3] }
it('should be nil') { (expect result).to be_nil }
end
context 'of attached block in last item in dlist' do
let :input_source do
<<~'END'
foo:: bar
sun:: moon
+
stars
paragraph
END
end
let(:start) { (doc.find_by context: :paragraph)[0] }
it('should be nil') { (expect result).to be_nil }
end
context 'of missing block' do
let :input_source do
<<~'END'
foo:: bar
yin:: yang
END
end
let :start do
list_items = (doc.find_by context: :list_item)
list_items[0].parent.items[0].replace [[list_items[0].dup], list_items[1].dup]
list_items[1]
end
it('should be nil') { (expect result).to be_nil }
end
context 'of preamble followed by section' do
let :input_source do
<<~'END'
= Document Title
[abstract]
--
A glimpse at what is to come.
--
== Intro
END
end
let(:start) { (doc.find_by context: :open)[0].parent }
it('should be next section') { (expect result).to eql doc.sections[-1] }
end
context 'of paragraph in abstract followed by section' do
let :input_source do
<<~'END'
= Document Title
[abstract]
--
A glimpse at what is to come.
--
== Intro
END
end
let(:start) { (doc.find_by context: :paragraph)[0] }
it('should be nil') { (expect result).to be_nil }
end
context 'of quote block in abstract followed by section' do
let :input_source do
<<~'END'
= Document Title
[abstract]
--
____
A glimpse at what is to come.
____
--
== Intro
END
end
let(:start) { (doc.find_by context: :quote)[0] }
it('should be nil') { (expect result).to be_nil }
end
context 'of last block in AsciiDoc table cell' do
let :input_source do
<<~'END'
[cols=2*]
|===
a|
foo
bar
____
quote
____
| another cell
|===
after
END
end
let(:start) { (doc.find_by context: :quote, traverse_documents: true)[0] }
it('should be nil') { (expect result).to be_nil }
end
context 'of last block in description of first item in horizontal dlist' do
example_group = self
let :input_source do
<<~'END'
.Title
[horizontal]
term:: desc
+
[quote]
capture:quote[]
another term::
END
end
let :doc do
Asciidoctor.load input_source, backend: 'pdf', extensions: (proc do
inline_macro :capture do
process do |parent, target|
example_group.let(:captured_block) { parent }
create_inline parent, :quoted, target
end
end
end)
end
let :start do
doc.convert
captured_block
end
it('should be next term') { (expect result).to eql (doc.find_by context: :list_item)[-1] }
end
context 'of last block in description of last item in horizontal dlist' do
example_group = self
let :input_source do
<<~'END'
.Title
[horizontal]
term:: desc
+
[quote]
capture:quote[]
after
END
end
let :doc do
Asciidoctor.load input_source, backend: 'pdf', extensions: (proc do
inline_macro :capture do
process do |parent, target|
example_group.let(:captured_block) { parent }
create_inline parent, :quoted, target
end
end
end)
end
let :start do
doc.convert
captured_block
end
it('should be nil') { (expect result).to be_nil }
end
end
describe 'Bounding Box' do
it 'should use correct left value when creating bounding box', visual: true do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def traverse node
return super unless node.context == :document
column_box [0, cursor], columns: 2, width: bounds.width, reflow_margins: true, spacer: 12 do
bounds.move_past_bottom
super
end
end
end
pdf_theme = { caption_background_color: 'EEEEEE' }
input = <<~'END'
= Article Title
* list item
NOTE: admonition
> quote
.Block title
----
code block <1>
----
<1> Callout description
END
(expect to_pdf_file input, 'bounding-box-left.pdf', backend: backend, pdf_theme: pdf_theme).to visually_match 'bounding-box-left.pdf'
end
it 'should not reflow margins on column box if reflow_margins option is not set' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def traverse node
return super unless node.context == :document
column_box [0, cursor], columns: 2, width: bounds.width, spacer: 12 do
super
end
end
end
input = <<~'END'
= Article Title
column 1, page 1
[.column]
<<<
column 2, page 1
[.column]
<<<
column 1, page 2
END
pdf = to_pdf input, backend: backend, analyze: true
(expect (pdf.find_unique_text 'column 1, page 2')[:page_number]).to eql 2
(expect (pdf.find_unique_text 'column 1, page 2')[:y]).to eql (pdf.find_unique_text 'column 1, page 1')[:y]
end
end
describe 'extend' do
it 'should use specified extended converter' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def convert_paragraph node
layout_prose node.content, anchor: 'next-section'
end
end
input = <<~'END'
see next section
[#next-section]
== Next Section
END
pdf = to_pdf input, backend: backend, analyze: true
para_text = pdf.find_unique_text 'see next section'
(expect para_text[:font_color]).to eql '428BCA'
pdf = to_pdf input, backend: backend
(expect get_names pdf).to have_key 'next-section'
annotations = get_annotations pdf, 1
(expect annotations).to have_size 1
link_annotation = annotations[0]
(expect link_annotation[:Subtype]).to be :Link
(expect link_annotation[:Dest]).to eql 'next-section'
end
it 'should allow extended converter to invoke layout_heading without any opts' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def convert_paragraph node
layout_heading %(#{node.role.capitalize} Heading) if node.role?
super
end
end
pdf = to_pdf <<~'END', backend: backend, pdf_theme: { heading_margin_bottom: 0, heading_margin_top: 100 }, analyze: true
[.first]
paragraph
[.second]
paragraph
END
first_heading_text = pdf.find_unique_text 'First Heading'
(expect first_heading_text).not_to be_nil
(expect first_heading_text[:font_size]).to eql 10.5
(expect first_heading_text[:font_color]).to eql '333333'
second_heading_text = pdf.find_unique_text 'Second Heading'
(expect second_heading_text).not_to be_nil
(expect second_heading_text[:font_size]).to eql 10.5
(expect second_heading_text[:font_color]).to eql '333333'
(expect second_heading_text[:y]).to be < 700
text = pdf.text
(expect text[0][:y] - text[1][:y]).to be < text[1][:y] - text[2][:y]
end
it 'should allow custom converter to invoke layout_heading with opts' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def convert_paragraph node
if node.has_role? 'heading'
layout_heading node.source, text_transform: 'uppercase', size: 100, color: 'AA0000', line_height: 1.2, margin: 20
else
super
end
end
end
pdf = to_pdf <<~'END', backend: backend, analyze: true
before
[.heading]
heading
paragraph
END
heading_text = pdf.find_unique_text 'HEADING'
(expect heading_text).not_to be_nil
(expect heading_text[:font_size]).to eql 100
(expect heading_text[:font_color]).to eql 'AA0000'
(expect heading_text[:y].floor).to eql 650
(expect (pdf.find_unique_text 'paragraph')[:y].floor).to eql 588
end
it 'should allow custom converter to invoke layout_general_heading without any opts' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def convert_paragraph node
layout_general_heading node, %(#{node.role.capitalize} Heading) if node.role?
super
end
end
pdf = to_pdf <<~'END', backend: backend, pdf_theme: { heading_margin_bottom: 0, heading_margin_top: 100 }, analyze: true
[.first]
paragraph
[.second]
paragraph
END
first_heading_text = pdf.find_unique_text 'First Heading'
(expect first_heading_text).not_to be_nil
(expect first_heading_text[:font_size]).to eql 10.5
(expect first_heading_text[:font_color]).to eql '333333'
second_heading_text = pdf.find_unique_text 'Second Heading'
(expect second_heading_text).not_to be_nil
(expect second_heading_text[:font_size]).to eql 10.5
(expect second_heading_text[:font_color]).to eql '333333'
(expect second_heading_text[:y]).to be < 700
text = pdf.text
(expect text[0][:y] - text[1][:y]).to be < text[1][:y] - text[2][:y]
end
it 'should allow custom converter to invoke layout_general_heading with opts' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def convert_paragraph node
if node.has_role? 'heading'
layout_general_heading node, node.source, text_transform: 'uppercase', size: 100, color: 'AA0000', line_height: 1.2, margin: 20
else
super
end
end
end
pdf = to_pdf <<~'END', backend: backend, analyze: true
before
[.heading]
heading
paragraph
END
heading_text = pdf.find_unique_text 'HEADING'
(expect heading_text).not_to be_nil
(expect heading_text[:font_size]).to eql 100
(expect heading_text[:font_color]).to eql 'AA0000'
(expect heading_text[:y].floor).to eql 650
(expect (pdf.find_unique_text 'paragraph')[:y].floor).to eql 588
end
it 'should allow custom converter to override layout_general_heading for section title' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def layout_general_heading node, title, opts = {}
layout_heading title, (opts.merge transform: (node.attr :transform).to_sym)
end
def layout_heading title, opts
title = title.send opts.delete :transform
super
end
end
pdf = to_pdf <<~'END', backend: backend, analyze: true
[transform=upcase]
== Section Title
END
heading_text = pdf.find_unique_text 'SECTION TITLE'
(expect heading_text).not_to be_nil
end
it 'should allow custom converter to override ink_general_heading for section title' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def ink_general_heading sect, title, opts = {}
if (image_path = sect.attr 'image')
image_attrs = { 'target' => image_path, 'pdfwidth' => '1in' }
image_block = Asciidoctor::Block.new sect.document, :image, content_model: :empty, attributes: image_attrs
convert_image image_block, relative_to_imagesdir: true, pinned: true
end
super
end
end
pdf = to_pdf <<~'END', backend: backend, analyze: :image
[image=tux.png]
== Section Title
END
(expect pdf.images).to have_size 1
end
it 'should allow custom converter to override ink_general_heading for article doctitle' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def ink_general_heading _sect, title, opts = {}
return super unless opts[:role] == :doctitle
theme_font :heading_doctitle do
ink_prose title, align: :center, margin: 0
end
theme_margin :heading_doctitle, :bottom
end
end
pdf_theme = { heading_doctitle_font_color: '0000EE', heading_doctitle_margin_bottom: 24 }
pdf = to_pdf <<~'END', backend: backend, pdf_theme: pdf_theme, analyze: true
= Article Title
First paragraph of body.
First paragraph of body.
First paragraph of body.
First paragraph of body.
END
(expect pdf.pages).to have_size 1
title_text = pdf.find_unique_text 'Article Title'
(expect title_text[:font_color]).to eql '0000EE'
para_text = pdf.text[1]
(expect title_text[:y] - (para_text[:y] + para_text[:font_size])).to be > 24
end
it 'should remap layout_ methods added by prepended module' do
backend = nil
converter_class = create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
end
converter_class.prepend (Module.new do
def layout_prose string, opts = {}
opts[:color] = 'FF0000'
super
end
end)
pdf = to_pdf 'color me red', backend: backend, analyze: true
text = pdf.text
(expect text).to have_size 1
(expect text[0][:font_color]).to eql 'FF0000'
end
it 'should allow extended converter to flag page as imported to suppress running content' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def ink_part_title sect, title, opts
super
page.imported
end
end
pdf = to_pdf <<~'END', backend: backend, enable_footer: true, analyze: true
= Document Title
:doctype: book
= Part Title
== Chapter
END
page_2_text = pdf.find_text page_number: 2
(expect page_2_text).to have_size 1
(expect page_2_text[0][:string]).to eql 'Part Title'
(expect (pdf.find_text page_number: 3).last[:string]).to eql '2'
end
it 'should allow extended converter to override convert_listing_or_literal to handle calls to convert_listing and convert_literal' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def convert_listing_or_literal node
node.lines[0] = node.lines[0].sub 'Ruby', 'World'
super
end
end
pdf = to_pdf <<~'END', backend: backend, analyze: true
[,ruby]
----
puts "Hello, Ruby!"
----
END
(expect pdf.text[0][:string]).to eql 'puts "Hello, World!"'
end
it 'should allow extended converter to override convert_code to handle calls to convert_listing and convert_literal' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def convert_code node
node.lines[0] = node.lines[0].sub 'Ruby', 'World'
super
end
end
pdf = to_pdf <<~'END', backend: backend, analyze: true
[,ruby]
----
puts "Hello, Ruby!"
----
END
(expect pdf.text[0][:string]).to eql 'puts "Hello, World!"'
end
it 'should allow extended converter to temporarily override theme using save_theme' do
backend = nil
create_class (Asciidoctor::Converter.for 'pdf') do
register_for (backend = %(pdf#{object_id}).to_sym)
def convert_table node
if node.role?
save_theme do
theme.table_border_color = theme.table_grid_color = '0000EE'
super
end
else
super
end
end
end
pdf = to_pdf <<~'END', backend: backend, analyze: :line
[.custom,cols=2*]
|===
|a |b
|c |d
|===
<<<
[cols=2*]
|===
|a |b
|c |d
|===
END
lines = pdf.lines
custom_lines = lines.select {|it| it[:color] == '0000EE' }
default_lines = lines.reject {|it| it[:color] == '0000EE' }
(expect custom_lines).to have_size 16
(expect custom_lines[0][:page_number]).to eql 1
(expect default_lines).to have_size 16
(expect default_lines[0][:page_number]).to eql 2
end
end
end