# frozen_string_literal: true

require_relative 'spec_helper'

describe 'Asciidoctor::PDF::Converter - TOC' do
  context 'book' do
    it 'should not generate toc by default' do
      pdf = to_pdf <<~'END', doctype: :book, analyze: true
      = Document Title

      == Introduction

      == Main

      == Conclusion
      END
      (expect pdf.pages).to have_size 4
      (expect pdf.find_text 'Table of Contents').to be_empty
    end

    it 'should insert toc between title page and first page of body when toc is set' do
      pdf = to_pdf <<~'END', doctype: :book, analyze: true
      = Document Title
      :toc:

      == Introduction

      == Main

      == Conclusion
      END
      (expect pdf.pages).to have_size 5
      (expect pdf.find_text 'Document Title', page_number: 1).not_to be_empty
      (expect pdf.find_text 'Table of Contents', page_number: 2).not_to be_empty
      (expect pdf.find_text '1', page_number: 2).not_to be_empty
      (expect pdf.find_text '2', page_number: 2).not_to be_empty
      (expect pdf.find_text '3', page_number: 2).not_to be_empty
      (expect pdf.find_text 'Introduction', page_number: 3).not_to be_empty
    end

    it 'should space items in toc evenly even if title is entirely monospace' do
      pdf = to_pdf <<~'END', doctype: :book, analyze: true
      = Document Title
      :toc:

      == Beginning

      == `Middle`

      == End
      END
      (expect pdf.find_text 'Table of Contents', page_number: 2).not_to be_empty
      beginning_pagenum_text = (pdf.find_text '1', page_number: 2)[0]
      middle_pagenum_text = (pdf.find_text '2', page_number: 2)[0]
      end_pagenum_text = (pdf.find_text '3', page_number: 2)[0]
      beginning_to_middle_spacing = (beginning_pagenum_text[:y] - middle_pagenum_text[:y]).round 2
      middle_to_end_spacing = (middle_pagenum_text[:y] - end_pagenum_text[:y]).round 2
      (expect beginning_to_middle_spacing).to eql middle_to_end_spacing
    end

    it 'should only include preface in toc if preface-title is set' do
      input = <<~'END'
      = Document Title

      [preface]
      This is the preface.

      == Chapter 1

      And away we go!
      END

      [{ 'toc' => '' }, { 'toc' => '', 'preface-title' => 'Preface' }].each do |attrs|
        pdf = to_pdf input, doctype: :book, attributes: attrs, analyze: :page
        (expect pdf.pages).to have_size 4
        (expect pdf.pages[0][:strings]).to include 'Document Title'
        (expect pdf.pages[1][:strings]).to include 'Table of Contents'
        if attrs.include? 'preface-title'
          (expect pdf.pages[1][:strings]).to include 'Preface'
          (expect pdf.pages[1][:strings]).to include '1'
        else
          (expect pdf.pages[1][:strings]).not_to include 'Preface'
          (expect pdf.pages[1][:strings]).not_to include '1'
        end
        (expect pdf.pages[1][:strings]).to include 'Chapter 1'
        (expect pdf.pages[1][:strings]).to include '2'
        (expect pdf.pages[2][:strings]).to include 'Preface' if attrs.include? 'preface-title'
        (expect pdf.pages[2][:strings]).to include 'This is the preface.'
        (expect pdf.pages[3][:strings]).to include 'Chapter 1'
      end
    end

    it 'should output toc with depth specified by toclevels' do
      pdf = to_pdf <<~'END', doctype: :book, analyze: :page
      = Document Title
      :toc:
      :toclevels: 1

      == Level 1

      === Level 2

      ==== Level 3
      END
      (expect pdf.pages).to have_size 3
      (expect pdf.pages[0][:strings]).to include 'Document Title'
      (expect pdf.pages[1][:strings]).to include 'Table of Contents'
      (expect pdf.pages[1][:strings]).to include 'Level 1'
      (expect pdf.pages[1][:strings]).not_to include 'Level 2'
      (expect pdf.pages[1][:strings]).not_to include 'Level 3'
      (expect pdf.pages[2][:strings]).to include 'Level 1'
    end

    it 'should only show parts in toc if toclevels attribute is 0' do
      pdf = to_pdf <<~'END', doctype: :book, analyze: :page
      = Document Title
      :toc:
      :toclevels: 0

      = Part One

      == Chapter A

      = Part Two

      == Chapter B
      END
      (expect pdf.pages).to have_size 6
      (expect pdf.pages[1][:strings]).to include 'Table of Contents'
      (expect pdf.pages[1][:strings]).to include 'Part One'
      (expect pdf.pages[1][:strings]).to include 'Part Two'
      (expect pdf.pages[1][:strings]).not_to include 'Chapter A'
      (expect pdf.pages[1][:strings]).not_to include 'Chapter B'
    end

    it 'should not show any section titles when toclevels is less than 0' do
      pdf = to_pdf <<~'END', doctype: :book, analyze: true
      = Document Title
      :toc:
      :toclevels: -1

      = Part One

      == Chapter A

      = Part Two

      == Chapter B
      END
      (expect pdf.pages).to have_size 6
      toc_lines = pdf.lines pdf.find_text page_number: 2
      (expect toc_lines).to eql ['Table of Contents']
    end

    it 'should allow section to override toclevels for descendant sections' do
      pdf = to_pdf <<~'END', analyze: true
      = Document Title
      :doctype: book
      :toc:
      :toclevels: 3

      == Chapter

      === Chapter Section

      ==== Chapter Subsection

      [appendix,toclevels=1]
      == Lorem Ipsum

      === Appendix Section

      ==== Appendix Subsection
      END

      (expect pdf.find_text page_number: 2, string: 'Chapter').to have_size 1
      (expect pdf.find_text page_number: 2, string: 'Chapter Section').to have_size 1
      (expect pdf.find_text page_number: 2, string: 'Chapter Subsection').to have_size 1
      (expect pdf.find_text page_number: 2, string: 'Appendix A: Lorem Ipsum').to have_size 1
      (expect pdf.find_text page_number: 2, string: 'Appendix Section').to have_size 0
    end

    it 'should include dot leaders in TOC entry if toclevels is increased for a given section' do
      pdf = to_pdf <<~'END', analyze: true
      = Document Title
      :doctype: book
      :toc:
      :toclevels: 1

      == Chapter

      === Chapter Section

      ==== Chapter Subsection

      [toclevels=3]
      == Lorem Ipsum

      === Lorem Ipsum Section

      ==== Lorem Ipsum Subsection
      END

      toc_lines = pdf.lines pdf.find_text page_number: 2
      (expect toc_lines).to have_size 5
      (expect toc_lines[1]).to match %r/^Chapter (\. )+.*?1/
      (expect toc_lines[2]).to match %r/^Lorem Ipsum (\. )+.*?2/
      (expect toc_lines[3]).to match %r/^Lorem Ipsum Section (\. )+.*?2/
      (expect toc_lines[4]).to match %r/^Lorem Ipsum Subsection (\. )+.*?2/
    end

    it 'should allow section to remove itself from toc by setting toclevels to less than section level' do
      pdf = to_pdf <<~'END', analyze: true
      = Document Title
      :doctype: book
      :toc:
      :toclevels: 3

      == Chapter

      === Chapter Section

      ==== Chapter Subsection

      [appendix,toclevels=0]
      == Lorem Ipsum

      === Appendix Section

      ==== Appendix Subsection
      END

      (expect pdf.find_text page_number: 2, string: 'Chapter').to have_size 1
      (expect pdf.find_text page_number: 2, string: 'Chapter Section').to have_size 1
      (expect pdf.find_text page_number: 2, string: 'Chapter Subsection').to have_size 1
      (expect pdf.find_text page_number: 2, string: 'Appendix A: Lorem Ipsum').to have_size 0
    end

    it 'should reserve enough pages for toc if it spans more than one page' do
      sections = (1..40).map {|num| %(\n\n=== Section #{num}) }
      pdf = to_pdf <<~END, doctype: :book, analyze: :page
      = Document Title
      :toc:

      == Chapter 1#{sections.join}
      END
      (expect pdf.pages).to have_size 6
      (expect pdf.pages[0][:strings]).to include 'Document Title'
      (expect pdf.pages[1][:strings]).to include 'Table of Contents'
      (expect pdf.pages[3][:strings]).to include 'Chapter 1'
    end

    it 'should render all TOC entries when computing extent of TOC when sectids is unset' do
      input = <<~END
      = Document Title
      :doctype: book
      :pdf-page-size: A5
      :toc:
      :!sectids:

      the preface

      #{30.times.map {|idx| %(== Chapter #{idx + 1}) }.join ?\n}
      END

      pdf = to_pdf input, analyze: true
      preface_text = pdf.find_unique_text 'the preface'
      last_toc_entry_text = (pdf.find_text 'Chapter 30')[0]
      (expect preface_text[:page_number]).to be > last_toc_entry_text[:page_number]
    end

    it 'should render descendants of section without ID when computing extent of TOC' do
      input = <<~END
      = Document Title
      :doctype: book
      :pdf-page-size: A5
      :toc:

      the preface

      :!sectids:
      == Chapter 1
      :sectids:

      #{5.times.map {|idx| %(=== Section #{idx + 1}) }.join ?\n}

      #{21.times.map {|idx| %(== Chapter #{idx + 2}) }.join ?\n}
      END

      pdf = to_pdf input, analyze: true
      preface_text = pdf.find_unique_text 'the preface'
      last_toc_entry_text = (pdf.find_text 'Chapter 22')[0]
      (expect preface_text[:page_number]).to be > last_toc_entry_text[:page_number]
    end

    it 'should insert toc after preamble if toc attribute is preamble' do
      pdf = to_pdf <<~'END', analyze: true
      = Document Title
      :toc: preamble

      This is the preamble.

      == Introduction

      content

      == Conclusion

      content
      END

      toc_title_text = pdf.find_unique_text 'Table of Contents'
      (expect toc_title_text[:page_number]).to be 1
      introduction_title_text = pdf.find_unique_text 'Introduction', font_name: 'NotoSerif-Bold'
      (expect introduction_title_text[:y]).to be < toc_title_text[:y]
      introduction_toc_text = pdf.find_unique_text 'Introduction', font_name: 'NotoSerif'
      (expect introduction_toc_text[:y]).to be > introduction_title_text[:y]
    end

    it 'should insert toc at location of toc macro if toc attribute is macro' do
      lorem = ['lorem ipsum'] * 10 * %(\n\n)
      input = <<~END
      = Document Title
      :doctype: book
      :toc: macro

      Preamble

      == Introduction

      #{lorem}

      toc::[]

      == Main

      #{lorem}

      == Conclusion

      #{lorem}
      END
      pdf = to_pdf input, analyze: true
      (expect pdf.pages).to have_size 6
      toc_title_text = (pdf.find_text 'Table of Contents')[0]
      (expect toc_title_text[:page_number]).to be 4

      pdf = to_pdf input
      outline = extract_outline pdf
      (expect outline).to have_size 5
      (expect outline.map {|it| it[:title] }).to eql ['Document Title', 'Introduction', 'Table of Contents', 'Main', 'Conclusion']
      toc_dest = outline[2][:dest]
      (expect toc_dest[:pagenum]).to be 4
      (expect toc_dest[:label]).to eql '3'
      (expect toc_dest[:y]).to be > 800
    end

    it 'should insert macro toc in outline as sibling of section in which it is contained' do
      pdf = to_pdf <<~'END'
      = Document Title
      :doctype: book
      :toc: macro

      == Chapter

      === Section

      toc::[]

      === Another Section
      END

      outline = extract_outline pdf
      chapter_entry = outline[1]
      (expect chapter_entry).not_to be_nil
      (expect chapter_entry[:title]).to eql 'Chapter'
      chapter_entry_children = chapter_entry[:children]
      (expect chapter_entry_children).to have_size 3
      toc_entry = chapter_entry_children[1]
      (expect toc_entry[:title]).to eql 'Table of Contents'
      (expect toc_entry[:dest][:label]).to eql '2'
    end

    it 'should not insert toc at default location if document has no sections' do
      pdf = to_pdf <<~'END', analyze: true
      = Document Title
      :toc:

      No sections here.

      No sections here either.

      Fin.
      END

      (expect pdf.lines).to eql ['Document Title', 'No sections here.', 'No sections here either.', 'Fin.']
      p1_text = pdf.find_unique_text 'No sections here.'
      p2_text = pdf.find_unique_text 'No sections here either.'
      p3_text = pdf.find_unique_text 'Fin.'
      (expect (p1_text[:y] - p2_text[:y]).round 2).to eql ((p2_text[:y] - p3_text[:y]).round 2)
    end

    it 'should not insert toc at default location if converter overrides get_entries_for_toc and value is empty' do
      backend = nil
      create_class (Asciidoctor::Converter.for 'pdf') do
        register_for (backend = %(pdf#{object_id}).to_sym)
        def get_entries_for_toc _node
          []
        end
      end

      pdf = to_pdf <<~'END', backend: backend, analyze: true
      = Document Title
      :toc:

      == Beginning

      content

      == End

      content
      END

      (expect (pdf.find_unique_text 'Table of Contents')).to be_nil
      (expect (pdf.find_text 'Beginning')).to have_size 1
    end

    it 'should not insert toc at location of toc macro if document has no sections' do
      pdf = to_pdf <<~'END', analyze: true
      :toc: macro

      No sections here.

      toc::[]

      No sections here either.

      Fin.
      END

      (expect pdf.lines).to eql ['No sections here.', 'No sections here either.', 'Fin.']
      p1_text = pdf.find_unique_text 'No sections here.'
      p2_text = pdf.find_unique_text 'No sections here either.'
      p3_text = pdf.find_unique_text 'Fin.'
      (expect (p1_text[:y] - p2_text[:y]).round 2).to eql ((p2_text[:y] - p3_text[:y]).round 2)
    end

    it 'should not insert toc at location of toc macro if converter overrides get_entries_for_toc and value is empty' do
      backend = nil
      create_class (Asciidoctor::Converter.for 'pdf') do
        register_for (backend = %(pdf#{object_id}).to_sym)
        def get_entries_for_toc _node
          []
        end
      end

      pdf = to_pdf <<~'END', backend: backend, analyze: true
      = Document Title
      :toc: macro

      == Beginning

      content

      toc::[]

      == End

      content
      END

      (expect (pdf.find_unique_text 'Table of Contents')).to be_nil
      (expect (pdf.find_text 'Beginning')).to have_size 1
    end

    it 'should only insert macro toc at location of first toc macro' do
      lorem = ['lorem ipsum'] * 10 * %(\n\n)
      input = <<~END
      = Document Title
      :doctype: book
      :toc: macro

      Preamble

      == Introduction

      #{lorem}

      toc::[]

      == Main

      #{lorem}

      toc::[]

      == Conclusion

      #{lorem}
      END

      pdf = to_pdf input, analyze: true
      (expect pdf.pages).to have_size 6
      toc_title_text = (pdf.find_text 'Table of Contents')[0]
      (expect toc_title_text[:page_number]).to be 4
      toc_lines = pdf.lines pdf.find_text page_number: 4
      (expect toc_lines).to have_size 4
      ['Table of Contents', 'Introduction', 'Main', 'Conclusion'].each_with_index do |title, idx|
        (expect toc_lines[idx]).to start_with title
      end
    end

    it 'should not insert toc at location of toc macro if toc attribute is not set' do
      pdf = to_pdf <<~'END', analyze: true
      == Before

      toc::[]

      == After
      END

      (expect pdf.lines).to eql %w(Before After)
    end

    it 'should not insert toc at location of toc macro if toc-placement attribute is set but not toc attribute' do
      pdf = to_pdf <<~'END', analyze: true
      :toc-placement: macro

      == Before

      toc::[]

      == After
      END

      (expect pdf.lines).to eql %w(Before After)
    end

    it 'should not insert toc at location of toc macro if value of toc attribute is not macro' do
      pdf = to_pdf <<~'END', analyze: true
      :doctype: book
      :toc:

      == Chapter

      toc::[]

      text
      END

      (expect pdf.find_unique_text 'Table of Contents').not_to be_nil
      (expect pdf.lines pdf.find_text page_number: 2).to eql %w(Chapter text)
    end

    it 'should not start new page for toc in book if already at top of page' do
      pdf = to_pdf <<~END, analyze: true
      = Document Title
      :doctype: book
      :toc: macro

      == First Chapter

      #{(['filler'] * 26).join %(\n\n)}

      toc::[]

      == Last Chapter

      Fin.
      END

      (expect pdf.pages).to have_size 4
      toc_heading_text = pdf.find_unique_text 'Table of Contents'
      (expect toc_heading_text[:page_number]).to be 3
    end

    it 'should add top margin specified by theme to toc contents when toc has a title' do
      input = <<~'END'
      = Document Title
      :toc:

      == Section A

      === Subsection

      == Section B
      END

      toc_top_without_margin_top = ((to_pdf input, analyze: true).find_text 'Section A')[0][:y]
      toc_top_with_margin_top = ((to_pdf input, pdf_theme: { toc_margin_top: 50 }, analyze: true).find_text 'Section A')[0][:y]
      (expect toc_top_without_margin_top - toc_top_with_margin_top).to eql 50.0
    end

    it 'should add top margin specified by theme to toc contents when toc has no title and not at page top' do
      input = <<~'END'
      = Document Title
      :toc:
      :!toc-title:

      == Section A

      === Subsection

      == Section B
      END

      toc_top_without_margin_top = ((to_pdf input, analyze: true).find_text 'Section A')[0][:y]
      toc_top_with_margin_top = ((to_pdf input, pdf_theme: { toc_margin_top: 50 }, analyze: true).find_text 'Section A')[0][:y]
      (expect toc_top_without_margin_top - toc_top_with_margin_top).to eql 50.0
    end

    it 'should not add top margin specified by theme to toc contents when toc contents is at top of page' do
      input = <<~'END'
      = Document Title
      :doctype: book
      :toc:
      :!toc-title:

      == Section A

      === Subsection

      == Section B
      END

      toc_top_without_margin_top = ((to_pdf input, analyze: true).find_text 'Section A')[0][:y]
      toc_top_with_margin_top = ((to_pdf input, pdf_theme: { toc_margin_top: 50 }, analyze: true).find_text 'Section A')[0][:y]
      (expect toc_top_without_margin_top).to eql toc_top_with_margin_top
    end

    it 'should start preamble toc on recto page for prepress book' do
      pdf = to_pdf <<~'END', analyze: true
      = Document Title
      :doctype: book
      :media: prepress
      :toc: preamble

      In a land far away...

      == First Chapter

      There was a hero...

      == Last Chapter

      Fin.
      END

      (expect pdf.pages).to have_size 9
      toc_heading_text = pdf.find_unique_text 'Table of Contents'
      (expect toc_heading_text[:page_number]).to be 5
    end

    it 'should start macro toc on recto page for prepress book' do
      pdf = to_pdf <<~END, analyze: true
      = Document Title
      :doctype: book
      :media: prepress
      :toc: macro

      == First Chapter

      #{(['filler'] * 26).join %(\n\n)}

      toc::[]

      == Last Chapter

      Fin.
      END

      (expect pdf.pages).to have_size 7
      toc_heading_text = pdf.find_unique_text 'Table of Contents'
      (expect toc_heading_text[:page_number]).to be 5
    end

    it 'should not advance toc to recto page for prepress book when nonfacing option is specified on macro' do
      pdf = to_pdf <<~END, analyze: true
      = Document Title
      :doctype: book
      :media: prepress
      :toc: macro

      == First Chapter

      #{(['filler'] * 26).join %(\n\n)}

      toc::[opts=nonfacing]

      == Last Chapter

      Fin.
      END

      (expect pdf.pages).to have_size 5
      toc_heading_text = pdf.find_unique_text 'Table of Contents'
      (expect toc_heading_text[:page_number]).to be 4
    end

    it 'should not advance toc in preamble to recto page for prepress book when nonfacing option is specified on macro' do
      pdf = to_pdf <<~'END', analyze: true
      = Document Title
      :doctype: book
      :media: prepress
      :toc: macro

      [%nonfacing]
      toc::[]

      == First Chapter

      Début.

      == Last Chapter

      Fin.
      END

      (expect pdf.pages).to have_size 5
      toc_heading_text = pdf.find_unique_text 'Table of Contents'
      (expect toc_heading_text[:page_number]).to be 2
    end

    it 'should disable running content periphery on toc page if noheader or nofooter option is set on macro' do
      pdf_theme = {
        header_height: 30,
        header_font_color: 'FF0000',
        header_recto_right_content: 'Page {page-number}',
        header_verso_right_content: 'Page {page-number}',
        footer_font_color: '0000FF',
        footer_recto_right_content: 'Page {page-number}',
        footer_verso_right_content: 'Page {page-number}',
      }

      sections = (1..37).map {|num| %(== Section #{num}) }.join %(\n\n)
      pdf = to_pdf <<~END, pdf_theme: pdf_theme, enable_footer: true, analyze: true
      = Document Title
      :doctype: book
      :toc: macro

      == First Chapter

      #{sections}

      [%noheader%nofooter]
      toc::[]

      == Last Chapter
      END

      toc_text = (pdf.find_text 'Table of Contents')[0]
      (expect toc_text).not_to be_nil
      toc_page_number = toc_text[:page_number]
      last_chapter_text = (pdf.find_text 'Last Chapter')[-1]
      last_chapter_page_number = last_chapter_text[:page_number]
      toc_page_number.upto last_chapter_page_number do |page_number|
        header_texts = (pdf.find_text font_color: 'FF0000', page_number: page_number)
        footer_texts = (pdf.find_text font_color: '0000FF', page_number: page_number)
        if page_number < last_chapter_page_number
          (expect header_texts).to be_empty
          (expect footer_texts).to be_empty
        else
          (expect header_texts).not_to be_empty
          (expect footer_texts).not_to be_empty
        end
      end
    end

    it 'should not add toc title to page or outline if toc-title is unset' do
      pdf = to_pdf <<~'END'
      = Document Title
      :doctype: book
      :toc:
      :!toc-title:

      == Beginning

      == Middle

      == End
      END

      (expect pdf.pages).to have_size 5
      (expect pdf.pages[1].text).to start_with 'Beginning'

      outline = extract_outline pdf
      (expect outline).to have_size 4
      (expect outline[0][:title]).to eql 'Document Title'
      (expect outline[1][:title]).to eql 'Beginning'
    end

    it 'should not attempt to create dots if number of dots is less than 0' do
      section_title = (%w(verylongsectiontitle) * 5).join
      pdf = to_pdf <<~END, doctype: :book, analyze: true
      :toc:
      :toc-max-pagenum-digits: 0

      == #{section_title}
      END

      toc_lines = pdf.lines pdf.find_text page_number: 1
      (expect toc_lines).to have_size 2
      (expect toc_lines[0]).to eql 'Table of Contents'
      (expect toc_lines[1]).to eql %(#{section_title}\u00a01)
    end

    it 'should line up dots and page number with wrapped line' do
      pdf = to_pdf <<~'END', doctype: :book, analyze: true
      = Document Title
      :toc:

      == This Here is an Absurdly Long Section Title That Exceeds the Length of a Single Line and Therefore Wraps

      content
      END

      toc_text = pdf.find_text page_number: 2
      (expect toc_text.size).to be > 1
      (expect toc_text[1][:string]).to eql 'This Here is an Absurdly Long Section Title That Exceeds the Length of a Single Line and'
      (expect toc_text[2][:string]).to eql 'Therefore Wraps'
      (expect toc_text[1][:x]).to eql toc_text[2][:x]
      dot_leader_text = (pdf.find_text page_number: 2).select {|it| it[:string].start_with? '.' }
      (expect dot_leader_text).not_to be_empty
      (expect dot_leader_text[0][:y]).to be < toc_text[1][:y]
      page_number_text = pdf.find_text page_number: 2, string: '1'
      (expect page_number_text).to have_size 1
    end

    it 'should line up dots and page number with wrapped line when section title gets split across a page boundary' do
      sections = (1..37).map {|num| %(\n\n== Section #{num}) }.join
      pdf = to_pdf <<~END, doctype: :book, analyze: true
      = Document Title
      :toc:
      #{sections}

      == This is a unbelievably long section title that probably shouldn't be a section title at all but here we are

      content
      END

      page_2_lines = pdf.lines pdf.find_text page_number: 2
      (expect page_2_lines).to include 'Table of Contents'
      (expect page_2_lines[-1]).to end_with 'but here'
      page_3_lines = pdf.lines pdf.find_text page_number: 3
      (expect page_3_lines).to have_size 1
      (expect page_3_lines[0]).to match %r/we are ?(\. )+ ?\u00a038$/
    end

    it 'should not crash if last fragment in toc entry is not rendered' do
      (expect do
        pdf = to_pdf <<~END, analyze: true
        = Document Title
        :notitle:
        :!toc-title:
        :toc:
        :doctype: book

        == Chapter

        == #{(['foo bar'] * 12).join ' '} foo +++<span><br></span>+++
        END

        toc_lines = pdf.lines pdf.find_text page_number: 1
        (expect toc_lines).to have_size 2
        (expect toc_lines[1]).to end_with %(foo . . \u00a02)
      end).to not_raise_exception
    end

    it 'should not crash if last fragment in toc entry that wraps is not rendered' do
      (expect do
        pdf = to_pdf <<~END, analyze: true
        = Document Title
        :notitle:
        :!toc-title:
        :toc:
        :doctype: book

        == Chapter

        == #{(['foo bar'] * 24).join ' '} foo foo +++<span><br></span>+++
        END

        toc_lines = pdf.lines pdf.find_text page_number: 1
        (expect toc_lines).to have_size 3
        (expect toc_lines[2]).to end_with %(foo . . \u00a02)
      end).to not_raise_exception
    end

    it 'should not crash if theme does not specify toc_indent' do
      (expect do
        pdf = to_pdf <<~'END', attributes: { 'pdf-theme' => (fixture_file 'custom-theme.yml') }, analyze: true
        = Document Title
        :toc:

        == Section
        END

        toc_text = pdf.find_unique_text %r/Table of Contents/
        (expect toc_text).not_to be_nil
        (expect toc_text[:font_name]).to eql 'Times-Roman'
      end).to not_raise_exception
    end

    it 'should allow hanging indent to be applied to lines that wrap' do
      pdf = to_pdf <<~'END', doctype: :book, pdf_theme: { toc_hanging_indent: 36 }, analyze: true
      = Document Title
      :toc:

      == This Here is an Absurdly Long Section Title That Exceeds the Length of a Single Line and Therefore Wraps

      content
      END

      toc_text = pdf.find_text page_number: 2
      (expect toc_text.size).to be > 1
      (expect toc_text[1][:string]).to eql 'This Here is an Absurdly Long Section Title That Exceeds the Length of a Single Line and'
      (expect toc_text[2][:string]).to eql 'Therefore Wraps'
      (expect toc_text[2][:x]).to be > toc_text[1][:x]
      dot_leader_text = (pdf.find_text page_number: 2).select {|it| it[:string].start_with? '.' }
      (expect dot_leader_text).not_to be_empty
      (expect dot_leader_text[0][:y]).to be < toc_text[1][:y]
      page_number_text = pdf.find_text page_number: 2, string: '1'
      (expect page_number_text).to have_size 1
    end

    it 'should allow theme to control font size of dot leader' do
      pdf = to_pdf <<~'END', pdf_theme: { toc_dot_leader_font_size: '0.5em' }, analyze: true
      = Book Title
      :doctype: book
      :toc:

      == Foo

      == Bar
      END

      reference_text = pdf.find_unique_text 'Foo', page_number: 2
      dot_leader_texts = pdf.find_text %r/(?:\. )+/, page_number: 2
      (expect dot_leader_texts).not_to be_empty
      dot_leader_texts.each do |text|
        (expect text[:font_size]).to eql (reference_text[:font_size] * 0.5)
      end
    end

    it 'should allow theme to control font style of dot leader' do
      pdf = to_pdf <<~'END', pdf_theme: { toc_dot_leader_font_style: 'bold' }, analyze: true
      = Book Title
      :doctype: book
      :toc:

      == Foo
      END

      dot_leader_text = pdf.find_unique_text %r/(?:\. )+/
      (expect dot_leader_text[:font_name]).to eql 'NotoSerif-Bold'
    end

    it 'should allow theme to disable dot leader by setting content to empty string' do
      pdf = to_pdf <<~'END', pdf_theme: { toc_dot_leader_content: '' }, analyze: true
      = Book Title
      :doctype: book
      :toc:

      == Foo

      == Bar

      == Baz
      END

      toc_lines = (pdf.lines pdf.find_text page_number: 2).join ?\n
      (expect toc_lines).to include 'Foo'
      (expect toc_lines).to include 'Bar'
      (expect toc_lines).to include 'Baz'
      (expect toc_lines).not_to include '.'
    end

    it 'should allow theme to disable dot leader by setting levels to none' do
      pdf = to_pdf <<~'END', pdf_theme: { toc_dot_leader_levels: 'none' }, analyze: true
      = Book Title
      :doctype: book
      :toc:

      == Foo

      == Bar

      == Baz
      END

      toc_lines = (pdf.lines pdf.find_text page_number: 2).join ?\n
      (expect toc_lines).to include 'Foo'
      (expect toc_lines).to include 'Bar'
      (expect toc_lines).to include 'Baz'
      (expect toc_lines).not_to include '.'
    end

    it 'should allow theme to disable dot leader for nested levels' do
      pdf = to_pdf <<~'END', pdf_theme: { toc_dot_leader_levels: 1 }, analyze: true
      = Book Title
      :doctype: book
      :toc:

      == Foo

      === Foo Subsection

      == Bar

      === Bar Subsection

      == Baz

      === Baz Subsection
      END

      toc_lines = pdf.lines.select {|it| it.include? 'Subsection' }.join ?\n
      (expect toc_lines).to include 'Foo Subsection'
      (expect toc_lines).to include 'Bar Subsection'
      (expect toc_lines).to include 'Baz Subsection'
      (expect toc_lines).not_to include '.'
    end

    it 'should allow theme to enable dot leader per level' do
      pdf = to_pdf <<~'END', pdf_theme: { toc_dot_leader_levels: '1 3' }, analyze: true
      = Book Title
      :doctype: book
      :toc:
      :toclevels: 3

      == Foo Top

      === Foo Subsection

      ==== Foo Deep

      == Bar Top

      === Bar Subsection

      ==== Bar Deep

      == Baz Top

      === Baz Subsection

      ==== Baz Deep
      END

      lines = pdf.lines

      main_section_toc_lines = lines.select {|it| it.include? 'Top' }.join ?\n
      (expect main_section_toc_lines).to include 'Foo Top'
      (expect main_section_toc_lines).to include 'Bar Top'
      (expect main_section_toc_lines).to include 'Baz Top'
      (expect main_section_toc_lines).to include '.'

      subsection_toc_lines = lines.select {|it| it.include? 'Subsection' }.join ?\n
      (expect subsection_toc_lines).to include 'Foo Subsection'
      (expect subsection_toc_lines).to include 'Bar Subsection'
      (expect subsection_toc_lines).to include 'Baz Subsection'
      (expect subsection_toc_lines).not_to include '.'

      deep_section_toc_lines = lines.select {|it| it.include? 'Deep' }.join ?\n
      (expect deep_section_toc_lines).to include 'Foo Deep'
      (expect deep_section_toc_lines).to include 'Bar Deep'
      (expect deep_section_toc_lines).to include 'Baz Deep'
      (expect deep_section_toc_lines).to include '.'
    end

    it 'should not use part or chapter signifier in toc' do
      pdf = to_pdf <<~'END', analyze: true
      = Book Title
      :doctype: book
      :sectnums:
      :partnums:
      :toc:

      = P1

      == C1

      = P2

      == C2
      END

      lines = pdf.lines pdf.find_text page_number: 2
      (expect lines).to have_size 5
      (expect lines[0]).to eql 'Table of Contents'
      (expect lines[1]).to start_with 'I: P1'
      (expect lines[3]).to start_with 'II: P2'
      (expect pdf.find_text 'Part I: P1').to have_size 1
      (expect lines[2]).to start_with '1. C1'
      (expect lines[4]).to start_with '2. C2'
      (expect pdf.find_text 'Chapter 1. C1').to have_size 1
    end

    it 'should reserve enough room for toc when page number forces section title in toc to wrap' do
      pdf = to_pdf <<~END, analyze: true
      = Document Title
      :doctype: book
      :notitle:
      :toc:

      #{(['== Chapter'] * 9).join ?\n}

      == This is a very long section title that wraps in the table of contents when the page number is added

      #{(['== Chapter'] * 27).join ?\n}

      == Last Chapter
      END

      (expect pdf.find_text page_number: 2, string: 'Last Chapter').to have_size 1
      (expect pdf.find_text page_number: 2, string: 'Chapter').to be_empty
    end
  end

  context 'article' do
    it 'should not generate toc by default' do
      pdf = to_pdf <<~'END', analyze: true
      = Document Title

      == Introduction

      == Main

      == Conclusion
      END
      (expect pdf.pages).to have_size 1
      (expect pdf.find_text 'Table of Contents').to be_empty
    end

    it 'should insert toc between document title and content when toc is set' do
      lorem = ['lorem ipsum'] * 10 * %(\n\n)
      input = <<~END
      = Document Title
      :toc:

      Preamble

      == Introduction

      #{lorem}

      == Main

      #{lorem}

      == Conclusion

      #{lorem}
      END
      pdf = to_pdf input, analyze: true
      (expect pdf.pages).to have_size 2
      (expect pdf.find_text 'Table of Contents', page_number: 1).to have_size 1
      (expect pdf.find_text 'Introduction', page_number: 1).to have_size 2
      doctitle_text = (pdf.find_text 'Document Title')[0]
      toc_title_text = (pdf.find_text 'Table of Contents')[0]
      toc_bottom_text = (pdf.find_text '2')[0]
      content_top_text = (pdf.find_text 'Preamble')[0]
      (expect doctitle_text[:y]).to be > toc_title_text[:y]
      (expect toc_title_text[:y]).to be > content_top_text[:y]
      (expect toc_bottom_text[:y]).to be > content_top_text[:y]
      # NOTE: assert there's no excess gap between end of toc and start of content
      (expect toc_bottom_text[:y] - content_top_text[:y]).to be < 35
    end

    it 'should insert toc at top of first page if toc is set and document has no doctitle' do
      pdf = to_pdf <<~'END', analyze: true
      :toc:

      == Section A

      == Section B
      END

      toc_title_text = (pdf.find_text 'Table of Contents')[0]
      sect_a_text = (pdf.find_text 'Section A', font_size: 22)[0]
      (expect toc_title_text[:y]).to be > sect_a_text[:y]
    end

    it 'should reserve enough pages for toc if it spans more than one page' do
      sections = (1..40).map {|num| %(\n\n== Section #{num}) }
      input = <<~END
      = Document Title
      :toc:

      #{sections.join}
      END
      pdf = to_pdf input, analyze: :page
      (expect pdf.pages).to have_size 4
      (expect pdf.pages[0][:strings]).to include 'Document Title'
      (expect pdf.pages[0][:strings]).to include 'Table of Contents'
      (expect pdf.pages[0][:strings]).not_to include 'Section 40'
      (expect pdf.pages[1][:strings]).to include 'Section 40'
      (expect pdf.pages[1][:strings]).to include 'Section 1'
      pdf = to_pdf input, analyze: true
      text = pdf.text
      idx_toc_bottom = nil
      idx_content_top = nil
      text.each_with_index do |candidate, idx|
        idx_toc_bottom = idx if candidate[:string] == 'Section 40' && candidate[:font_size] == 10.5
        idx_content_top = idx if candidate[:string] == 'Section 1' && candidate[:font_size] == 22
      end
      (expect text[idx_toc_bottom][:y]).to be > text[idx_content_top][:y]
      # NOTE: assert there's no excess gap between end of toc and start of content
      (expect text[idx_toc_bottom][:y] - text[idx_content_top][:y]).to be < 50
    end

    it 'should insert toc between title page and first page of body when toc and title-page are set' do
      pdf = to_pdf <<~'END', analyze: :page
      = Document Title
      :toc:
      :title-page:

      == Introduction

      == Main

      == Conclusion
      END
      (expect pdf.pages).to have_size 3
      (expect pdf.pages[0][:strings]).to include 'Document Title'
      (expect pdf.pages[1][:strings]).to include 'Table of Contents'
      (expect pdf.pages[1][:strings]).to include '1'
      (expect pdf.pages[1][:strings]).not_to include '2'
      (expect pdf.pages[2][:strings]).to include 'Introduction'
    end

    it 'should not force page break after toc when title-page attribute is set and toc-break-after is auto' do
      pdf = to_pdf <<~'END', pdf_theme: { toc_break_after: 'auto' }, analyze: true
      = Document Title
      :title-page:
      :toc:

      == Introduction

      == Main

      == Conclusion
      END
      (expect pdf.pages).to have_size 2
      (expect pdf.find_unique_text 'Document Title', page_number: 1).not_to be_nil
      (expect pdf.find_unique_text 'Table of Contents', page_number: 2).not_to be_nil
      (expect pdf.find_unique_text 'Introduction', page_number: 2, font_name: 'NotoSerif-Bold').not_to be_nil
      (expect pdf.find_unique_text 'Conclusion', page_number: 2, font_name: 'NotoSerif-Bold').not_to be_nil
      (expect (pdf.find_unique_text 'Introduction', page_number: 2, font_name: 'NotoSerif-Bold')[:y]).to be <
        (pdf.find_unique_text 'Conclusion', page_number: 2, font_name: 'NotoSerif')[:y]
    end

    it 'should insert toc at location of toc macro if toc attribute is macro' do
      lorem = ['lorem ipsum'] * 10 * %(\n\n)
      input = <<~END
      = Document Title
      :toc: macro

      Preamble

      == Introduction

      #{lorem}

      toc::[]

      == Main

      #{lorem}

      == Conclusion

      #{lorem}
      END
      pdf = to_pdf input, analyze: true
      (expect pdf.pages).to have_size 2
      (expect pdf.find_text 'Table of Contents', page_number: 1).to have_size 1
      (expect pdf.find_text 'Introduction', page_number: 1).to have_size 2
      doctitle_text = (pdf.find_text 'Document Title')[0]
      toc_title_text = (pdf.find_text 'Table of Contents')[0]
      toc_bottom_text = (pdf.find_text '2')[0]
      content_top_text = (pdf.find_text 'Preamble')[0]
      intro_title_text = (pdf.find_text 'Introduction')[0]
      (expect doctitle_text[:y]).to be > toc_title_text[:y]
      (expect toc_title_text[:y]).to be < content_top_text[:y]
      (expect toc_bottom_text[:y]).to be < content_top_text[:y]
      (expect toc_title_text[:y]).to be < intro_title_text[:y]

      pdf = to_pdf input
      outline = extract_outline pdf
      (expect outline).to have_size 5
      (expect outline.map {|it| it[:title] }).to eql ['Document Title', 'Introduction', 'Table of Contents', 'Main', 'Conclusion']
      toc_dest = outline[2][:dest]
      (expect toc_dest[:pagenum]).to be 1
      (expect toc_dest[:label]).to eql '1'
      (expect toc_dest[:y]).to be < 800
    end

    it 'should insert macro toc in outline as sibling of section in which it is contained' do
      pdf = to_pdf <<~'END'
      = Document Title
      :toc: macro

      == Section

      === Subsection

      toc::[]

      === Another Subsection
      END

      outline = extract_outline pdf
      section_entry = outline[1]
      (expect section_entry).not_to be_nil
      (expect section_entry[:title]).to eql 'Section'
      section_entry_children = section_entry[:children]
      (expect section_entry_children).to have_size 3
      toc_entry = section_entry_children[1]
      (expect toc_entry[:title]).to eql 'Table of Contents'
      (expect toc_entry[:dest][:label]).to eql '1'
    end

    it 'should insert macro toc in outline before other sections if macro proceeds sections' do
      pdf = to_pdf <<~'END'
      = Document Title
      :toc: macro

      toc::[]

      == Section

      === Subsection

      === Another Subsection
      END

      outline = extract_outline pdf
      toc_entry = outline[1]
      (expect toc_entry[:title]).to eql 'Table of Contents'
      (expect toc_entry[:dest][:label]).to eql '1'
    end
  end

  it 'should apply consistent font color to running content when base font color is unset', visual: true do
    pdf_theme = {
      extends: 'base',
      base_font_color: nil,
      header_height: 36,
      header_font_color: '0000FF',
      header_columns: '0% =100% 0%',
      header_recto_center_content: 'header text',
      header_verso_center_content: 'header text',
      toc_dot_leader_font_color: 'CCCCCC',
      running_content_start_at: 'toc',
    }
    to_file = to_pdf_file <<~'END', 'toc-running-content-font-color.pdf', pdf_theme: pdf_theme
    = Document Title
    Author Name
    :doctype: book
    :toc:

    == A

    text

    == B

    text
    END

    (expect to_file).to visually_match 'toc-running-content-font-color.pdf'
  end

  it 'should use same font color for text and dot leader if dot leader font color is unspecified' do
    pdf_theme = {
      extends: 'base',
      toc_font_color: '4a4a4a',
    }
    pdf = to_pdf <<~'END', pdf_theme: pdf_theme, analyze: true
    = Document Title
    Author Name
    :doctype: book
    :toc:

    == Intro

    text

    == Conclusion

    text
    END

    intro_entry_font_color = (pdf.find_unique_text 'Intro', page_number: 2)[:font_color]
    dot_leader_font_color = (pdf.find_text page_number: 2).select {|it| it[:string].start_with? '.' }.map {|it| it[:font_color] }.uniq[0]
    (expect dot_leader_font_color).to eql intro_entry_font_color
  end

  it 'should not apply bold to italic text if headings are bold in theme' do
    pdf_theme = {
      toc_font_style: 'bold',
    }

    pdf = to_pdf <<~'END', pdf_theme: pdf_theme, analyze: true
    = Document Title
    :doctype: book
    :toc:

    == Get Started _Quickly_
    END

    get_started_text = (pdf.find_text page_number: 2, string: /^Get Started/)[0]
    quickly_text = (pdf.find_text page_number: 2, string: 'Quickly')[0]
    (expect get_started_text[:font_name]).to eql 'NotoSerif-Bold'
    (expect quickly_text[:font_name]).to eql 'NotoSerif-Italic'
  end

  it 'should allow theme to specify text decoration for entries in toc' do
    pdf_theme = {
      toc_text_decoration: 'underline',
    }
    input = <<~'END'
    = Document Title
    :toc:
    :title-page:

    == Underline Me
    END

    pdf = to_pdf input, pdf_theme: pdf_theme, analyze: :line
    lines = pdf.lines
    (expect lines).to have_size 1
    toc_entry_underline = lines[0]
    pdf = to_pdf input, pdf_theme: pdf_theme, analyze: true
    toc_entry_text = (pdf.find_text page_number: 2, string: 'Underline Me')[0]
    (expect toc_entry_underline[:from][:x]).to eql toc_entry_text[:x]
    (expect toc_entry_underline[:from][:y]).to be_within(2).of(toc_entry_text[:y])
    (expect toc_entry_underline[:color]).to eql toc_entry_text[:font_color]
  end

  it 'should allow theme to specify color and width of text decoration for entries in toc' do
    pdf_theme = {
      toc_text_decoration: 'underline',
      toc_text_decoration_color: 'cccccc',
      toc_text_decoration_width: 0.5,
    }
    input = <<~'END'
    = Document Title
    :toc:
    :title-page:

    == Underline Me
    END

    pdf = to_pdf input, pdf_theme: pdf_theme, analyze: :line
    lines = pdf.lines
    (expect lines).to have_size 1
    toc_entry_underline = lines[0]
    (expect toc_entry_underline[:color]).to eql 'CCCCCC'
    (expect toc_entry_underline[:width]).to eql 0.5
  end

  it 'should use fallback value to align toc title if alignment not specified in theme' do
    [
      {
        toc_title_text_align: 'center',
        heading_h2_text_align: 'left',
        heading_text_align: 'left',
      },
      {
        toc_title_text_align: nil,
        heading_h2_text_align: 'center',
        heading_text_align: 'left',
      },
      {
        toc_title_text_align: nil,
        heading_h2_text_align: nil,
        heading_text_align: 'center',
      },
      {
        toc_title_text_align: nil,
        heading_h2_text_align: nil,
        heading_text_align: nil,
        base_text_align: 'center',
      },
    ].each do |pdf_theme|
      pdf = to_pdf <<~'END', pdf_theme: pdf_theme, analyze: true
      = Document Title
      :toc:
      :doctype: book

      == Section A

      == Section B
      END
      toc_title_text = pdf.find_unique_text 'Table of Contents'
      (expect toc_title_text[:x]).to be > 48.24
    end
  end

  it 'should allow theme to specify text decoration per heading level in toc' do
    pdf_theme = {
      toc_h3_text_decoration: 'underline',
      toc_h3_text_decoration_color: 'cccccc',
      toc_h3_text_decoration_width: 0.5,
    }
    input = <<~'END'
    = Document Title
    :toc:
    :title-page:

    == Plain Title

    === Decorated Title
    END

    pdf = to_pdf input, pdf_theme: pdf_theme, analyze: :line
    lines = pdf.lines
    pdf = to_pdf input, pdf_theme: pdf_theme, analyze: true
    underlined_toc_entry_text = pdf.find_unique_text 'Decorated Title', page_number: 2
    (expect lines).to have_size 1
    toc_entry_underline = lines[0]
    (expect toc_entry_underline[:color]).to eql 'CCCCCC'
    (expect toc_entry_underline[:width]).to eql 0.5
    (expect toc_entry_underline[:from][:y]).to be < underlined_toc_entry_text[:y]
    (expect toc_entry_underline[:from][:y]).to be_within(2).of(underlined_toc_entry_text[:y])
  end

  it 'should allow theme to specify text transform for entries in toc' do
    pdf_theme = {
      toc_text_transform: 'uppercase',
    }
    input = <<~'END'
    = Document Title
    :doctype: book
    :toc:

    == Transform Me
    END

    pdf = to_pdf input, pdf_theme: pdf_theme, analyze: true
    toc_lines = pdf.lines pdf.find_text page_number: 2
    (expect toc_lines.join ?\n).to include 'TRANSFORM ME'
  end

  it 'should decode character references in toc entries' do
    pdf = to_pdf <<~'END', analyze: true
    = Document Title
    :toc:

    == Paper Clips &#x2116;&nbsp;4
    END

    (expect pdf.find_text %(Paper Clips \u2116\u00a04)).to have_size 2
  end

  it 'should not crash if section title is empty' do
    pdf = to_pdf <<~'END', analyze: true
    :toc:

    == {empty}

    content
    END

    (expect pdf.text).to have_size 2
    (expect pdf.find_unique_text 'content').not_to be_nil
    (expect pdf.find_unique_text 'Table of Contents').not_to be_nil
  end

  it 'should not include section title or its children in toc if title is empty' do
    pdf = to_pdf <<~'END', analyze: true
    = Document Title
    :toc:
    :title-page:

    == First Section

    == {empty}

    === Child Section

    content
    END

    toc_lines = pdf.lines pdf.find_text page_number: 2
    (expect toc_lines).to have_size 2
    (expect toc_lines[0]).to eql 'Table of Contents'
    (expect toc_lines[1]).to start_with 'First Section'
  end

  it 'should allocate correct number of pages for toc if line numbers cause lines to wrap' do
    chapter_title = %(\n\n== A long chapter title that wraps to a second line in the toc when the page number exceeds one digit)

    input = <<~END
    = Document Title
    :doctype: book
    :toc:
    :nofooter:
    #{chapter_title * 38}
    END

    pdf = to_pdf input, analyze: true
    last_pagenum_text = (pdf.find_text '38')[0]
    first_chapter_text = (pdf.find_text font_name: 'NotoSerif-Bold', font_size: 22, string: /^A long chapter title/)[0]
    (expect first_chapter_text[:page_number]).to be last_pagenum_text[:page_number].next
  end

  it 'should render image at end of section title in toc entry' do
    pdf = to_pdf <<~'END', analyze: :image
    = Document Title
    :doctype: book
    :toc:

    == Chapter image:tux.png[,16]
    END

    images = pdf.images
    (expect images).to have_size 2
    (expect images[0][:page_number]).to be 2
    (expect images[1][:page_number]).to be 3
    (expect images[0][:data]).to eql images[1][:data]
    (expect images[0][:width]).to eql images[1][:width]
  end

  it 'should remove links from entries in toc but leave behind linked text' do
    pdf = to_pdf <<~'END'
    = Document Title
    :doctype: book
    :toc:

    [#ch1]
    == https://example.org[Once] Upon a https://example.com[Time]

    [#ch2]
    == Continuing What <<ch1>> Started
    END

    (expect pdf.pages).to have_size 4
    toc_lines = ((pdf.page 2).text.split ?\n).reject(&:empty?)
    (expect toc_lines).to have_size 3
    (expect toc_lines[0]).to eql 'Table of Contents'
    (expect toc_lines[1]).to start_with 'Once Upon a Time'
    (expect toc_lines[2]).to start_with 'Continuing What Once Upon a Time Started'
    annots = get_annotations pdf, 2
    (expect annots).to have_size 4
    (expect annots.map {|it| it[:Dest] }.sort).to eql %w(ch1 ch1 ch2 ch2)
  end

  it 'should allow extended converter to insert extra page before toc' do
    backend = nil
    create_class (Asciidoctor::Converter.for 'pdf') do
      register_for (backend = %(pdf#{object_id}).to_sym)
      def ink_toc doc, num_levels, toc_page_number, start_cursor, num_front_matter_pages = 0
        go_to_page toc_page_number unless (page_number == toc_page_number) || scratch?
        unless scratch?
          theme_font :heading, level: 2 do
            ink_heading 'Extra', level: 2
          end
          go_to_page page_number + 1
        end
        offset = 1
        toc_page_numbers = super doc, num_levels, (toc_page_number + offset), start_cursor, num_front_matter_pages
        scratch? ? ((toc_page_numbers.begin - offset)..toc_page_numbers.end) : toc_page_numbers
      end
    end

    input = <<~'END'
    = Document Title
    :doctype: book
    :toc:

    == Chapter A

    == Chapter B
    END

    pdf = to_pdf input, backend: backend, analyze: true
    (expect pdf.pages).to have_size 5
    [['Extra', 2], ['Table of Contents', 3], ['Chapter A', 4]].each do |title, pnum|
      title_text = (pdf.find_text title)[-1]
      (expect title_text[:page_number]).to eql pnum
    end
    (expect (pdf.find_text 'Chapter A')[0][:page_number]).to eql 3
    (expect (pdf.find_text 'Chapter B')[0][:page_number]).to eql 3
  end

  it 'should allow extended converter to insert extra entries into TOC' do
    backend = nil
    create_class (Asciidoctor::Converter.for 'pdf') do
      register_for (backend = %(pdf#{object_id}).to_sym)
      def get_entries_for_toc node
        return super if node.context == :document
        node.blocks.select(&:id)
      end
    end

    input = <<~'END'
    = Document Title
    :doctype: book
    :toc:

    == Chapter A

    .Check for Ruby
    [#check-for-ruby]
    ****
    Run `ruby -v` to check if you have Ruby installed.
    ****

    <<<

    === Section

    == Chapter B

    [#screenshot,reftext=Screenshot]
    image::tux.png[]

    === Another Section
    END

    pdf = to_pdf input, backend: backend, analyze: true
    (expect pdf.pages).to have_size 5
    toc_text = (pdf.find_text page_number: 2).reject {|it| it[:string] == 'Table of Contents' }
    toc_lines = pdf.lines toc_text
    [['Chapter A', 1], ['Check for Ruby', 1], ['Section', 2], ['Chapter B', 3], ['Screenshot', 3], ['Another Section', 3]].each_with_index do |(title, pnum), idx|
      line = toc_lines[idx]
      (expect line).to start_with title
      (expect line).to end_with pnum.to_s
    end

    pdf = to_pdf input, backend: backend
    annots = get_annotations pdf, 2
    (expect annots.select {|it| it[:Dest] == 'check-for-ruby' }).to have_size 2
    (expect annots.select {|it| it[:Dest] == 'screenshot' }).to have_size 2
  end

  it 'should allow extended converter to insert chapter per TOC' do
    source_file = doc_file 'modules/extend/examples/pdf-converter-chapter-toc.rb'
    source_lines = (File.readlines source_file).select {|l| l == ?\n || (l.start_with? ' ') }
    ext_class = create_class Asciidoctor::Converter.for 'pdf'
    backend = %(pdf#{ext_class.object_id})
    source_lines[0] = %(  register_for '#{backend}'\n)
    ext_class.class_eval source_lines.join, source_file
    pdf = to_pdf <<~'END', backend: backend, analyze: true
    = Document Title
    :doctype: book
    :toc:
    :toclevels: 1
    :chapter-toc:
    :chapter-toclevels: 2

    == Chapter Title

    === First Section

    ==== Subsection

    <<<

    === Last Section

    == Another Chapter
    END

    toc_text = pdf.find_text page_number: 2
    toc_lines = toc_text
      .sort_by {|it| -it[:y] }
      .group_by {|it| it[:y] }
      .map {|_, it| it.sort_by {|fragment| fragment[:order] }.map {|fragment| fragment[:string] }.join }
    (expect toc_lines).to have_size 3
    (expect toc_lines[0]).to eql 'Table of Contents'
    (expect toc_lines[1]).to start_with 'Chapter Title'
    (expect toc_lines[1]).to end_with '1'
    (expect toc_lines[2]).to start_with 'Another Chapter'
    (expect toc_lines[2]).to end_with '3'

    ch1_text = pdf.find_text page_number: 3
    ch1_lines = ch1_text
      .sort_by {|it| -it[:y] }
      .group_by {|it| it[:y] }
      .map {|_, it| it.sort_by {|fragment| fragment[:order] }.map {|fragment| fragment[:string] }.join }
    (expect ch1_lines).to have_size 6
    (expect ch1_lines[0]).to eql 'Chapter Title'
    (expect ch1_lines[1]).to start_with 'First Section'
    (expect ch1_lines[1]).to end_with '1'
    (expect ch1_lines[2]).to start_with 'Subsection'
    (expect ch1_lines[2]).to end_with '1'
    (expect ch1_lines[3]).to start_with 'Last Section'
    (expect ch1_lines[3]).to end_with '2'
    (expect ch1_lines[4]).to eql 'First Section'
    (expect ch1_lines[5]).to eql 'Subsection'
  end
end
