# frozen_string_literal: true

require_relative 'spec_helper'

describe 'Asciidoctor::PDF::Converter - Index' do
  it 'should not add index entries to the document if an index section is not present' do
    pdf = to_pdf <<~'EOS', analyze: true
    You can add a ((visible index entry)) to your document by enclosing it in double round brackets.
    EOS

    (expect pdf.find_text %r/visible index entry/).to have_size 1
    (expect pdf.lines).to eql ['You can add a visible index entry to your document by enclosing it in double round brackets.']
  end

  it 'should collapse space in front of hidden index terms' do
    pdf = to_pdf <<~'EOS', analyze: true
    before
    (((zen)))
    (((yin)))
    (((yang)))
    after

    foo (((bar))) baz
    EOS

    (expect pdf.lines).to eql ['before after', 'foo baz']
  end

  it 'should not indent paragraph preceded by index terms' do
    pdf = to_pdf <<~'EOS', analyze: true
    (((zen)))
    (((yin)))
    (((yang)))
    text

    reference
    EOS

    (expect pdf.lines).to eql %w(text reference)
    (expect pdf.text[0][:x]).to eql pdf.text[1][:x]
  end

  it 'should not indent paragraph preceded by index terms when using font without NULL character' do
    pdf_theme = {
      extends: 'default',
      font_catalog: {
        'Missing Null' => {
          'normal' => (fixture_file 'mplus1mn-regular-ascii.ttf'),
          'bold' => (fixture_file 'mplus1mn-regular-ascii.ttf'),
        },
      },
      base_font_family: 'Missing Null',
    }
    pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: true
    (((zen)))
    (((yin)))
    (((yang)))
    text

    reference
    EOS

    (expect pdf.lines).to eql %w(text reference)
    (expect pdf.text[0][:x]).to eql pdf.text[1][:x]
  end

  it 'should normalize space in term in body and index section' do
    pdf = to_pdf <<~'EOS', analyze: true
    ((foo
    {empty}
    bar))(((yin
    {empty}
    {empty}
    yang)))

    <<<

    [index]
    == Index
    EOS

    p1_text = pdf.find_text page_number: 1
    body_lines = pdf.lines p1_text
    (expect body_lines).to eql ['foo bar']
    p2_text = pdf.find_text page_number: 2
    index_lines = pdf.lines p2_text
    (expect index_lines).to include 'foo bar, 1'
    (expect index_lines).to include 'yin yang, 1'
  end

  it 'should not add index section if there are no index entries' do
    pdf = to_pdf <<~'EOS', analyze: true
    == About

    This document has no index entries.

    [index]
    == Index
    EOS

    (expect pdf.pages).to have_size 1
    (expect pdf.find_text 'Index').to be_empty
  end

  it 'should not include index section in TOC if index is empty' do
    pdf = to_pdf <<~'EOS'
    = Document Title
    :doctype: book
    :toc:

    == Chapter

    content

    [index]
    == Index

    [glossary]
    == Glossary
    EOS

    (expect pdf.pages).to have_size 4
    (expect (pdf.page 2).text).not_to include 'Index'
    (expect (pdf.page 2).text).to include 'Glossary'
    (expect (pdf.page 4).text).to include 'Glossary'
  end

  it 'should add the index entries to the section with the index style' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    = Document Title

    == Chapter About Cats

    We know that ((cats)) control the internet.
    But they sort of run nature too.
    (((cats,big cats,lion)))
    After all, the ((king of the jungle)) is the lion, which is a big cat.

    == Chapter About Dogs

    Cats may rule, well, everything.
    But ((dogs)) are a human's best friend.

    [index]
    == Index
    EOS

    index_text = pdf.find_text 'Index', page_number: 4, font_size: 22
    (expect index_text).to have_size 1
    category_c_text = pdf.find_text 'C', page_number: 4
    (expect category_c_text).to have_size 1
    (expect category_c_text[0][:font_name].downcase).to include 'bold'
    category_d_text = pdf.find_text 'D', page_number: 4
    (expect category_d_text).to have_size 1
    (expect category_d_text[0][:font_name].downcase).to include 'bold'
    category_k_text = pdf.find_text 'K', page_number: 4
    (expect category_k_text).to have_size 1
    (expect category_k_text[0][:font_name].downcase).to include 'bold'
    (expect (pdf.lines pdf.find_text page_number: 4).join ?\n).to eql <<~'EOS'.chomp
    Index
    C
    cats, 1
    big cats
    lion, 1
    D
    dogs, 2
    K
    king of the jungle, 1
    EOS
  end

  it 'should add index terms to index defined in section title with autogenerated ID' do
    pdf = to_pdf <<~'EOS', analyze: true
    = Guide
    :doctype: book

    == Working with ((Custom Pipelines))

    (((custom behavior)))
    Custom pipelines allow you to add custom behavior to the application.

    [index]
    == Index
    EOS

    index_title_text = pdf.find_unique_text 'Index'
    (expect index_title_text[:page_number]).to be 3
    c_text = pdf.find_unique_text 'C'
    (expect c_text[:page_number]).to be 3
    index_lines = pdf.lines pdf.find_text page_number: 3
    (expect index_lines).to include 'Custom Pipelines, 1'
    (expect index_lines).to include 'custom behavior, 1'
  end

  it 'should preserve text formatting in display of index term' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    = Document Title

    == Content

    Use the ((`return`)) keyword(((_keyword_))) to force a method to return early.

    There are cats, and then there are ((*big* cats)).

    A ((mouse _gesture_)) is a movement the software recognizes and interprets as a command.

    [index]
    = Index
    EOS

    (expect pdf.pages).to have_size 3
    return_entry_text = pdf.find_unique_text 'return', page_number: 3
    (expect return_entry_text[:font_name]).to eql 'mplus1mn-regular'
    keyword_entry_text = pdf.find_unique_text 'keyword', page_number: 3
    (expect keyword_entry_text[:font_name]).to eql 'NotoSerif-Italic'
    big_text = pdf.find_unique_text 'big', page_number: 3
    (expect big_text[:font_name]).to eql 'NotoSerif-Bold'
    gesture_text = pdf.find_unique_text 'gesture', page_number: 3
    (expect gesture_text[:font_name]).to eql 'NotoSerif-Italic'
  end

  it 'should not group term with and without formatting' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    The ((`proc`)) keyword in Ruby defines a ((proc)), which is a block of code.

    [index]
    = Index
    EOS

    (expect pdf.pages).to have_size 2
    proc_text = (pdf.find_text %r/^proc/, page_number: 2).sort_by {|it| it[:string].length }
    (expect proc_text).to have_size 2
    (expect proc_text[0][:string]).to eql 'proc'
    (expect proc_text[0][:font_name]).to eql 'mplus1mn-regular'
    (expect proc_text[1][:font_name]).not_to eql 'mplus1mn-regular'
    index_lines = pdf.lines pdf.find_text page_number: 2
    (expect index_lines).to eql ['Index', 'P', 'proc, 1', 'proc, 1']
  end

  it 'should not add index entries inside delimited block to index twice' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    = Document Title

    == Chapter about Cats

    We know that ((cats)) control the internet.
    But they sort of run nature too.
    (((cats,big cats,lion)))
    After all, the ((king of the jungle)) is the lion, which is a big cat.

    .Dogs
    ****
    Cats may rule, well, everything.
    But ((dogs)) are a (((humans)))human's best friend.
    ****

    [index]
    == Index
    EOS

    index_heading_text = pdf.find_unique_text 'Index', page_number: 3, font_size: 22
    (expect index_heading_text).not_to be_nil
    index_lines = pdf.lines pdf.find_text page_number: 3
    (expect index_lines).to include 'dogs, 1'
    (expect index_lines).to include 'humans, 1'
  end

  it 'should create link from entry in index to location of term' do
    input = <<~'EOS'
    = Document Title
    :doctype: book

    == Chapter About Dogs

    Cats may rule, well, everything.
    But ((dogs)) are a human's best friend.

    [index]
    == Index
    EOS

    pdf = to_pdf input, analyze: true
    dogs_text = (pdf.find_text 'dogs are a human’s best friend.')[0]

    pdf = to_pdf input
    annotations = get_annotations pdf, 3
    (expect annotations).to have_size 1
    dest_name = annotations[0][:Dest]
    (expect (dest = get_dest pdf, dest_name)).not_to be_nil
    (expect dest[:x]).to eql dogs_text[:x]
    (expect dest[:page_number]).to be 2
  end

  it 'should target first occurance of index term, not in xreftext' do
    pdf = to_pdf <<~'EOS', analyze: true
    = Document Title
    :doctype: book
    :notitle:

    == Install

    .((node.install))
    [[node-install]]
     $ nvm install node

    == Version

    .((node.version))
    [[node-version]]
     $ node -v

    <<node-install>>

    == Uninstall

    .((node.uninstall))
    [[node-uninstall]]
     $ nvm uninstall node

    <<node-install>>

    [index]
    == Index
    EOS

    index_pgnum = (pdf.find_text 'Index')[0][:page_number]
    index_lines = pdf.lines pdf.find_text page_number: index_pgnum
    (expect index_lines).to eql ['Index', 'N', 'node.install, 1', 'node.uninstall, 3', 'node.version, 2']
  end

  it 'should not assign number or chapter label to index section' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    = Cats & Dogs
    :sectnums:

    == Cats

    We know that ((cats)) control the internet.

    == Dogs

    Cats may rule, well, everything.
    But ((dogs)) are a human's best friend.

    [index]
    == Index
    EOS

    index_text = pdf.find_text 'Chapter 3. Index', page_number: 4
    (expect index_text).to be_empty
    index_text = pdf.find_text 'Index', page_number: 4
    (expect index_text).to have_size 1
  end

  it 'should generate anchor names for indexterms which are reproducible between runs' do
    input = <<~'EOS'
    = Cats & Dogs
    :reproducible:

    == Cats

    We know that ((cats)) control the internet.

    == Dogs

    Cats may rule, well, everything.
    But ((dogs)) are a human's best friend.

    [index]
    == Index
    EOS

    to_file_a = to_pdf_file input, 'index-reproducible-a.pdf', doctype: :book
    to_file_b = to_pdf_file input, 'index-reproducible-b.pdf', doctype: :book
    (expect FileUtils.compare_file to_file_a, to_file_b).to be true
  end

  it 'should not automatically promote nested index terms' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    = Document Title

    == Big Cats

    (((cats,big cats)))
    (((cats,big cats,lion)))
    The king of the jungle is the lion, which is a big cat.

    [index]
    == Index
    EOS

    category_c_text = pdf.find_text 'C', page_number: 3
    (expect category_c_text).to have_size 1
    (expect category_c_text[0][:font_name].downcase).to include 'bold'
    category_b_text = pdf.find_text 'B', page_number: 3
    (expect category_b_text).to be_empty
    category_l_text = pdf.find_text 'L', page_number: 3
    (expect category_l_text).to be_empty
    (expect (pdf.lines pdf.find_text page_number: 3).join ?\n).to eql <<~'EOS'.chomp
    Index
    C
    cats
    big cats, 1
    lion, 1
    EOS
  end

  it 'should ignore empty list of terms' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    Not worth indexing.
    indexterm:[ ]

    [index]
    == Index
    EOS

    (expect pdf.pages).to have_size 1
    (expect pdf.find_text 'Index').to be_empty
  end

  it 'should not group terms with different casing' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    = Document Title

    ((This)) is not the same as ((this)) or ((that)).

    [index]
    == Index
    EOS

    (expect (pdf.lines pdf.find_text page_number: 3).join ?\n).to eql <<~'EOS'.chomp
    Index
    T
    that, 1
    This, 1
    this, 1
    EOS
  end

  it 'should sort capitalized terms ahead of non-capitalized terms' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    = Document Title

    ((O.A.R.)) is a band, whereas ((oar)) is something you use to propel a boat.

    [index]
    == Index
    EOS

    (expect (pdf.lines pdf.find_text page_number: 3).join ?\n).to eql <<~'EOS'.chomp
    Index
    O
    O.A.R., 1
    oar, 1
    EOS
  end

  it 'should group index entries that start with symbol under symbol category' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    = Document Title

    == Symbols

    Use the ((@Transactional)) annotation to mark a bean as transactional.

    Use the ((&#169;)) symbol to indicate the copyright.

    [index]
    == Index
    EOS

    (expect (pdf.lines pdf.find_text page_number: 3).join ?\n).to eql <<~'EOS'.chomp
    Index
    @
    @Transactional, 1
    ©, 1
    EOS
  end

  it 'should handle XML special chars in index term' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    Put styles in the ((<head>)) tag.

    <<<

    XML special chars(((XML special chars,<)))(((XML special chars,>)))(((XML special chars,&)))

    [index]
    == Index
    EOS

    (expect (pdf.lines pdf.find_text page_number: 3).join ?\n).to eql <<~'EOS'.chomp
    Index
    @
    <head>, 1
    X
    XML special chars
    &, 2
    <, 2
    >, 2
    EOS
  end

  it 'should not put letters outside of ASCII charset in symbol category' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    = Document Title

    == Unicode Party

    ((étudier)) means to study.

    Use a ((λ)) to define a lambda function.

    [index]
    == Index
    EOS

    (expect (pdf.lines pdf.find_text page_number: 3).join ?\n).to eql <<~'EOS'.chomp
    Index
    É
    étudier, 1
    Λ
    λ, 1
    EOS
  end

  it 'should sort terms in index, ignoring case' do
    pdf = to_pdf <<~'EOS', analyze: true
    = Document Title
    :doctype: book

    == Chapter A

    ((AsciiDoc)) is a lightweight markup language.
    It is used for content ((authoring)).

    == Chapter B

    ((Asciidoctor)) is an AsciiDoc processor.

    == Chapter C

    If an element has an ((anchor)), you can link to it.

    [index]
    == Index
    EOS

    index_pagenum = (pdf.find_text 'Index')[0][:page_number]
    index_page_lines = pdf.lines pdf.find_text page_number: index_pagenum
    terms = index_page_lines.select {|it| it.include? ',' }.map {|it| (it.split ',', 2)[0] }
    (expect terms).to eql %w(anchor AsciiDoc Asciidoctor authoring)
  end

  it 'should adjust start page number for prepress book when page numbering starts at toc and toc macro is used' do
    pdf = to_pdf <<~'EOS', pdf_theme: { page_numbering_start_at: 'toc' }, analyze: true
    = Book Title
    :doctype: book
    :media: prepress
    :toc: macro

    [dedication]
    = Dedication

    Credit where credit is due.

    toc::[]

    == First Chapter

    ((apples))

    == Second Chapter

    ((bananas))

    [index]
    == Index
    EOS

    index_title_text = (pdf.find_text 'Index')[-1]
    (expect index_title_text[:page_number]).to be 11
    index_lines = pdf.lines pdf.find_text page_number: 11
    (expect index_lines).to eql ['Index', 'A', 'apples, 3', 'B', 'bananas, 5']
  end

  it 'should adjust start page number for prepress book when page numbering starts after-toc and toc macro is used' do
    pdf = to_pdf <<~'EOS', pdf_theme: { page_numbering_start_at: 'after-toc' }, analyze: true
    = Book Title
    :doctype: book
    :media: prepress
    :toc: macro

    toc::[]

    == First Chapter

    ((apples))

    == Second Chapter

    ((bananas))

    [index]
    == Index
    EOS

    index_title_text = (pdf.find_text 'Index')[-1]
    (expect index_title_text[:page_number]).to be 9
    index_lines = pdf.lines pdf.find_text page_number: 9
    (expect index_lines).to eql ['Index', 'A', 'apples, 1', 'B', 'bananas, 3']
  end

  it 'should start with no categories' do
    index = Asciidoctor::PDF::IndexCatalog.new
    (expect index).to be_empty
  end

  it 'should initiate category with no terms' do
    index = Asciidoctor::PDF::IndexCatalog.new
    index.init_category 'C'
    (expect (index.find_category 'C').terms).to be_empty
  end

  it 'should sort arabic page numbers in index term numerically' do
    index = Asciidoctor::PDF::IndexCatalog.new
    term_name = FormattedString.new [{ text: 'monkey' }]
    [11, 10, 100].each do |pgnum|
      index.store_primary_term term_name, { anchor: (anchor_name = index.next_anchor_name) }
      index.link_dest_to_page anchor_name, pgnum
    end
    monkey_term = index.find_primary_term 'monkey'
    (expect monkey_term.dests.map {|it| it[:page] }).to eql %w(10 11 100)
  end

  it 'should sort roman page numbers in index term numerically' do
    index = Asciidoctor::PDF::IndexCatalog.new
    index.start_page_number = 101
    term_name = FormattedString.new [{ text: 'monkey' }]
    [11, 10, 100].each do |pgnum|
      index.store_primary_term term_name, { anchor: (anchor_name = index.next_anchor_name) }
      index.link_dest_to_page anchor_name, pgnum
    end
    monkey_term = index.find_primary_term 'monkey'
    (expect monkey_term.dests.map {|it| it[:page] }).to eql %w(x xi c)
  end

  it 'should sort mixed page numbers in index term numerically' do
    index = Asciidoctor::PDF::IndexCatalog.new
    index.start_page_number = 101
    term_name = FormattedString.new [{ text: 'monkey' }]
    [11, 10, 100, 101].each do |pgnum|
      index.store_primary_term term_name, { anchor: (anchor_name = index.next_anchor_name) }
      index.link_dest_to_page anchor_name, pgnum
    end
    monkey_term = index.find_primary_term 'monkey'
    (expect monkey_term.dests.map {|it| it[:page] }).to eql %w(x xi c 1)
  end

  it 'should not combine deduplicate page numbers if same index entry occurs on the same page when media is screen' do
    input = <<~'EOS'
    = Document Title

    == First Chapter

    ((coming soon))

    == Second Chapter

    ((coming soon))

    ((coming soon)), no really

    == Third Chapter

    ((coming soon)) means ((coming soon))

    [index]
    == Index
    EOS

    pdf = to_pdf input, doctype: :book, analyze: true
    (expect (pdf.lines pdf.find_text page_number: 5).join ?\n).to include 'coming soon, 1, 2, 2, 3, 3'
    pdf = to_pdf input, doctype: :book
    annots = get_annotations pdf, 5
    (expect annots).to have_size 5
    (expect annots.uniq {|it| it[:Dest] }).to have_size 5
  end

  it 'should not combine range if same index entry occurs on sequential pages when media is screen' do
    pdf = to_pdf <<~'EOS', doctype: :book, analyze: true
    = Document Title

    == First Chapter

    ((coming soon))

    == Second Chapter

    ((coming soon))

    == Third Chapter

    ((coming soon))

    [index]
    == Index
    EOS

    (expect (pdf.lines pdf.find_text page_number: 5).join ?\n).to include 'coming soon, 1, 2, 3'
  end

  it 'should remove duplicate sequential pages and link to first occurrence when index-pagenum-sequence-style is page' do
    input = <<~'EOS'
    = Document Title
    :doctype: book
    :index-pagenum-sequence-style: page

    == First Chapter

    ((coming soon)) ((almost here))

    ((coming soon)), really

    == Second Chapter

    ((coming soon)) ((in draft))

    I promise, ((coming soon))

    == Third Chapter

    ((coming soon)) ((almost here))

    [index]
    == Index
    EOS

    pdf = to_pdf input, analyze: true
    index_lines = pdf.lines pdf.find_text page_number: 5
    (expect index_lines).to include 'coming soon, 1, 2, 3'
    (expect index_lines).to include 'in draft, 2'
    (expect index_lines).to include 'almost here, 1, 3'
    pdf = to_pdf input
    annots = get_annotations pdf, 5
    (expect annots).to have_size 6
    [[2, 2], [3, 3], [4, 4]].each do |idx, expected_pagenum|
      dest = get_dest pdf, annots[idx][:Dest]
      (expect dest[:page_number]).to eql expected_pagenum
    end
  end

  it 'should combine range if same index entry occurs on sequential pages when index-pagenum-sequence-style is range' do
    input = <<~'EOS'
    = Document Title
    :doctype: book
    :index-pagenum-sequence-style: range

    == First Chapter

    ((coming soon)) ((almost here))

    == Second Chapter

    ((coming soon)) ((in draft))

    == Third Chapter

    ((coming soon)) ((almost here))

    [index]
    == Index
    EOS

    pdf = to_pdf input, analyze: true
    index_lines = pdf.lines pdf.find_text page_number: 5
    (expect index_lines).to include 'coming soon, 1-3'
    (expect index_lines).to include 'in draft, 2'
    (expect index_lines).to include 'almost here, 1, 3'
    pdf = to_pdf input
    annots = get_annotations pdf, 5
    (expect annots).to have_size 4
    coming_soon_dest = get_dest pdf, annots[2][:Dest]
    (expect coming_soon_dest[:page_number]).to eql 2
  end

  it 'should combine range if same index entry occurs on sequential pages when media is not screen' do
    pdf = to_pdf <<~'EOS', doctype: :book, attribute_overrides: { 'media' => 'print' }, analyze: true
    = Document Title

    == First Chapter

    ((coming soon)) ((almost here))

    == Second Chapter

    ((coming soon)) ((in draft))

    == Third Chapter

    ((coming soon)) ((almost here))

    [index]
    == Index
    EOS

    index_lines = pdf.lines pdf.find_text page_number: 5
    (expect index_lines).to include 'coming soon, 1-3'
    (expect index_lines).to include 'in draft, 2'
    (expect index_lines).to include 'almost here, 1, 3'
  end

  it 'should not enclose page number in link or apply link styles if media is not screen' do
    pdf = to_pdf <<~'EOS', doctype: :book, attribute_overrides: { 'media' => 'print', 'pdf-theme' => 'default' }, analyze: true
    ((index term))

    [index]
    == Index
    EOS

    index_term_text = pdf.find_unique_text %r/^index term/, page_number: 2
    (expect index_term_text[:string]).to eql 'index term, 1'
    (expect index_term_text[:font_color]).to eql '333333'
  end

  it 'should sort page ranges using first page in sequence when media=print' do
    indexterm_pagenums = [1, 10, 11, 13, 15, 16, 100, 150]
    pagebreak = %(\n\n<<<\n\n)
    input_lines = (1.upto 150).map {|pagenum| (indexterm_pagenums.include? pagenum) ? '((monkey))' : 'business' }
    pdf = to_pdf <<~EOS, analyze: true
    :doctype: book
    :media: print

    #{input_lines.join pagebreak}

    [index]
    == Index
    EOS

    (expect pdf.lines pdf.pages[-1][:text]).to include 'monkey, 1, 10-11, 13, 15-16, 100, 150'
  end

  it 'should apply hanging indent to wrapped lines equal to twice level indent' do
    pdf = to_pdf <<~'EOS', analyze: true
    = Document Title
    :doctype: book

    text(((searching,for fun and profit)))(((searching,when you have absolutely no clue where to begin)))

    [index]
    == Index
    EOS

    searching_text = (pdf.find_text page_number: 3, string: 'searching')[0]
    fun_profit_text = (pdf.find_text page_number: 3, string: /^for fun/)[0]
    begin_text = (pdf.find_text page_number: 3, string: /^begin/)[0]
    left_margin = searching_text[:x]
    level_indent = fun_profit_text[:x] - left_margin
    hanging_indent = begin_text[:x] - fun_profit_text[:x]
    (expect hanging_indent.round).to eql (level_indent * 2).round
  end

  it 'should not insert blank line if index term is forced to break' do
    pdf = to_pdf <<~'EOS', analyze: true
    = Document Title
    :doctype: book
    :notitle:

    text(((flags,SHORT_FLAG)))(((flags,SUPER_LONG_FLAG_THAT_IS_FORCED_TO_BREAK)))

    [index]
    == Index
    EOS

    flags_text = (pdf.find_text 'flags', page_number: 2)[0]
    short_flag_text = (pdf.find_text %r/^SHORT_FLAG/, page_number: 2)[0]
    long_flag_text = (pdf.find_text %r/^SUPER_LONG_FLAG/, page_number: 2)[0]
    line_gap = (flags_text[:y] - short_flag_text[:y]).round 2
    (expect short_flag_text[:x]).to eql long_flag_text[:x]
    (expect (short_flag_text[:y] - long_flag_text[:y]).round 2).to eql line_gap
  end

  it 'should arrange index entries into two columns by default' do
    pdf = to_pdf <<~EOS, analyze: true
    = Document Title
    :doctype: book
    :notitle:

    #{('a'..'z').map {|it| %(((#{it}-term))) }.join}

    [index]
    == Index
    EOS

    category_a_text = (pdf.find_text 'A')[0]
    category_p_text = (pdf.find_text 'P')[0]
    (expect category_a_text[:page_number]).to be 2
    (expect category_p_text[:page_number]).to be 2
    (expect category_p_text[:y]).to eql category_a_text[:y]
    (expect category_p_text[:x]).to be > category_a_text[:x]
  end

  it 'should allow theme to configure number of columns' do
    pdf = to_pdf <<~EOS, pdf_theme: { index_columns: 3 }, analyze: true
    = Document Title
    :doctype: book
    :notitle:

    #{('a'..'z').map {|it| %(((#{it}-keyword))((#{it}-term))) }.join}

    [index]
    == Index
    EOS

    category_a_text = (pdf.find_text 'A')[0]
    category_l_text = (pdf.find_text 'L')[0]
    category_w_text = (pdf.find_text 'W')[0]
    (expect category_a_text[:page_number]).to be 2
    (expect category_l_text[:page_number]).to be 2
    (expect category_w_text[:page_number]).to be 2
    (expect category_l_text[:y]).to eql category_a_text[:y]
    (expect category_w_text[:y]).to eql category_a_text[:y]
    (expect category_w_text[:x]).to be > category_l_text[:x]
    (expect category_l_text[:x]).to be > category_a_text[:x]
  end

  it 'should ignore index columns if columns are set on page' do
    pdf = to_pdf <<~EOS, pdf_theme: { page_columns: 2, index_columns: 3 }, analyze: true
    = Document Title
    :notitle:

    #{('a'..'z').map {|it| %(((#{it}-keyword))((#{it}-term))) }.join}

    [index]
    == Index
    EOS

    midpoint = (get_page_size pdf)[0] * 0.5
    category_g_text = (pdf.find_text 'A')[0]
    category_s_text = (pdf.find_text 'L')[0]
    category_t_text = (pdf.find_text 'W')[0]
    (expect category_g_text[:page_number]).to be 1
    (expect category_g_text[:x]).to eql 48.24
    (expect category_s_text[:x]).to be > midpoint
    (expect category_t_text[:x]).to eql 48.24
  end

  it 'should not allocate space for anchor if font is missing glyph for null character' do
    pdf_theme = {
      extends: 'default',
      font_catalog: {
        'Missing Null' => {
          'normal' => (fixture_file 'mplus1mn-regular-ascii.ttf'),
          'bold' => (fixture_file 'mplus1mn-regular-ascii.ttf'),
        },
      },
      base_font_family: 'Missing Null',
    }

    pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: true
    foo ((bar)) #baz#

    foo bar #baz#

    [index]
    == Index
    EOS

    baz_texts = pdf.find_text 'baz'
    (expect baz_texts).to have_size 2
    (expect baz_texts[0][:x]).to eql baz_texts[1][:x]
  end

  it 'should not distribute excess bottom margin at top of next column' do
    pdf = to_pdf <<~'EOS', analyze: true
    = Document Title
    :doctype: book
    :pdf-page-size: A6

    ((foo))((bar))((baz))((boom))((bang))((fee))((fi))((fo))((fum))((fan))((fool))((ying))((yang))((zed))

    [index]
    == Index
    EOS

    b_category_text = pdf.find_unique_text 'B', page_number: 3
    z_category_text = pdf.find_unique_text 'Z', page_number: 3
    (expect b_category_text[:y]).to eql z_category_text[:y]
  end

  it 'should apply prepress margins to subsequent pages in index' do
    pdf_theme = {
      page_margin: [48.24, 48.24, 48.24, 48.24],
      page_margin_inner: 72,
      page_margin_outer: 36,
      index_columns: 1,
    }
    pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: true
    = Document Title
    :doctype: book
    :notitle:
    :media: prepress
    :pdf-page-size: A5

    == Chapter

    ((foo)) and ((bar))

    ((yin)) and ((yang))

    ((tea)) and ((coffee))

    ((left)) and ((right))

    ((salt)) and ((pepper))

    ((up)) and ((down))

    ((sugar)) and ((spice))

    ((day)) and ((night))

    ((melody)) and ((harmony))

    [index]
    == Index
    EOS

    d_category_text = pdf.find_unique_text 'D', page_number: 3
    s_category_text = pdf.find_unique_text 'S', page_number: 4
    (expect d_category_text).not_to be_nil
    (expect s_category_text).not_to be_nil
    (expect d_category_text[:x]).to eql 72.0
    (expect s_category_text[:x]).to eql 36.0
  end

  # this happens if the term sits right on the page boundary and the hanging indent is still active
  it 'should preserve indentation when recreating column box on subsequent pages' do
    pdf_theme = {
      page_margin: [50, 54],
      page_margin_inner: 72,
      page_margin_outer: 36,
    }
    pdf = to_pdf <<~'EOS', pdf_theme: pdf_theme, analyze: true
    = Document Title
    :doctype: book
    :media: prepress
    :notitle:
    :pdf-page-size: A5

    == Chapter

    ((foo)) and ((bar))

    ((yin)) and ((yang))

    ((tea)) and ((coffee))

    ((left)) and ((right))

    ((salt)) and ((pepper))

    ((up)) and ((down))

    ((sugar)) and ((spice))

    ((day)) and ((night))

    ((melody)) and ((harmony))

    ((inside)) and ((outside))

    ((forward)) and ((back))

    ((cake)) and ((icing))

    ((to that place where you wish to go)) and ((fro))

    ((apples)) and ((oranges))

    ((over)) and ((under))

    ((light)) and ((dark))

    [index]
    == Index
    EOS

    wrapped_text = pdf.find_unique_text 'to go, 1', page_number: 4
    (expect wrapped_text).not_to be_nil
    (expect wrapped_text[:x]).to eql 66.0
    u_category_text = pdf.find_unique_text 'U', page_number: 4
    (expect u_category_text).not_to be_nil
    (expect u_category_text[:x]).to eql 36.0
  end

  it 'should preserve column count on subsequent pages' do
    pdf_theme = {
      page_margin: 36,
      page_margin_inner: 54,
      page_margin_outer: 18,
    }
    pdf = to_pdf <<~EOS, pdf_theme: pdf_theme, analyze: true
    = Document Title
    :doctype: book
    :notitle:
    :media: prepress
    :pdf-page-size: A5

    == Chapter

    #{('a'..'z').map {|l| [l, l * 2, l * 3, l * 4] }.flatten.map {|it| '((' + it + '))' }.join ' '}

    [index]
    == Index
    EOS

    (expect pdf.pages).to have_size 5
    midpoint_recto = 54 + (pdf.pages[0][:size][0] - 72) * 0.5
    midpoint_verso = 18 + (pdf.pages[0][:size][0] - 72) * 0.5

    a_category_text = pdf.find_unique_text 'A', page_number: 3
    f_category_text = pdf.find_unique_text 'F', page_number: 3
    k_category_text = pdf.find_unique_text 'K', page_number: 4
    q_category_text = pdf.find_unique_text 'Q', page_number: 4
    z_category_text = pdf.find_unique_text 'Z', page_number: 5
    (expect a_category_text).not_to be_nil
    (expect a_category_text[:x]).to eql 54.0
    (expect f_category_text).not_to be_nil
    (expect f_category_text[:x]).to be > midpoint_recto
    (expect k_category_text).not_to be_nil
    (expect k_category_text[:x]).to eql 18.0
    (expect q_category_text).not_to be_nil
    (expect q_category_text[:x]).to be > midpoint_verso
    (expect q_category_text[:x]).to be < midpoint_recto
    (expect z_category_text).not_to be_nil
    (expect z_category_text[:x]).to eql 54.0
  end

  it 'should indent TOC title properly when index exceeds a page and section indent is positive' do
    pdf = to_pdf <<~EOS, pdf_theme: { section_indent: 50 }, analyze: true
    = Document Title
    :doctype: book
    :toc:

    == Chapter A

    This is the first chapter.
    #{(0...50).map {|it| %[(((A#{it})))] }.join}

    == Chapter B

    This is the second and last chapter.
    #{(0...50).map {|it| %[(((B#{it})))] }.join}

    [index]
    == Index
    EOS

    toc_title_text = pdf.find_unique_text 'Table of Contents'
    (expect toc_title_text[:x]).to eql 48.24

    chapter_a_toc_text = pdf.find_unique_text 'Chapter A', page_number: 2
    (expect chapter_a_toc_text[:x]).to eql 98.24
  end

  it 'should not push following section to new page if index section does not extend to bottom of page' do
    pdf = to_pdf <<~'EOS', analyze: true
    = Document Title

    == Chapter About Cats

    We know that ((cats)) control the internet.
    But they sort of run nature too.
    (((cats,big cats,lion)))
    After all, the ((king of the jungle)) is the lion, which is a big cat.

    == Chapter About Dogs

    Cats may rule, well, everything.
    But ((dogs)) are a human's best friend.

    [index]
    == Index

    == Section After Index
    EOS

    (expect pdf.pages).to have_size 1
    category_k_text = pdf.find_unique_text 'K'
    (expect category_k_text[:page_number]).to eql 1
    section_after_index_text = pdf.find_unique_text 'Section After Index'
    (expect section_after_index_text[:page_number]).to eql 1
    (expect section_after_index_text[:y]).to be < category_k_text[:y]
  end

  it 'should not push following section to new page if index section does not extend to bottom of second page' do
    pdf = to_pdf <<~EOS, analyze: true
    = Document Title

    #{('a'..'z').map {|it| %(((#{it}-term))) }.join}

    == Chapter About Cats

    We know that ((cats)) control the internet.
    But they sort of run nature too.
    (((cats,big cats,lion)))
    After all, the ((king of the jungle)) is the lion, which is a big cat.

    == Chapter About Dogs

    Cats may rule, well, everything.
    But ((dogs)) are a human's best friend.

    [index]
    == Index

    == Section After Index
    EOS

    (expect pdf.pages).to have_size 2
    category_z_text = pdf.find_unique_text 'Z'
    (expect category_z_text[:page_number]).to eql 2
    (expect category_z_text[:x]).to eql 48.24
    section_after_index_text = pdf.find_unique_text 'Section After Index'
    (expect section_after_index_text[:page_number]).to eql 2
    (expect section_after_index_text[:y]).to be < category_z_text[:y]
  end

  context 'associations' do
    it 'should label term with see assocation with link to alternate term in index and no page number' do
      input = <<~'EOS'
      ((Flash >> HTML 5)) has been supplanted by HTML 5.

      ((HTML 5)) is THE language of the web.

      <<<

      [index]
      == Index
      EOS

      pdf = to_pdf input, analyze: true
      index_lines = pdf.lines pdf.find_text page_number: 2
      (expect index_lines).to include 'Flash (see HTML 5)'
      html5_term_text = pdf.find_unique_text 'HTML 5, ', page_number: 2, font_color: '333333'
      pdf = to_pdf input
      annotations = (get_annotations pdf, 2).select {|it| it[:Dest].start_with? '__indextermdef-' }
      (expect annotations).to have_size 1
      html5_term_id = %(__indextermdef-#{(Digest::MD5.new << 'HTML 5').hexdigest})
      (expect annotations[0][:Dest]).to eql html5_term_id
      html5_term_ref = pdf.objects[(get_names pdf)[html5_term_id]]
      (expect html5_term_ref[2]).to eql html5_term_text[:x]
      (expect html5_term_ref[3].round).to eql (html5_term_text[:y] + 11).round
    end

    it 'should not link to alternate term specified by see if media is not screen' do
      input = <<~'EOS'
      ((Flash >> HTML 5)) has been supplanted by HTML 5.

      ((HTML 5)) is THE language of the web.

      <<<

      [index]
      == Index
      EOS

      pdf = to_pdf input, attribute_overrides: { 'media' => 'print' }, analyze: true
      index_lines = pdf.lines pdf.find_text page_number: 2
      (expect index_lines).to include 'Flash (see HTML 5)'
      pdf = to_pdf input, attribute_overrides: { 'media' => 'print' }
      annotations = get_annotations pdf, 2
      (expect annotations).to be_empty
    end

    it 'should append see also association directly after term and aligned with subterms' do
      input = <<~'EOS'
      ((Desserts &> Cookies)) are essential.

      (((Desserts, Cakes)))Cakes are for special occasions.

      ((Cookies)) are quick and easy to make.

      <<<

      [index]
      == Index
      EOS

      pdf = to_pdf input, analyze: true
      index_lines = pdf.lines pdf.find_text page_number: 2
      (expect index_lines).to include '(see also Cookies)'
      desserts_term_text = pdf.find_unique_text 'Desserts, ', page_number: 2
      cakes_term_text = pdf.find_unique_text 'Cakes, ', page_number: 2
      cookies_see_also_text = pdf.find_unique_text 'Cookies', page_number: 2, font_color: '428BCA'
      see_also_text = pdf.find_unique_text '(see also ', page_number: 2
      (expect cookies_see_also_text[:y]).to be < desserts_term_text[:y]
      (expect cookies_see_also_text[:y]).to be > cakes_term_text[:y]
      (expect see_also_text[:x]).to eql cakes_term_text[:x]
      pdf = to_pdf input
      annotations = (get_annotations pdf, 2).select {|it| it[:Dest].start_with? '__indextermdef-' }
      (expect annotations).to have_size 1
      cookies_term_id = %(__indextermdef-#{(Digest::MD5.new << 'Cookies').hexdigest})
      (expect annotations[0][:Dest]).to eql cookies_term_id
    end

    it 'should append all see also associations directly after term indented with subterms' do
      input = <<~'EOS'
      ((Desserts &> Cookies &> Candies)) are essential.

      (((Desserts, Cakes)))Cakes are for special occasions.

      ((Cookies)) are quick and easy to make.
      ((Candies)), on the other hand, not so much.

      <<<

      [index]
      == Index
      EOS

      pdf = to_pdf input, analyze: true
      index_lines = pdf.lines pdf.find_text page_number: 2
      (expect index_lines).to include '(see also Candies)'
      (expect index_lines).to include '(see also Cookies)'
      desserts_term_text = pdf.find_unique_text 'Desserts, ', page_number: 2
      cakes_term_text = pdf.find_unique_text 'Cakes, ', page_number: 2
      candies_see_also_text = pdf.find_unique_text 'Candies', page_number: 2, font_color: '428BCA'
      cookies_see_also_text = pdf.find_unique_text 'Cookies', page_number: 2, font_color: '428BCA'
      (expect candies_see_also_text[:y]).to be < desserts_term_text[:y]
      (expect cookies_see_also_text[:y]).to be < candies_see_also_text[:y]
      (expect cakes_term_text[:y]).to be < cookies_see_also_text[:y]
    end

    it 'should not link to see also term specified by see-also if media is not screen' do
      input = <<~'EOS'
      ((Desserts &> Cookies)) are essential.

      (((Desserts, Cakes)))Cakes are for special occasions.

      ((Cookies)) are quick and easy to make.

      <<<

      [index]
      == Index
      EOS

      pdf = to_pdf input, attribute_overrides: { 'media' => 'print' }, analyze: true
      index_lines = pdf.lines pdf.find_text page_number: 2
      (expect index_lines).to include '(see also Cookies)'
      pdf = to_pdf input, attribute_overrides: { 'media' => 'print' }
      annotations = get_annotations pdf, 2
      (expect annotations).to be_empty
    end

    it 'should support see also on secondary term' do
      input = <<~'EOS'
      (((Big Cats, Cougars &> Puma)))Cougars, oh my!

      ((Puma)) is another name for cougar.

      <<<

      [index]
      == Index
      EOS

      pdf = to_pdf input, analyze: true
      index_lines = pdf.lines pdf.find_text page_number: 2
      (expect index_lines).to include '(see also Puma)'
      cougars_term_text = pdf.find_unique_text 'Cougars, ', page_number: 2
      puma_see_also_text = pdf.find_unique_text 'Puma', page_number: 2, font_color: '428BCA'
      see_also_text = pdf.find_unique_text '(see also ', page_number: 2
      (expect puma_see_also_text[:y]).to be < cougars_term_text[:y]
      (expect see_also_text[:y]).to eql puma_see_also_text[:y]
      (expect see_also_text[:x]).to be > cougars_term_text[:x]
    end

    it 'should not create link to alternate term if not in index' do
      input = <<~'EOS'
      (((Desserts >> Cookies)))Cookies are the best type of dessert.

      <<<

      [index]
      == Index
      EOS

      pdf = to_pdf input, analyze: true
      index_lines = pdf.lines pdf.find_text page_number: 2
      (expect index_lines).to include 'Desserts (see Cookies)'
      see_cookies_text = pdf.find_unique_text 'Desserts (see Cookies)'
      (expect see_cookies_text).not_to be_nil
      pdf = to_pdf input
      annotations = (get_annotations pdf, 2).select {|it| it[:Dest].start_with? '__indextermdef-' }
      (expect annotations).to be_empty
    end

    it 'should not create link to associated term if not in index' do
      input = <<~'EOS'
      ((Desserts &> Cookies)) are essential.

      <<<

      [index]
      == Index
      EOS

      pdf = to_pdf input, analyze: true
      index_lines = pdf.lines pdf.find_text page_number: 2
      (expect index_lines).to include '(see also Cookies)'
      see_also_cookies_text = pdf.find_unique_text '(see also Cookies)'
      (expect see_also_cookies_text).not_to be_nil
      pdf = to_pdf input
      annotations = (get_annotations pdf, 2).select {|it| it[:Dest].start_with? '__indextermdef-' }
      (expect annotations).to be_empty
    end
  end
end
