# frozen_string_literal: true require_relative 'spec_helper' describe 'Asciidoctor::PDF::Converter - Listing' do it 'should render empty block if listing block is empty' do pdf_theme = { code_line_height: 1, code_padding: 0, code_border_width: 1, code_border_radius: 0, } pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: :line ---- ---- EOS lines = pdf.lines (expect lines).to have_size 4 (expect lines[1][:from][:y] - lines[1][:to][:y]).to be <= 1 end it 'should wrap text consistently regardless of whether the characters contain diacritics' do pdf = to_pdf <<~'EOS', analyze: true :pdf-page-size: A5 .... aàbècìdòeùf gáhéiíjókúlým nâoêpîqôrûs tñuõvãw xäyëzïaöbücÿd aabbccddeef gghhiijjkkllm nnooppqqrrs ttuuvvw xxyyzzaabbccd .... EOS text = pdf.text (expect text).to have_size 4 (expect text[1][:string]).to start_with 'x' (expect text[3][:string]).to start_with 'x' end it 'should move unbreakable block shorter than page to next page to avoid splitting it' do pdf = to_pdf <<~EOS, analyze: true #{(['paragraph'] * 20).join (?\n * 2)} [%unbreakable] ---- #{(['listing'] * 20).join ?\n} ---- EOS listing_page_numbers = (pdf.find_text 'listing').map {|it| it[:page_number] }.uniq (expect listing_page_numbers).to eql [2] end it 'should keep anchor together with block when block is moved to next page' do pdf = to_pdf <<~EOS #{(['paragraph'] * 20).join (?\n * 2)} [#listing-1%unbreakable] ---- #{(['listing'] * 20).join ?\n} ---- EOS (expect (pdf.page 1).text).not_to include 'listing' (expect (pdf.page 2).text).to include 'listing' (expect (dest = get_dest pdf, 'listing-1')).not_to be_nil (expect dest[:page_number]).to be 2 (expect dest[:y]).to eql 805.89 end it 'should place anchor directly at top of block' do input = <<~'EOS' paragraph [#listing-1] ---- listing ---- EOS lines = (to_pdf input, analyze: :line).lines pdf = to_pdf input (expect (dest = get_dest pdf, 'listing-1')).not_to be_nil (expect dest[:page_number]).to be 1 (expect dest[:y]).to eql lines[0][:from][:y] end it 'should offset anchor from top of block by value of block_anchor_top' do input = <<~'EOS' paragraph [#listing-1] ---- listing ---- EOS pdf_theme = { block_anchor_top: -12 } lines = (to_pdf input, pdf_theme: pdf_theme, analyze: :line).lines pdf = to_pdf input, pdf_theme: pdf_theme (expect (dest = get_dest pdf, 'listing-1')).not_to be_nil (expect dest[:page_number]).to be 1 (expect dest[:y]).to eql (lines[0][:from][:y] + -pdf_theme[:block_anchor_top]) end it 'should place anchor at top of block if advanced to next page' do input = <<~EOS paragraph [#listing-1%unbreakable] ---- #{(['filler'] * 25).join %(\n\n)} ---- EOS lines = (to_pdf input, analyze: :line).lines pdf = to_pdf input (expect (dest = get_dest pdf, 'listing-1')).not_to be_nil (expect dest[:page_number]).to be 2 (expect dest[:y]).to eql lines[0][:from][:y] end it 'should split block if it cannot fit on a whole page' do pdf = to_pdf <<~EOS, analyze: true #{(['paragraph'] * 20).join (?\n * 2)} ---- #{(['listing'] * 60).join ?\n} ---- EOS (expect pdf.pages).to have_size 2 listing_texts = pdf.find_text 'listing' (expect listing_texts[0][:page_number]).to be 1 (expect listing_texts[-1][:page_number]).to be 2 end it 'should use dashed border to indicate where block is split across a page boundary', visual: true do to_file = to_pdf_file <<~EOS, 'listing-page-split.pdf' ---- #{(['listing'] * 60).join ?\n} ---- ---- #{(['more listing'] * 2).join ?\n} ---- EOS (expect to_file).to visually_match 'listing-page-split.pdf' end it 'should not collapse bottom padding if block ends near bottom of page' do pdf_theme = { code_padding: 11, code_background_color: 'EEEEEE', code_border_width: 0, code_border_radius: 0, } pdf = with_content_spacer 10, 695 do |spacer_path| to_pdf <<~EOS, pdf_theme: pdf_theme, analyze: true image::#{spacer_path}[] ---- $ gem install asciidoctor-pdf $ asciidoctor-pdf doc.adoc ---- EOS end pages = pdf.pages (expect pages).to have_size 1 gs = pdf.extract_graphic_states pages[0][:raw_content] (expect gs[1]).to have_background color: 'EEEEEE', top_left: [48.24, 98.89], bottom_right: [48.24, 48.24] last_text_y = pdf.text[-1][:y] (expect last_text_y - pdf_theme[:code_padding]).to be > 48.24 pdf = with_content_spacer 10, 696 do |spacer_path| to_pdf <<~EOS, pdf_theme: pdf_theme, analyze: true image::#{spacer_path}[] ---- $ gem install asciidoctor-pdf $ asciidoctor-pdf doc.adoc ---- EOS end pages = pdf.pages (expect pages).to have_size 2 gs = pdf.extract_graphic_states pages[0][:raw_content] (expect gs[1]).to have_background color: 'EEEEEE', top_left: [48.24, 97.89], bottom_right: [48.24, 48.24] (expect pdf.text[0][:page_number]).to eql 1 (expect pdf.text[1][:page_number]).to eql 2 (expect pdf.text[0][:y] - pdf_theme[:code_padding]).to be > 48.24 end it 'should break line if wider than content area of block and still compute height correctly' do pdf_theme = { code_border_radius: 0, code_border_color: 'CCCCCC', code_border_width: [1, 0], code_background_color: 'transparent', sidebar_border_radius: 0, sidebar_border_width: [1, 0], sidebar_border_color: '0000EE', sidebar_background_color: 'transparent', } input = <<~EOS **** before ---- one tw#{'o' * 250} three ---- after **** EOS pdf = to_pdf input, pdf_theme: pdf_theme, analyze: true (expect pdf.find_text %r/^ooo/).to have_size 3 lines = (to_pdf input, pdf_theme: pdf_theme, analyze: :line).lines.sort_by {|it| -it[:from][:y] } (expect lines).to have_size 4 (expect lines[0][:color]).to eql '0000EE' (expect lines[1][:color]).to eql 'CCCCCC' (expect lines[2][:color]).to eql 'CCCCCC' (expect lines[3][:color]).to eql '0000EE' (expect (lines[0][:from][:y] - lines[1][:from][:y]).round 5).to eql ((lines[2][:from][:y] - lines[3][:from][:y]).round 5) end it 'should resize font to prevent wrapping if autofit option is set' do pdf = to_pdf <<~'EOS', analyze: true [%autofit] ---- @themesdir = ::File.expand_path theme.__dir__ || (doc.attr 'pdf-themesdir') || ::Dir.pwd ---- EOS (expect pdf.text).to have_size 1 (expect pdf.text[0][:font_size]).to be < build_pdf_theme.code_font_size end it 'should not resize font if not necessary' do pdf = to_pdf <<~'EOS', analyze: true [%autofit] ---- puts 'Hello, World!' ---- EOS (expect pdf.text).to have_size 1 (expect pdf.text[0][:font_size]).to eql 11 end it 'should not resize font more than base minimum font size' do pdf = to_pdf <<~'EOS', pdf_theme: { base_font_size_min: 8 }, analyze: true [%autofit] ---- play_symbol = (node.document.attr? 'icons', 'font') ? %(#{(icon_font_data 'fas').unicode 'play'}) : RightPointer ---- EOS (expect pdf.text).to have_size 2 (expect pdf.text[0][:font_size]).to be 8 end it 'should not resize font more than code minimum font size' do pdf = to_pdf <<~'EOS', pdf_theme: { base_font_size_min: 0, code_font_size_min: 8 }, analyze: true [%autofit] ---- play_symbol = (node.document.attr? 'icons', 'font') ? %(#{(icon_font_data 'fas').unicode 'play'}) : RightPointer ---- EOS (expect pdf.text).to have_size 2 (expect pdf.text[0][:font_size]).to be 8 end it 'should allow autofit to shrink text as much as it needs if the minimum font size is 0 or nil' do [0, nil].each do |size| pdf = to_pdf <<~'EOS', pdf_theme: { base_font_size_min: size }, analyze: true [%autofit] ---- +--------------------------------------+----------------------------------------------------+-----------------------------------------------------+ | id | name | subnets | +--------------------------------------+----------------------------------------------------+-----------------------------------------------------+ ---- EOS expected_line = '+--------------------------------------+----------------------------------------------------+-----------------------------------------------------+' lines = pdf.lines (expect lines).to have_size 3 (expect lines[0]).to eql expected_line (expect lines[2]).to eql expected_line end end it 'should use base font color if font color is not specified' do pdf = to_pdf <<~'EOS', pdf_theme: { base_font_color: 'AA0000', code_font_color: nil }, analyze: true before ---- in the mix ---- EOS before_text = pdf.find_unique_text 'before' (expect before_text[:font_color]).to eql 'AA0000' code_text = pdf.find_unique_text 'in the mix' (expect code_text[:font_color]).to eql 'AA0000' end it 'should allow theme to set different padding per edge when autofit is enabled' do pdf_theme = { code_border_radius: 0, code_padding: [5, 10, 15, 20], code_background_color: nil, } input = <<~EOS [%autofit] ---- downloading#{(%w(.) * 100).join} done ---- EOS text = (to_pdf input, pdf_theme: pdf_theme, analyze: true).text lines = (to_pdf input, pdf_theme: pdf_theme, analyze: :line).lines (expect text).to have_size 2 left = lines[0][:from][:x] top = lines[0][:to][:y] bottom = lines[1][:to][:y] (expect text[0][:x]).to eql (left + 20.0).round 2 (expect text[0][:y] + text[0][:font_size]).to be_within(2).of(top - 5) (expect text[1][:y]).to be_within(5).of(bottom + 15) end it 'should guard indentation using no-break space character' do pdf = to_pdf <<~'EOS', analyze: true ---- flush indented flush ---- EOS (expect pdf.lines).to eql ['flush', %(\u00a0 indented), 'flush'] end it 'should guard indentation using no-break space character if string starts with indented line' do pdf = to_pdf <<~'EOS', analyze: true ---- indented flush indented ---- EOS (expect pdf.lines).to eql [%(\u00a0 indented), 'flush', %(\u00a0 indented)] end it 'should expand tabs if tabsize attribute is not specified' do pdf = to_pdf <<~EOS, analyze: true ---- flush lead space \tlead tab \tlead tab\tcolumn tab lead space\tcolumn tab flush\t\t\tcolumn tab ---- EOS expected_lines = [ 'flush', %(\u00a0 lead space), %(\u00a0 lead tab), %(\u00a0 lead tab column tab), %(\u00a0 lead space column tab), 'flush column tab', ] (expect pdf.lines).to eql expected_lines lines = pdf.text line_gaps = 1.upto(lines.size - 1).map {|idx| (lines[idx - 1][:y] - lines[idx][:y]).round 2 } (expect line_gaps[-1]).to eql line_gaps[-2] * 2 (expect line_gaps[-2]).to eql line_gaps[-3] end it 'should expand tabs if tabsize is specified as block attribute' do pdf = to_pdf <<~EOS, analyze: true [tabsize=4] ---- flush lead space \tlead tab \tlead tab\tcolumn tab lead space\tcolumn tab flush\t\t\tcolumn tab ---- EOS expected_lines = [ 'flush', %(\u00a0 lead space), %(\u00a0 lead tab), %(\u00a0 lead tab column tab), %(\u00a0 lead space column tab), 'flush column tab', ] (expect pdf.lines).to eql expected_lines lines = pdf.text line_gaps = 1.upto(lines.size - 1).map {|idx| (lines[idx - 1][:y] - lines[idx][:y]).round 2 } (expect line_gaps[-1]).to eql line_gaps[-2] * 2 (expect line_gaps[-2]).to eql line_gaps[-3] end it 'should expand tabs if tabsize is specified as document attribute' do pdf = to_pdf <<~EOS, analyze: true :tabsize: 4 ---- flush lead space \tlead tab \tlead tab\tcolumn tab lead space\tcolumn tab flush\t\t\tcolumn tab ---- EOS expected_lines = [ 'flush', %(\u00a0 lead space), %(\u00a0 lead tab), %(\u00a0 lead tab column tab), %(\u00a0 lead space column tab), 'flush column tab', ] (expect pdf.lines).to eql expected_lines lines = pdf.text line_gaps = 1.upto(lines.size - 1).map {|idx| (lines[idx - 1][:y] - lines[idx][:y]).round 2 } (expect line_gaps[-1]).to eql line_gaps[-2] * 2 (expect line_gaps[-2]).to eql line_gaps[-3] end it 'should add numbered label to block title if listing-caption attribute is set' do pdf = to_pdf <<~'EOS', analyze: true :listing-caption: Listing .Title ---- content ---- EOS title_text = pdf.find_unique_text font_name: 'NotoSerif-Italic' (expect title_text[:string]).to eql 'Listing 1. Title' end it 'should allow theme to override caption for code blocks' do pdf_theme = { caption_font_color: '0000ff', code_caption_font_style: 'bold', } pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: true .Title ---- content ---- EOS title_text = (pdf.find_text 'Title')[0] (expect title_text[:font_color]).to eql '0000FF' (expect title_text[:font_name]).to eql 'NotoSerif-Bold' end it 'should allow theme to set background color on caption' do pdf_theme = { code_caption_font_color: 'ffffff', code_caption_font_style: 'bold', code_caption_background_color: 'AA0000', code_background_color: 'transparent', code_border_radius: 0, } pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: true .Caption with background color ---- content ---- EOS title_text = pdf.find_unique_text 'Caption with background color' (expect title_text[:font_color]).to eql 'FFFFFF' (expect title_text[:font_name]).to eql 'NotoSerif-Bold' (expect pdf.pages[0][:raw_content]).to include %(/DeviceRGB cs\n0.66667 0.0 0.0 scn\n48.24 790.899 498.8 14.991 re) end it 'should allow theme to place caption below block' do pdf_theme = { code_caption_end: 'bottom' } pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: true .Look out below! ---- code ---- EOS content_text = pdf.find_unique_text 'code' title_text = pdf.find_unique_text 'Look out below!' (expect title_text[:y]).to be < content_text[:y] end it 'should apply inline formatting if quotes subs is enabled' do pdf = to_pdf <<~'EOS', analyze: true [subs=+quotes] ---- _1_ skipped *99* passing ---- EOS italic_text = (pdf.find_text '1')[0] (expect italic_text[:font_name]).to eql 'mplus1mn-italic' bold_text = (pdf.find_text '99')[0] (expect bold_text[:font_name]).to eql 'mplus1mn-bold' end it 'should honor font family set on conum category in theme for conum in listing block' do pdf = to_pdf <<~'EOS', pdf_theme: { code_font_family: 'Courier' }, analyze: true ---- fe <1> fi <2> fo <3> ---- EOS lines = pdf.lines (expect lines[0]).to end_with ' ①' (expect lines[1]).to end_with ' ②' (expect lines[2]).to end_with ' ③' conum_text = (pdf.find_text '①')[0] (expect conum_text[:font_name]).not_to eql 'Courier' end it 'should allow theme to set conum color using CMYK value' do cmyk_color = [0, 100, 100, 60].extend Asciidoctor::PDF::ThemeLoader::CMYKColorValue pdf = to_pdf <<~'EOS', pdf_theme: { conum_font_color: cmyk_color }, analyze: true ---- foo <1> ---- <1> the counterpart of bar EOS conum_texts = pdf.find_text '①' (expect conum_texts).to have_size 2 # NOTE: yes, the hex color is all weird here; could be a parser issue (expect conum_texts[0][:font_color]).to eql cmyk_color.map(&:to_f) (expect conum_texts[1][:font_color]).to eql cmyk_color.map(&:to_f) end it 'should allow width of border to be set only on ends' do pdf_theme = { code_border_color: 'AA0000', code_border_width: [1, nil], } pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: :line ---- foo bar baz ---- EOS lines = pdf.lines (expect lines).to have_size 2 (expect lines[0][:from][:y]).to eql lines[0][:to][:y] (expect lines[1][:from][:y]).to eql lines[1][:to][:y] end it 'should allow width of border to be set only on sides' do pdf_theme = { code_border_color: 'AA0000', code_border_width: [nil, 1], } pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: :line ---- foo bar baz ---- EOS lines = pdf.lines (expect lines).to have_size 2 (expect lines[0][:from][:x]).to eql lines[0][:to][:x] (expect lines[1][:from][:x]).to eql lines[1][:to][:x] end it 'should allow width of border on ends and sides to be different' do pdf_theme = { code_border_color: 'AA0000', code_border_width: [2, 1], } pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: :line ---- foo bar baz ---- EOS lines = pdf.lines (expect lines).to have_size 4 (expect lines[0][:from][:y]).to eql lines[0][:to][:y] (expect lines[0][:width]).to eql 2 (expect lines[1][:from][:x]).to eql lines[1][:to][:x] (expect lines[1][:width]).to eql 1 end it 'should allow width of border to be only set on one end' do pdf_theme = { code_border_color: 'AA0000', code_border_width: [1, 0, 0, 0], } pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: :line ---- foo bar baz ---- EOS lines = pdf.lines (expect lines).to have_size 1 (expect lines[0][:from][:y]).to eql lines[0][:to][:y] (expect lines[0][:width]).to eql 1 end it 'should allow max width of border with different ends and sides to be less than 1' do pdf_theme = { code_border_color: 'AA0000', code_border_width: [0.5, 0], } pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: :line ---- foo bar baz ---- EOS lines = pdf.lines (expect lines).to have_size 2 (expect lines[0][:from][:y]).to eql lines[0][:to][:y] (expect lines[0][:width]).to eql 0.5 (expect lines[1][:from][:y]).to eql lines[1][:to][:y] (expect lines[1][:width]).to eql 0.5 end it 'should use dashed border to indicate where block is split across a page boundary when border is only on ends', visual: true do pdf_theme = { code_border_color: 'AA0000', code_border_width: [1, 0], } to_file = to_pdf_file <<~EOS, 'listing-page-split-border-ends.pdf', pdf_theme: pdf_theme ---- #{(['listing'] * 60).join ?\n} ---- EOS (expect to_file).to visually_match 'listing-page-split-border-ends.pdf' end it 'should allow theme to set different padding per edge' do pdf_theme = { code_border_radius: 0, code_padding: [5, 10, 15, 20], code_background_color: nil, } input = <<~EOS ---- downloading#{(%w(.) * 100).join}done ---- EOS text = (to_pdf input, pdf_theme: pdf_theme, analyze: true).text lines = (to_pdf input, pdf_theme: pdf_theme, analyze: :line).lines left = lines[0][:from][:x] top = lines[0][:to][:y] bottom = lines[1][:to][:y] (expect text[0][:x]).to eql (left + 20.0).round 2 (expect text[0][:y] + text[0][:font_size]).to be_within(1).of(top - 5) (expect text[1][:y]).to be_within(5).of(bottom + 15) end it 'should allow theme to set different padding for ends and sides' do pdf_theme = { code_border_radius: 0, code_padding: [10, 5], code_background_color: nil, } input = <<~EOS ---- source code here ---- EOS text = (to_pdf input, pdf_theme: pdf_theme, analyze: true).text lines = (to_pdf input, pdf_theme: pdf_theme, analyze: :line).lines left = lines[0][:from][:x] top = lines[0][:to][:y] bottom = lines[1][:to][:y] (expect text[0][:x]).to eql (left + 5.0).round 2 (expect text[0][:y] + text[0][:font_size]).to be_within(1).of(top - 10) (expect text[0][:y]).to be_within(3).of(bottom + 10) end it 'should allow theme to set 3-value padding that contains a nil value' do pdf_theme = { code_border_radius: 0, code_padding: [10, nil, 5], code_background_color: nil, code_border_width: [1, 0], } input = <<~EOS ---- source code here ---- EOS lines = (to_pdf input, pdf_theme: pdf_theme, analyze: :line).lines text = (to_pdf input, pdf_theme: pdf_theme, analyze: true).text (expect lines).to have_size 2 (expect text).to have_size 1 (expect lines[0][:from][:x]).to eql 48.24 (expect text[0][:x]).to eql 48.24 end it 'should not substitute conums if callouts sub is absent' do pdf = to_pdf <<~'EOS', analyze: true [subs=-callouts] ---- not a conum <1> ---- EOS (expect pdf.lines).to include 'not a conum <1>' (expect pdf.find_text '①').to be_empty end end