# frozen_string_literal: true

require_relative 'test_helper'

context 'Sections' do
  context 'Ids' do
    test 'synthetic id is generated by default' do
      sec = block_from_string '== Section One'
      assert_equal '_section_one', sec.id
    end

    test 'duplicate synthetic id is automatically enumerated' do
      doc = document_from_string <<~'EOS'
      == Section One

      == Section One
      EOS
      assert_equal 2, doc.blocks.size
      assert_equal '_section_one', doc.blocks[0].id
      assert_equal '_section_one_2', doc.blocks[1].id
    end

    test 'synthetic id removes non-word characters' do
      sec = block_from_string '== We’re back!'
      assert_equal '_were_back', sec.id
    end

    test 'synthetic id removes repeating separators' do
      sec = block_from_string '== Section $ One'
      assert_equal '_section_one', sec.id
    end

    test 'synthetic id removes entities' do
      sec = block_from_string '== Ben & Jerry &amp; Company&sup1; &#34;Ice Cream Brothers&#34; &#12354;'
      assert_equal '_ben_jerry_company_ice_cream_brothers', sec.id
    end

    test 'synthetic id removes adjacent entities with mixed case' do
      sec = block_from_string '== a &#xae;&AMP;&#xA9; b'
      assert_equal '_a_b', sec.id
    end

    test 'synthetic id removes XML tags' do
      sec = block_from_string '== Use the `run` command to make it icon:gear[]'
      assert_equal '_use_the_run_command_to_make_it_gear', sec.id
    end

    test 'synthetic id collapses repeating spaces' do
      sec = block_from_string '== Go    Far'
      assert_equal '_go_far', sec.id
    end

    test 'synthetic id replaces hyphens with separator' do
      sec = block_from_string '== State-of-the-art design'
      assert_equal '_state_of_the_art_design', sec.id
    end

    test 'synthetic id replaces dots with separator' do
      sec = block_from_string '== Section 1.1.1'
      assert_equal '_section_1_1_1', sec.id
    end

    test 'synthetic id prefix can be customized' do
      sec = block_from_string ":idprefix: id_\n\n== Section One"
      assert_equal 'id_section_one', sec.id
    end

    test 'synthetic id prefix can be set to blank' do
      sec = block_from_string ":idprefix:\n\n== Section One"
      assert_equal 'section_one', sec.id
    end

    test 'synthetic id prefix is stripped from beginning of id if set to blank' do
      sec = block_from_string ":idprefix:\n\n== & ! More"
      assert_equal 'more', sec.id
    end

    test 'synthetic id separator can be customized' do
      sec = block_from_string ":idseparator: -\n\n== Section One"
      assert_equal '_section-one', sec.id
    end

    test 'synthetic id separator can be hyphen and hyphens are preserved' do
      sec = block_from_string ":idseparator: -\n\n== State-of-the-art design"
      assert_equal '_state-of-the-art-design', sec.id
    end

    test 'synthetic id separator can be dot and dots are preserved' do
      sec = block_from_string ":idseparator: .\n\n== Version 5.0.1"
      assert_equal '_version.5.0.1', sec.id
    end

    test 'synthetic id separator can only be one character' do
      input = <<~'EOS'
      :idseparator: -=-

      == This Section Is All You Need
      EOS
      sec = block_from_string input
      assert_equal '_this-section-is-all-you-need', sec.id
    end

    test 'synthetic id separator can be set to blank' do
      sec = block_from_string ":idseparator:\n\n== Section One"
      assert_equal '_sectionone', sec.id
    end

    test 'synthetic id separator can be set to blank when idprefix is blank' do
      sec = block_from_string ":idprefix:\n:idseparator:\n\n== Section One"
      assert_equal 'sectionone', sec.id
    end

    test 'synthetic id separator is removed from beginning of id when idprefix is blank' do
      sec = block_from_string ":idprefix:\n:idseparator: _\n\n== +Section One"
      assert_equal 'section_one', sec.id
    end

    test 'synthetic ids can be disabled' do
      sec = block_from_string ":sectids!:\n\n== Section One\n"
      assert_nil sec.id
    end

    test 'explicit id in anchor above section title overrides synthetic id' do
      sec = block_from_string "[[one]]\n== Section One"
      assert_equal 'one', sec.id
    end

    test 'explicit id in block attributes above section title overrides synthetic id' do
      sec = block_from_string "[id=one]\n== Section One"
      assert_equal 'one', sec.id
    end

    test 'empty id in block attributes above section title prevents assignment of synthetic id' do
      sect = block_from_string %([id=]\n== Section One)
      assert_nil sect.id
    end

    test 'explicit id set using shorthand in style above section title overrides synthetic id' do
      sec = block_from_string "[#one]\n== Section One"
      assert_equal 'one', sec.id
    end

    test 'should use explicit id from last block attribute line above section title that defines an explicit id' do
      input = <<~'EOS'
      [#un]
      [#one]
      == Section One
      EOS
      sec = block_from_string input
      assert_equal 'one', sec.id
    end

    test 'explicit id can be defined using an embedded anchor' do
      sec = block_from_string '== Section One [[one]] =='
      assert_equal 'one', sec.id
      assert_equal 'Section One', sec.title
    end

    test 'explicit id can be defined using an embedded anchor when using setext section titles' do
      input = <<~'EOS'
      Section Title [[refid,reftext]]
      -------------------------------
      EOS
      sec = block_from_string input
      assert_equal 'Section Title', sec.title
      assert_equal 'refid', sec.id
      assert_equal 'reftext', (sec.attr 'reftext')
    end

    test 'explicit id can be defined using an embedded anchor with reftext' do
      sec = block_from_string '== Section One [[one,Section Uno]] =='
      assert_equal 'one', sec.id
      assert_equal 'Section One', sec.title
      assert_equal 'Section Uno', (sec.attr 'reftext')
    end

    test 'id and reftext in embedded anchor cannot be quoted' do
      sec = block_from_string %(== Section One [["one","Section Uno"]] ==)
      refute_equal 'one', sec.id
      assert_equal 'Section One [["one","Section Uno"]]', sec.title
      assert_nil sec.attr 'reftext'
    end

    test 'reftext in embedded anchor may contain comma' do
      sec = block_from_string %(== Section One [[one, Section,Uno]] ==)
      assert_equal 'one', sec.id
      assert_equal 'Section One', sec.title
      assert_equal 'Section,Uno', (sec.attr 'reftext')
    end

    test 'should unescape but not process inline anchor' do
      sec = block_from_string %(== Section One \\[[one]] ==)
      refute_equal 'one', sec.id
      assert_equal 'Section One [[one]]', sec.title
    end

    test 'should not process inline anchor in section title if section has explicit ID' do
      sec = block_from_string %([#sect-one]\n== Section One [[one]])
      assert_equal 'sect-one', sec.id
      assert_equal 'Section One <a id="one"></a>', sec.title
    end

    test 'should apply substitutions to title with attribute references when registering section with auto-generated ID' do
      input = <<~'EOS'
      = Document Title
      :foo: bar

      See <<_section_baz>>.

      :foo: baz

      == Section {foo}

      That's all, folks!
      EOS

      doc = document_from_string input
      ref = doc.catalog[:refs]['_section_baz']
      refute_nil ref
      output = doc.convert standalone: false
      assert_xpath '//a[@href="#_section_baz"][text()="Section baz"]', output, 1
      assert_xpath '//h2[@id="_section_baz"][text()="Section baz"]', output, 1
    end

    test 'should apply substitutions to title with attribute references when registering section with explicit ID' do
      input = <<~'EOS'
      = Document Title
      :foo: bar

      See <<explicit>>.

      :foo: baz

      [#explicit]
      == Section {foo}

      That's all, folks!
      EOS

      doc = document_from_string input
      ref = doc.catalog[:refs]['explicit']
      refute_nil ref
      output = doc.convert standalone: false
      assert_xpath '//a[@href="#explicit"][text()="Section baz"]', output, 1
      assert_xpath '//h2[@id="explicit"][text()="Section baz"]', output, 1
    end

    test 'should not apply substitutions to title with attribute references when parsing section with empty ID' do
      input = <<~'EOS'
      = Document Title
      :attribute-missing: warn

      [id=]
      == Section {missing}

      That's all, folks!
      EOS

      using_memory_logger do |logger|
        doc = document_from_string input
        assert_empty doc.catalog[:refs]
        assert_empty logger.messages
      end
    end

    test 'title substitutions are applied before generating id' do
      sec = block_from_string "== Section{sp}One\n"
      assert_equal '_section_one', sec.id
    end

    test 'synthetic ids are unique' do
      input = <<~'EOS'
      == Some section

      text

      == Some section

      text
      EOS
      doc = document_from_string input
      assert_equal '_some_section', doc.blocks[0].id
      assert_equal '_some_section_2', doc.blocks[1].id
    end

    # NOTE test cannot be run in parallel with other tests
    test 'can set start index of synthetic ids' do
      old_unique_id_start_index = Asciidoctor::Compliance.unique_id_start_index
      begin
        input = <<~'EOS'
        == Some section

        text

        == Some section

        text
        EOS
        Asciidoctor::Compliance.unique_id_start_index = 1
        doc = document_from_string input
        assert_equal '_some_section', doc.blocks[0].id
        assert_equal '_some_section_1', doc.blocks[1].id
      ensure
        Asciidoctor::Compliance.unique_id_start_index = old_unique_id_start_index
      end
    end

    test 'should use specified id and reftext when registering section reference' do
      input = <<~'EOS'
      [[install,Install Procedure]]
      == Install

      content
      EOS

      doc = document_from_string input
      ref = doc.catalog[:refs]['install']
      refute_nil ref
      assert_equal 'Install Procedure', ref.reftext
      assert_equal 'install', (doc.resolve_id 'Install Procedure')
    end

    test 'should use specified reftext when registering section reference' do
      input = <<~'EOS'
      [reftext="Install Procedure"]
      == Install

      content
      EOS

      doc = document_from_string input
      ref = doc.catalog[:refs]['_install']
      refute_nil ref
      assert_equal 'Install Procedure', ref.reftext
      assert_equal '_install', (doc.resolve_id 'Install Procedure')
    end

    test 'should resolve attribute reference in title using attribute defined at location of section title' do
      input = <<~'EOS'
      :platform-id: linux
      :platform-name: Linux

      [#install-{platform-id}]
      == Install on {platform-name}

      content

      :platform-id: win32
      :platform-name: Windows

      [#install-{platform-id}]
      == Install on {platform-name}

      content
      EOS

      doc = document_from_string input
      ref = doc.catalog[:refs]['install-win32']
      refute_nil ref
      assert_equal 'Install on Windows', ref.title
      assert_equal 'install-win32', (doc.resolve_id 'Install on Windows')
    end

    test 'should substitute attributes when registering reftext for section' do
      input = <<~'EOS'
      :platform-name: n/a
      == Overview

      :platform-name: Linux

      [[install,install on {platform-name}]]
      == Install

      content
      EOS

      doc = document_from_string input
      ref = doc.catalog[:refs]['install']
      refute_nil ref
      assert_equal 'install on Linux', ref.reftext
      assert_equal 'install', (doc.resolve_id 'install on Linux')
    end

    test 'duplicate section id should not overwrite existing section id entry in references table' do
      input = <<~'EOS'
      [#install]
      == First Install

      content

      [#install]
      == Second Install

      content
      EOS

      using_memory_logger do |logger|
        doc = document_from_string input
        ref = doc.catalog[:refs]['install']
        refute_nil ref
        assert_nil ref.reftext
        assert_equal 'First Install', ref.title
        assert_equal 'install', (doc.resolve_id 'First Install')
        assert_message logger, :WARN, '<stdin>: line 7: id assigned to section already in use: install', Hash
      end
    end

    test 'should warn if explicit section ID matches auto-generated section ID' do
      input = <<~'EOS'
      == Do Not Repeat Yourself

      content

      [#_do_not_repeat_yourself]
      == Do Not Repeat Yourself

      content
      EOS

      using_memory_logger do |logger|
        doc = document_from_string input
        ref = doc.catalog[:refs]['_do_not_repeat_yourself']
        refute_nil ref
        assert_nil ref.reftext
        assert_equal 'Do Not Repeat Yourself', ref.title
        assert_equal '_do_not_repeat_yourself', (doc.resolve_id 'Do Not Repeat Yourself')
        assert_message logger, :WARN, '<stdin>: line 6: id assigned to section already in use: _do_not_repeat_yourself', Hash
        assert_equal 2, (doc.convert.scan 'id="_do_not_repeat_yourself"').size
      end
    end

    test 'duplicate block id should not overwrite existing section id entry in references table' do
      input = <<~'EOS'
      [#install]
      == First Install

      content

      [#install]
      content
      EOS

      using_memory_logger do |logger|
        doc = document_from_string input
        ref = doc.catalog[:refs]['install']
        refute_nil ref
        assert_nil ref.reftext
        assert_equal 'First Install', ref.title
        assert_equal 'install', (doc.resolve_id 'First Install')
        assert_message logger, :WARN, '<stdin>: line 7: id assigned to block already in use: install', Hash
      end
    end
  end

  context 'Levels' do
    context 'Document Title (Level 0)' do
      test 'document title with multiline syntax' do
        title = 'My Title'
        chars = '=' * title.length
        assert_xpath '//h1[not(@id)][text() = "My Title"]', convert_string(title + "\n" + chars)
        assert_xpath '//h1[not(@id)][text() = "My Title"]', convert_string(title + "\n" + chars + "\n")
      end

      test 'document title with multiline syntax, give a char' do
        title = 'My Title'
        chars = '=' * (title.length + 1)
        assert_xpath '//h1[not(@id)][text() = "My Title"]', convert_string(title + "\n" + chars)
        assert_xpath '//h1[not(@id)][text() = "My Title"]', convert_string(title + "\n" + chars + "\n")
      end

      test 'document title with multiline syntax, take a char' do
        title = 'My Title'
        chars = '=' * (title.length - 1)
        assert_xpath '//h1[not(@id)][text() = "My Title"]', convert_string(title + "\n" + chars)
        assert_xpath '//h1[not(@id)][text() = "My Title"]', convert_string(title + "\n" + chars + "\n")
      end

      test 'document title with multiline syntax and unicode characters' do
        input = <<~'EOS'
        AsciiDoc Writer’s Guide
        =======================
        Author Name

        preamble
        EOS

        result = convert_string input
        assert_xpath '//h1', result, 1
        assert_xpath '//h1[text()="AsciiDoc Writer’s Guide"]', result, 1
      end

      test 'not enough chars for a multiline document title' do
        title = 'My Title'
        chars = '=' * (title.length - 2)
        using_memory_logger do |logger|
          output = convert_string title + "\n" + chars
          assert_xpath '//h1', output, 0
          refute_empty logger
          logger.clear
          output = convert_string title + "\n" + chars + "\n"
          assert_xpath '//h1', output, 0
          refute_empty logger
        end
      end

      test 'too many chars for a multiline document title' do
        title = 'My Title'
        chars = '=' * (title.length + 2)
        using_memory_logger do |logger|
          output = convert_string title + "\n" + chars
          assert_xpath '//h1', output, 0
          refute_empty logger
          logger.clear
          output = convert_string title + "\n" + chars + "\n"
          assert_xpath '//h1', output, 0
          refute_empty logger
        end
      end

      test 'document title with multiline syntax cannot begin with a dot' do
        title = '.My Title'
        chars = '=' * title.length
        using_memory_logger do |logger|
          output = convert_string title + "\n" + chars
          assert_xpath '//h1', output, 0
          refute_empty logger
        end
      end

      test 'document title with atx syntax' do
        assert_xpath '//h1[not(@id)][text() = "My Title"]', convert_string('= My Title')
      end

      test 'document title with symmetric syntax' do
        assert_xpath '//h1[not(@id)][text() = "My Title"]', convert_string('= My Title =')
      end

      test 'document title created from leveloffset shift defined in document' do
        assert_xpath '//h1[not(@id)][text() = "Document Title"]', convert_string(%(:leveloffset: -1\n== Document Title))
      end

      test 'document title created from leveloffset shift defined in API' do
        assert_xpath '//h1[not(@id)][text() = "Document Title"]', convert_string('== Document Title', attributes: { 'leveloffset' => '-1@' })
      end

      test 'should assign id on document title to body' do
        input = <<~'EOS'
        [[idname]]
        = Document Title

        content
        EOS
        output = convert_string input
        assert_css 'body#idname', output, 1
      end

      test 'should assign id defined using shorthand syntax on document title to body' do
        input = <<~'EOS'
        [#idname]
        = Document Title

        content
        EOS
        output = convert_string input
        assert_css 'body#idname', output, 1
      end

      test 'should use ID defined in block attributes instead of ID defined inline' do
        input = <<~'EOS'
        [#idname-block]
        = Document Title [[idname-inline]]

        content
        EOS
        output = convert_string input
        assert_css 'body#idname-block', output, 1
      end

      test 'block id above document title sets id on document' do
        input = <<~'EOS'
        [[reference]]
        = Reference Manual
        :css-signature: refguide

        preamble
        EOS
        doc = document_from_string input
        assert_equal 'reference', doc.id
        assert_equal 'refguide', doc.attr('css-signature')
        output = doc.convert
        assert_css 'body#reference', output, 1
      end

      test 'should register document in catalog if id is set' do
        input = <<~'EOS'
        [[manual,Manual]]
        = Reference Manual

        preamble
        EOS
        doc = document_from_string input
        assert_equal 'manual', doc.id
        assert_equal 'Manual', doc.attributes['reftext']
        assert_equal doc, doc.catalog[:refs]['manual']
      end

      test 'should compute xreftext to document title' do
        input = <<~'EOS'
        [#manual]
        = Reference Manual
        :xrefstyle: full

        This is the <<manual>>.
        EOS
        output = convert_string input
        assert_xpath '//a[text()="Reference Manual"]', output, 1
      end

      test 'should not interpret level-0 section as document title if it has a style' do
        input = <<~'EOS'
        [glossary]
        = Document Title

        content
        EOS
        using_memory_logger do |logger|
          doc = document_from_string input
          assert_message logger, :ERROR, '<stdin>: line 2: level 0 sections can only be used when doctype is book', Hash
          refute doc.header?
          assert_nil doc.attributes['title']
          assert_equal 'glossary', doc.blocks[0].attributes['style']
        end
      end

      test 'should discard role and options shorthand attributes defined on document title' do
        input = <<~'EOS'
        [#idname.rolename%optionname]
        = Document Title

        content
        EOS
        doc = document_from_string input
        assert_empty doc.blocks[0].attributes
        output = doc.convert
        assert_css '#idname', output, 1
        assert_css 'body#idname', output, 1
        assert_css '.rolename', output, 1
        assert_css 'body.rolename', output, 1
      end
    end

    context 'Level 1' do
      test 'with multiline syntax' do
        assert_xpath '//h2[@id="_my_section"][text() = "My Section"]', convert_string(%(My Section\n-----------))
      end

      test 'should not recognize underline containing a mix of characters as setext section title' do
        input = <<~'EOS'
        My Section
        ----^^----
        EOS

        result = convert_string_to_embedded input
        assert_xpath '//h2[@id="_my_section"][text() = "My Section"]', result, 0
        assert_includes result, '----^^----'
      end

      test 'should not recognize section title that does not contain alphanumeric character' do
        input = <<~'EOS'
        !@#$
        ----
        EOS

        using_memory_logger do |logger|
          result = convert_string_to_embedded input
          assert_css 'h2', result, 0
          assert_message logger, :WARN, '<stdin>: line 2: unterminated listing block', Hash
        end
      end

      test 'should not recognize section title that consists of only underscores' do
        input = <<~'EOS'
        ____
        ----
        EOS

        using_memory_logger do |logger|
          result = convert_string_to_embedded input
          assert_css 'h2', result, 0
          assert_messages logger, [
            [:WARN, '<stdin>: line 1: unterminated quote block', Hash],
            [:WARN, '<stdin>: line 2: unterminated listing block', Hash],
          ]
        end
      end

      test 'should preprocess second line of setext section title' do
        input = <<~'EOS'
        Section Title
        ifdef::asciidoctor[]
        -------------
        endif::[]
        EOS
        result = convert_string_to_embedded input
        assert_xpath '//h2', result, 1
      end

      test 'heading title with multiline syntax cannot begin with a dot' do
        title = '.My Title'
        chars = '-' * title.length
        using_memory_logger do |logger|
          output = convert_string title + "\n" + chars
          assert_xpath '//h2', output, 0
          refute_empty logger
        end
      end

      test 'with atx syntax' do
        assert_xpath '//h2[@id="_my_title"][text() = "My Title"]', convert_string('== My Title')
      end

      test 'with atx symmetric syntax' do
        assert_xpath '//h2[@id="_my_title"][text() = "My Title"]', convert_string('== My Title ==')
      end

      test 'with atx non-matching symmetric syntax' do
        assert_xpath '//h2[@id="_my_title"][text() = "My Title ==="]', convert_string('== My Title ===')
      end

      test 'with XML entity' do
        assert_xpath %(//h2[@id='_whats_new'][text() = "What#{decode_char 8217}s new?"]), convert_string(%q(== What's new?))
      end

      test 'with non-word character' do
        assert_xpath '//h2[@id="_whats_new"][text() = "What’s new?"]', convert_string('== What’s new?')
      end

      test 'with sequential non-word characters' do
        assert_xpath '//h2[@id="_what_the_is_this"][text() = "What the #@$ is this?"]', convert_string('== What the #@$ is this?')
      end

      test 'with trailing whitespace' do
        assert_xpath '//h2[@id="_my_title"][text() = "My Title"]', convert_string('== My Title ')
      end

      test 'with custom blank idprefix' do
        assert_xpath '//h2[@id="my_title"][text() = "My Title"]', convert_string(%(:idprefix:\n\n== My Title ))
      end

      test 'with custom non-blank idprefix' do
        assert_xpath '//h2[@id="ref_my_title"][text() = "My Title"]', convert_string(%(:idprefix: ref_\n\n== My Title ))
      end

      test 'with multibyte characters' do
        input = '== Asciidoctor in 中文'
        output = convert_string input
        assert_xpath '//h2[@id="_asciidoctor_in_中文"][text()="Asciidoctor in 中文"]', output
      end

      test 'with only multibyte characters' do
        input = '== 视图'
        output = convert_string_to_embedded input
        assert_xpath '//h2[@id="_视图"][text()="视图"]', output
      end

      test 'multiline syntax with only multibyte characters' do
        input = <<~'EOS'
        视图
        --

        content

        连接器
        ---

        content
        EOS
        output = convert_string_to_embedded input
        assert_xpath '//h2[@id="_视图"][text()="视图"]', output
        assert_xpath '//h2[@id="_连接器"][text()="连接器"]', output
      end
    end

    context 'Level 2' do
      test 'with multiline syntax' do
        assert_xpath '//h3[@id="_my_section"][text() = "My Section"]', convert_string(%(:fragment:\nMy Section\n~~~~~~~~~~~))
      end

      test 'with atx line syntax' do
        assert_xpath '//h3[@id="_my_title"][text() = "My Title"]', convert_string(%(:fragment:\n=== My Title))
      end
    end

    context 'Level 3' do
      test 'with multiline syntax' do
        assert_xpath '//h4[@id="_my_section"][text() = "My Section"]', convert_string(%(:fragment:\nMy Section\n^^^^^^^^^^))
      end

      test 'with atx line syntax' do
        assert_xpath '//h4[@id="_my_title"][text() = "My Title"]', convert_string(%(:fragment:\n==== My Title))
      end
    end

    context 'Level 4' do
      test 'with multiline syntax' do
        assert_xpath '//h5[@id="_my_section"][text() = "My Section"]', convert_string(%(:fragment:\nMy Section\n++++++++++))
      end

      test 'with atx line syntax' do
        assert_xpath '//h5[@id="_my_title"][text() = "My Title"]', convert_string(%(:fragment:\n===== My Title))
      end
    end

    context 'Level 5' do
      test 'with atx line syntax' do
        assert_xpath '//h6[@id="_my_title"][text() = "My Title"]', convert_string(%(:fragment:\n====== My Title))
      end
    end
  end

  context 'Substitutions' do
    test 'should apply substitutions in normal order' do
      input = <<~'EOS'
      == {link-url}[{link-text}]{tm}

      The one and only!
      EOS

      output = convert_string_to_embedded input, attributes: {
        'link-url' => 'https://acme.com',
        'link-text' => 'ACME',
        'tm' => '(TM)',
      }
      assert_css 'h2', output, 1
      assert_css 'h2 a[href="https://acme.com"]', output, 1
      assert_xpath %(//h2[contains(text(),"#{decode_char 8482}")]), output, 1
    end
  end

  context 'Nesting' do
    test 'should warn if section title is out of sequence' do
      input = <<~'EOS'
      = Document Title

      == Section A

      ==== Nested Section

      content

      == Section B

      content
      EOS

      using_memory_logger do |logger|
        result = convert_string_to_embedded input
        assert_xpath '//h4[text()="Nested Section"]', result, 1
        assert_message logger, :WARN, '<stdin>: line 5: section title out of sequence: expected level 2, got level 3', Hash
      end
    end

    test 'should warn if chapter title is out of sequence' do
      input = <<~'EOS'
      = Document Title
      :doctype: book

      === Not a Chapter

      content
      EOS

      using_memory_logger do |logger|
        result = convert_string_to_embedded input
        assert_xpath '//h3[text()="Not a Chapter"]', result, 1
        assert_message logger, :WARN, '<stdin>: line 4: section title out of sequence: expected levels 0 or 1, got level 2', Hash
      end
    end

    test 'should not warn if top-level section title is out of sequence when fragment attribute is set on document' do
      input = <<~'EOS'
      = Document Title

      === First Section

      content
      EOS

      using_memory_logger do |logger|
        convert_string_to_embedded input, attributes: { 'fragment' => '' }
        assert_empty logger
      end
    end

    test 'should warn if nested section title is out of sequence when fragment attribute is set on document' do
      input = <<~'EOS'
      = Document Title

      === First Section

      ===== Nested Section
      EOS

      using_memory_logger do |logger|
        convert_string_to_embedded input, attributes: { 'fragment' => '' }
        assert_message logger, :WARN, '<stdin>: line 5: section title out of sequence: expected level 3, got level 4', Hash
      end
    end

    test 'should log error if subsections are found in special sections in article that do not support subsections' do
      input = <<~'EOS'
      = Document Title

      == Section

      === Subsection of Section

      allowed

      [appendix]
      == Appendix

      === Subsection of Appendix

      allowed

      [glossary]
      == Glossary

      === Subsection of Glossary

      not allowed

      [bibliography]
      == Bibliography

      === Subsection of Bibliography

      not allowed
      EOS

      using_memory_logger do |logger|
        convert_string_to_embedded input
        assert_messages logger, [
          [:ERROR, '<stdin>: line 19: glossary sections do not support nested sections', Hash],
          [:ERROR, '<stdin>: line 26: bibliography sections do not support nested sections', Hash],
        ]
      end
    end

    test 'should log error if subsections are found in special sections in book that do not support subsections' do
      input = <<~'EOS'
      = Document Title
      :doctype: book

      [preface]
      = Preface

      === Subsection of Preface

      allowed

      [colophon]
      = Colophon

      === Subsection of Colophon

      not allowed

      [dedication]
      = Dedication

      === Subsection of Dedication

      not allowed

      = Part 1

      [abstract]
      == Abstract

      === Subsection of Abstract

      allowed

      == Chapter 1

      === Subsection of Chapter

      allowed

      [appendix]
      = Appendix

      === Subsection of Appendix

      allowed

      [glossary]
      = Glossary

      === Subsection of Glossary

      not allowed

      [bibliography]
      = Bibliography

      === Subsection of Bibliography

      not allowed
      EOS

      using_memory_logger do |logger|
        convert_string_to_embedded input
        assert_messages logger, [
          [:ERROR, '<stdin>: line 14: colophon sections do not support nested sections', Hash],
          [:ERROR, '<stdin>: line 21: dedication sections do not support nested sections', Hash],
          [:ERROR, '<stdin>: line 50: glossary sections do not support nested sections', Hash],
          [:ERROR, '<stdin>: line 57: bibliography sections do not support nested sections', Hash],
        ]
      end
    end
  end

  context 'Markdown-style headings' do
    test 'atx document title with leading marker' do
      input = '# Document Title'
      output = convert_string input
      assert_xpath '//h1[not(@id)][text() = "Document Title"]', output, 1
    end

    test 'atx document title with symmetric markers' do
      input = '# Document Title #'
      output = convert_string input
      assert_xpath '//h1[not(@id)][text() = "Document Title"]', output, 1
    end

    test 'atx section title with leading marker' do
      input = <<~'EOS'
      ## Section One

      blah blah
      EOS
      output = convert_string input
      assert_xpath '//h2[@id="_section_one"][text() = "Section One"]', output, 1
    end

    test 'atx section title with symmetric markers' do
      input = <<~'EOS'
      ## Section One ##

      blah blah
      EOS
      output = convert_string input
      assert_xpath '//h2[@id="_section_one"][text() = "Section One"]', output, 1
    end

    test 'should not match atx syntax with mixed markers' do
      input = '=#= My Title'
      output = convert_string_to_embedded input
      assert_xpath '//h3[@id="_my_title"][text() = "My Title"]', output, 0
      assert_includes output, '<p>=#= My Title</p>'
    end
  end

  context 'Discrete Heading' do
    test 'should create discrete heading instead of section if style is float' do
      input = <<~'EOS'
      [float]
      = Independent Heading!

      not in section
      EOS

      output = convert_string_to_embedded input
      assert_xpath '/h1[@id="_independent_heading"]', output, 1
      assert_xpath '/h1[@class="float"]', output, 1
      assert_xpath %(/h1[@class="float"][text()="Independent Heading!"]), output, 1
      assert_xpath '/h1/following-sibling::*[@class="paragraph"]', output, 1
      assert_xpath '/h1/following-sibling::*[@class="paragraph"]/p', output, 1
      assert_xpath '/h1/following-sibling::*[@class="paragraph"]/p[text()="not in section"]', output, 1
    end

    test 'should create discrete heading instead of section if style is discrete' do
      input = <<~'EOS'
      [discrete]
      === Independent Heading!

      not in section
      EOS

      output = convert_string_to_embedded input
      assert_xpath '/h3', output, 1
      assert_xpath '/h3[@id="_independent_heading"]', output, 1
      assert_xpath '/h3[@class="discrete"]', output, 1
      assert_xpath %(/h3[@class="discrete"][text()="Independent Heading!"]), output, 1
      assert_xpath '/h3/following-sibling::*[@class="paragraph"]', output, 1
      assert_xpath '/h3/following-sibling::*[@class="paragraph"]/p', output, 1
      assert_xpath '/h3/following-sibling::*[@class="paragraph"]/p[text()="not in section"]', output, 1
    end

    test 'should generate id for discrete heading from converted title' do
      input = <<~'EOS'
      [discrete]
      === {sp}Heading{sp}

      not in section
      EOS

      output = convert_string_to_embedded input
      assert_xpath '/h3', output, 1
      assert_xpath '/h3[@class="discrete"][@id="_heading"]', output, 1
      assert_xpath '/h3[@class="discrete"][@id="_heading"][text()=" Heading "]', output, 1
    end

    test 'should create discrete heading if style is float with shorthand role and id' do
      input = <<~'EOS'
      [float.independent#first]
      = Independent Heading!

      not in section
      EOS

      output = convert_string_to_embedded input
      assert_xpath '/h1[@id="first"]', output, 1
      assert_xpath '/h1[@class="float independent"]', output, 1
      assert_xpath %(/h1[@class="float independent"][text()="Independent Heading!"]), output, 1
      assert_xpath '/h1/following-sibling::*[@class="paragraph"]', output, 1
      assert_xpath '/h1/following-sibling::*[@class="paragraph"]/p', output, 1
      assert_xpath '/h1/following-sibling::*[@class="paragraph"]/p[text()="not in section"]', output, 1
    end

    test 'should create discrete heading if style is discrete with shorthand role and id' do
      input = <<~'EOS'
      [discrete.independent#first]
      = Independent Heading!

      not in section
      EOS

      output = convert_string_to_embedded input
      assert_xpath '/h1[@id="first"]', output, 1
      assert_xpath '/h1[@class="discrete independent"]', output, 1
      assert_xpath %(/h1[@class="discrete independent"][text()="Independent Heading!"]), output, 1
      assert_xpath '/h1/following-sibling::*[@class="paragraph"]', output, 1
      assert_xpath '/h1/following-sibling::*[@class="paragraph"]/p', output, 1
      assert_xpath '/h1/following-sibling::*[@class="paragraph"]/p[text()="not in section"]', output, 1
    end

    test 'discrete heading should be a block with context floating_title' do
      input = <<~'EOS'
      [float]
      === Independent Heading!

      not in section
      EOS

      doc = document_from_string input
      heading = doc.blocks.first
      assert_kind_of Asciidoctor::Block, heading
      assert_equal :floating_title, heading.context
      assert_equal '_independent_heading', heading.id
      assert doc.catalog[:refs].key? '_independent_heading'
    end

    test 'should preprocess second line of setext discrete heading' do
      input = <<~'EOS'
      [discrete]
      Heading Title
      ifdef::asciidoctor[]
      -------------
      endif::[]
      EOS
      result = convert_string_to_embedded input
      assert_xpath '//h2', result, 1
    end

    test 'can assign explicit id to discrete heading' do
      input = <<~'EOS'
      [[unchained]]
      [float]
      === Independent Heading!

      not in section
      EOS

      doc = document_from_string input
      heading = doc.blocks.first
      assert_equal 'unchained', heading.id
      assert doc.catalog[:refs].key? 'unchained'
    end

    test 'should not include discrete heading in toc' do
      input = <<~'EOS'
      :toc:

      == Section One

      [float]
      === Miss Independent

      == Section Two
      EOS

      output = convert_string input
      assert_xpath '//*[@id="toc"]', output, 1
      assert_xpath %(//*[@id="toc"]//a[contains(text(), "Section ")]), output, 2
      assert_xpath %(//*[@id="toc"]//a[text()="Miss Independent"]), output, 0
    end

    test 'should not set id on discrete heading if sectids attribute is unset' do
      input = <<~'EOS'
      [float]
      === Independent Heading!

      not in section
      EOS

      output = convert_string_to_embedded input, attributes: { 'sectids' => nil }
      assert_xpath '/h3', output, 1
      assert_xpath '/h3[@id="_independent_heading"]', output, 0
      assert_xpath '/h3[@class="float"]', output, 1
    end

    test 'should use explicit id for discrete heading if specified' do
      input = <<~'EOS'
      [[free]]
      [float]
      == Independent Heading!

      not in section
      EOS

      output = convert_string_to_embedded input
      assert_xpath '/h2', output, 1
      assert_xpath '/h2[@id="free"]', output, 1
      assert_xpath '/h2[@class="float"]', output, 1
    end

    test 'should add role to class attribute on discrete heading' do
      input = <<~'EOS'
      [float, role="isolated"]
      == Independent Heading!

      not in section
      EOS

      output = convert_string_to_embedded input
      assert_xpath '/h2', output, 1
      assert_xpath '/h2[@id="_independent_heading"]', output, 1
      assert_xpath '/h2[@class="float isolated"]', output, 1
    end

    test 'should ignore title attribute on discrete heading' do
      input = <<~'EOS'
      [discrete,title="Captured!"]
      == Independent Heading!

      not in section
      EOS

      doc = document_from_string input
      heading = doc.blocks[0]
      assert_equal 'Independent Heading!', heading.title
      refute heading.attributes.key? 'title'
    end

    test 'should use specified id and reftext when registering discrete section reference' do
      input = <<~'EOS'
      [[install,Install Procedure]]
      [discrete]
      == Install

      content
      EOS

      doc = document_from_string input
      ref = doc.catalog[:refs]['install']
      refute_nil ref
      assert_equal 'Install Procedure', ref.reftext
      assert_equal 'install', (doc.resolve_id 'Install Procedure')
    end

    test 'should use specified reftext when registering discrete section reference' do
      input = <<~'EOS'
      [reftext="Install Procedure"]
      [discrete]
      == Install

      content
      EOS

      doc = document_from_string input
      ref = doc.catalog[:refs]['_install']
      refute_nil ref
      assert_equal 'Install Procedure', ref.reftext
      assert_equal '_install', (doc.resolve_id 'Install Procedure')
    end

    test 'should not process inline anchor in discrete heading if explicit ID is assigned' do
      input = <<~'EOS'
      [discrete#install]
      == Install [[installation]]

      content
      EOS

      block = block_from_string input
      assert_equal 'install', block.id
      assert_equal 'Install <a id="installation"></a>', block.title
    end
  end

  context 'Level offset' do
    test 'should print error if standalone document is included without level offset' do
      input = <<~'EOS'
      = Main Document
      Doc Writer

      text in main document

      // begin simulated include::[]
      = Standalone Document
      :author: Junior Writer

      text in standalone document

      // end simulated include::[]
      EOS

      using_memory_logger do |logger|
        convert_string input
        assert_message logger, :ERROR, '<stdin>: line 7: level 0 sections can only be used when doctype is book', Hash
      end
    end

    test 'should add level offset to section level' do
      input = <<~'EOS'
      = Main Document
      Doc Writer

      Main document written by {author}.

      :leveloffset: 1

      // begin simulated include::[]
      = Standalone Document
      :author: Junior Writer

      Standalone document written by {author}.

      == Section in Standalone

      Standalone section text.
      // end simulated include::[]

      :leveloffset!:

      == Section in Main

      Main section text.
      EOS

      output = nil
      using_memory_logger do |logger|
        output = convert_string input
        assert_empty logger
      end

      assert_match(/Main document written by Doc Writer/, output)
      assert_match(/Standalone document written by Junior Writer/, output)
      assert_xpath '//*[@class="sect1"]/h2[text() = "Standalone Document"]', output, 1
      assert_xpath '//*[@class="sect2"]/h3[text() = "Section in Standalone"]', output, 1
      assert_xpath '//*[@class="sect1"]/h2[text() = "Section in Main"]', output, 1
    end

    test 'level offset should be added to discrete heading' do
      input = <<~'EOS'
      = Main Document
      Doc Writer

      :leveloffset: 1

      [float]
      = Discrete Heading
      EOS

      output = convert_string input
      assert_xpath '//h2[@class="float"][text() = "Discrete Heading"]', output, 1
    end

    test 'should be able to reset level offset' do
      input = <<~'EOS'
      = Main Document
      Doc Writer

      Main preamble.

      :leveloffset: 1

      = Standalone Document

      Standalone preamble.

      :leveloffset!:

      == Level 1 Section
      EOS

      output = convert_string input
      assert_xpath '//*[@class = "sect1"]/h2[text() = "Standalone Document"]', output, 1
      assert_xpath '//*[@class = "sect1"]/h2[text() = "Level 1 Section"]', output, 1
    end

    test 'should add relative offset value to current leveloffset' do
      input = <<~'EOS'
      = Main Document
      Doc Writer

      Main preamble.

      :leveloffset: 1

      = Chapter 1

      content

      :leveloffset: +1

      = Standalone Section

      content
      EOS

      output = convert_string input
      assert_xpath '//*[@class = "sect1"]/h2[text() = "Chapter 1"]', output, 1
      assert_xpath '//*[@class = "sect2"]/h3[text() = "Standalone Section"]', output, 1
    end
  end

  context 'Section Numbering' do
    test 'should create section number with one entry for level 1' do
      doc = empty_document
      sect1 = Asciidoctor::Section.new nil, nil, true
      doc << sect1
      assert_equal '1.', sect1.sectnum
    end

    test 'should create section number with two entries for level 2' do
      doc = empty_document
      sect1 = Asciidoctor::Section.new nil, nil, true
      doc << sect1
      sect1_1 = Asciidoctor::Section.new sect1, nil, true
      sect1 << sect1_1
      assert_equal '1.1.', sect1_1.sectnum
    end

    test 'should create section number with three entries for level 3' do
      doc = empty_document
      sect1 = Asciidoctor::Section.new nil, nil, true
      doc << sect1
      sect1_1 = Asciidoctor::Section.new sect1, nil, true
      sect1 << sect1_1
      sect1_1_1 = Asciidoctor::Section.new sect1_1, nil, true
      sect1_1 << sect1_1_1
      assert_equal '1.1.1.', sect1_1_1.sectnum
    end

    test 'should create section number for second section in level' do
      doc = empty_document
      sect1 = Asciidoctor::Section.new nil, nil, true
      doc << sect1
      sect1_1 = Asciidoctor::Section.new sect1, nil, true
      sect1 << sect1_1
      sect1_2 = Asciidoctor::Section.new sect1, nil, true
      sect1 << sect1_2
      assert_equal '1.2.', sect1_2.sectnum
    end

    test 'sectnum should use specified delimiter and append string' do
      doc = empty_document
      sect1 = Asciidoctor::Section.new nil, nil, true
      doc << sect1
      sect1_1 = Asciidoctor::Section.new sect1, nil, true
      sect1 << sect1_1
      sect1_1_1 = Asciidoctor::Section.new sect1_1, nil, true
      sect1_1 << sect1_1_1
      assert_equal '1,1,1,', sect1_1_1.sectnum(',')
      assert_equal '1:1:1', sect1_1_1.sectnum(':', false)
    end

    test 'should output section numbers when sectnums attribute is set' do
      input = <<~'EOS'
      = Title
      :sectnums:

      == Section_1

      text

      === Section_1_1

      text

      ==== Section_1_1_1

      text

      == Section_2

      text

      === Section_2_1

      text

      === Section_2_2

      text
      EOS

      output = convert_string input
      assert_xpath '//h2[@id="_section_1"][starts-with(text(), "1. ")]', output, 1
      assert_xpath '//h3[@id="_section_1_1"][starts-with(text(), "1.1. ")]', output, 1
      assert_xpath '//h4[@id="_section_1_1_1"][starts-with(text(), "1.1.1. ")]', output, 1
      assert_xpath '//h2[@id="_section_2"][starts-with(text(), "2. ")]', output, 1
      assert_xpath '//h3[@id="_section_2_1"][starts-with(text(), "2.1. ")]', output, 1
      assert_xpath '//h3[@id="_section_2_2"][starts-with(text(), "2.2. ")]', output, 1
    end

    test 'should output section numbers when numbered attribute is set' do
      input = <<~'EOS'
      = Title
      :numbered:

      == Section_1

      text

      === Section_1_1

      text

      ==== Section_1_1_1

      text

      == Section_2

      text

      === Section_2_1

      text

      === Section_2_2

      text
      EOS

      output = convert_string input
      assert_xpath '//h2[@id="_section_1"][starts-with(text(), "1. ")]', output, 1
      assert_xpath '//h3[@id="_section_1_1"][starts-with(text(), "1.1. ")]', output, 1
      assert_xpath '//h4[@id="_section_1_1_1"][starts-with(text(), "1.1.1. ")]', output, 1
      assert_xpath '//h2[@id="_section_2"][starts-with(text(), "2. ")]', output, 1
      assert_xpath '//h3[@id="_section_2_1"][starts-with(text(), "2.1. ")]', output, 1
      assert_xpath '//h3[@id="_section_2_2"][starts-with(text(), "2.2. ")]', output, 1
    end

    test 'should not crash if child section of part is out of sequence and part numbering is disabled' do
      input = <<~'EOS'
      = Document Title
      :doctype: book
      :sectnums:

      = Part

      === Out of Sequence Section
      EOS

      using_memory_logger do |logger|
        output = convert_string input
        assert_xpath '//h1[text()="Part"]', output, 1
        assert_xpath '//h3[text()=".1. Out of Sequence Section"]', output, 1
        assert_message logger, :WARN, '<stdin>: line 7: section title out of sequence: expected level 1, got level 2', Hash
      end
    end

    test 'should not hang if relative leveloffset attempts to make resolved section level negative' do
      input = <<~'EOS'
      = Document Title
      :doctype: book
      :leveloffset: -1

      = Part Title

      == Chapter Title
      EOS

      using_memory_logger do |logger|
        output = convert_string input
        assert_xpath '//h1[text()="Part Title"]', output, 1
        assert_xpath '//h1[text()="Chapter Title"]', output, 1
        # FIXME this prints 2 error messages instead of 1
        assert_message logger, :ERROR, '<stdin>: line 7: invalid part, must have at least one section (e.g., chapter, appendix, etc.)', Hash, 0
      end
    end

    test 'should number parts when doctype is book and partnums attributes is set' do
      input = <<~'EOS'
      = Book Title
      :doctype: book
      :sectnums:
      :partnums:

      = Language

      == Syntax

      content

      = Processor

      == CLI

      content
      EOS

      output = convert_string input
      assert_xpath '//h1[@id="_language"][text() = "I: Language"]', output, 1
      assert_xpath '//h1[@id="_processor"][text() = "II: Processor"]', output, 1
    end

    test 'should assign sequential roman numerals to book parts' do
      input = <<~'EOS'
      = Book Title
      :doctype: book
      :sectnums:
      :partnums:

      = First Part

      part intro

      == First Chapter

      = Second Part

      part intro

      == Second Chapter
      EOS

      doc = document_from_string input
      assert_equal 'I', doc.sections[0].numeral
      assert_equal '1', doc.sections[0].sections[0].numeral
      assert_equal 'II', doc.sections[1].numeral
      assert_equal '2', doc.sections[1].sections[0].numeral
    end

    test 'should prepend value of part-signifier attribute to title of numbered part' do
      input = <<~'EOS'
      = Book Title
      :doctype: book
      :sectnums:
      :partnums:
      :part-signifier: Part

      = Language

      == Syntax

      content

      = Processor

      == CLI

      content
      EOS

      output = convert_string input
      assert_xpath '//h1[@id="_language"][text() = "Part I: Language"]', output, 1
      assert_xpath '//h1[@id="_processor"][text() = "Part II: Processor"]', output, 1
    end

    test 'should prepend value of chapter-signifier attribute to title of numbered chapter' do
      input = <<~'EOS'
      = Book Title
      :doctype: book
      :sectnums:
      :partnums:
      :chapter-signifier: Chapter

      = Language

      == Syntax

      content

      = Processor

      == CLI

      content
      EOS

      output = convert_string input
      assert_xpath '//h2[@id="_syntax"][text() = "Chapter 1. Syntax"]', output, 1
      assert_xpath '//h2[@id="_cli"][text() = "Chapter 2. CLI"]', output, 1
    end

    test 'should allow chapter number to be controlled using chapter-number attribute' do
      input = <<~'EOS'
      = Book Title
      :doctype: book
      :sectnums:
      :chapter-signifier: Chapter
      :chapter-number: 9

      == Not the Beginning

      == Maybe the End
      EOS

      output = convert_string input
      assert_xpath '//h2[@id="_not_the_beginning"][text() = "Chapter 10. Not the Beginning"]', output, 1
      assert_xpath '//h2[@id="_maybe_the_end"][text() = "Chapter 11. Maybe the End"]', output, 1
    end

    test 'blocks should have level' do
      input = <<~'EOS'
      = Title

      preamble

      == Section 1

      paragraph

      === Section 1.1

      paragraph
      EOS
      doc = document_from_string input
      assert_equal 0, doc.blocks[0].level
      assert_equal 1, doc.blocks[1].level
      assert_equal 1, doc.blocks[1].blocks[0].level
      assert_equal 2, doc.blocks[1].blocks[1].level
      assert_equal 2, doc.blocks[1].blocks[1].blocks[0].level
    end

    test 'section numbers should not increment when numbered attribute is turned off within document' do
      input = <<~'EOS'
      = Document Title
      :numbered:

      :numbered!:

      == Colophon Section

      == Another Colophon Section

      == Final Colophon Section

      :numbered:

      == Section One

      === Section One Subsection

      == Section Two

      == Section Three
      EOS

      output = convert_string input
      assert_xpath '//h1[text()="Document Title"]', output, 1
      assert_xpath '//h2[@id="_colophon_section"][text()="Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_another_colophon_section"][text()="Another Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_final_colophon_section"][text()="Final Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_section_one"][text()="1. Section One"]', output, 1
      assert_xpath '//h3[@id="_section_one_subsection"][text()="1.1. Section One Subsection"]', output, 1
      assert_xpath '//h2[@id="_section_two"][text()="2. Section Two"]', output, 1
      assert_xpath '//h2[@id="_section_three"][text()="3. Section Three"]', output, 1
    end

    test 'section numbers can be toggled even if numbered attribute is enable via the API' do
      input = <<~'EOS'
      = Document Title

      :numbered!:

      == Colophon Section

      == Another Colophon Section

      == Final Colophon Section

      :numbered:

      == Section One

      === Section One Subsection

      == Section Two

      == Section Three
      EOS

      output = convert_string input, attributes: { 'numbered' => '' }
      assert_xpath '//h1[text()="Document Title"]', output, 1
      assert_xpath '//h2[@id="_colophon_section"][text()="Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_another_colophon_section"][text()="Another Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_final_colophon_section"][text()="Final Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_section_one"][text()="1. Section One"]', output, 1
      assert_xpath '//h3[@id="_section_one_subsection"][text()="1.1. Section One Subsection"]', output, 1
      assert_xpath '//h2[@id="_section_two"][text()="2. Section Two"]', output, 1
      assert_xpath '//h2[@id="_section_three"][text()="3. Section Three"]', output, 1
    end

    test 'section numbers cannot be toggled even if numbered attribute is disabled via the API' do
      input = <<~'EOS'
      = Document Title

      :numbered!:

      == Colophon Section

      == Another Colophon Section

      == Final Colophon Section

      :numbered:

      == Section One

      === Section One Subsection

      == Section Two

      == Section Three
      EOS

      output = convert_string input, attributes: { 'numbered!' => '' }
      assert_xpath '//h1[text()="Document Title"]', output, 1
      assert_xpath '//h2[@id="_colophon_section"][text()="Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_another_colophon_section"][text()="Another Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_final_colophon_section"][text()="Final Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_section_one"][text()="Section One"]', output, 1
      assert_xpath '//h3[@id="_section_one_subsection"][text()="Section One Subsection"]', output, 1
      assert_xpath '//h2[@id="_section_two"][text()="Section Two"]', output, 1
      assert_xpath '//h2[@id="_section_three"][text()="Section Three"]', output, 1
    end

    # NOTE AsciiDoc.py fails this test because it does not properly check for a None value when looking up the numbered attribute
    test 'section numbers should not increment until numbered attribute is turned back on' do
      input = <<~'EOS'
      = Document Title
      :numbered!:

      == Colophon Section

      == Another Colophon Section

      == Final Colophon Section

      :numbered:

      == Section One

      === Section One Subsection

      == Section Two

      == Section Three
      EOS

      output = convert_string input
      assert_xpath '//h1[text()="Document Title"]', output, 1
      assert_xpath '//h2[@id="_colophon_section"][text()="Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_another_colophon_section"][text()="Another Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_final_colophon_section"][text()="Final Colophon Section"]', output, 1
      assert_xpath '//h2[@id="_section_one"][text()="1. Section One"]', output, 1
      assert_xpath '//h3[@id="_section_one_subsection"][text()="1.1. Section One Subsection"]', output, 1
      assert_xpath '//h2[@id="_section_two"][text()="2. Section Two"]', output, 1
      assert_xpath '//h2[@id="_section_three"][text()="3. Section Three"]', output, 1
    end

    test 'table with asciidoc content should not disable numbering of subsequent sections' do
      input = <<~'EOS'
      = Document Title
      :numbered:

      preamble

      == Section One

      |===
      a|content
      |===

      == Section Two

      content
      EOS

      output = convert_string input
      assert_xpath '//h2[@id="_section_one"]', output, 1
      assert_xpath '//h2[@id="_section_one"][text()="1. Section One"]', output, 1
      assert_xpath '//h2[@id="_section_two"]', output, 1
      assert_xpath '//h2[@id="_section_two"][text()="2. Section Two"]', output, 1
    end

    test 'should not number parts when doctype is book' do
      input = <<~'EOS'
      = Document Title
      :doctype: book
      :numbered:

      = Part 1

      == Chapter 1

      content

      = Part 2

      == Chapter 2

      content
      EOS

      output = convert_string input
      assert_xpath '(//h1)[1][text()="Document Title"]', output, 1
      assert_xpath '(//h1)[2][text()="Part 1"]', output, 1
      assert_xpath '(//h1)[3][text()="Part 2"]', output, 1
      assert_xpath '(//h2)[1][text()="1. Chapter 1"]', output, 1
      assert_xpath '(//h2)[2][text()="2. Chapter 2"]', output, 1
    end

    test 'should number chapters sequentially even when divided into parts' do
      input = <<~'EOS'
      = Document Title
      :doctype: book
      :numbered:

      == Chapter 1

      content

      = Part 1

      == Chapter 2

      content

      = Part 2

      == Chapter 3

      content

      == Chapter 4

      content
      EOS

      result = convert_string input
      (1..4).each do |num|
        assert_xpath %(//h2[@id="_chapter_#{num}"]), result, 1
        assert_xpath %(//h2[@id="_chapter_#{num}"][text()="#{num}. Chapter #{num}"]), result, 1
      end
    end

    test 'reindex_sections should correct section enumeration after sections are modified' do
      input = <<~'EOS'
      :sectnums:

      == First Section

      content

      == Last Section

      content
      EOS

      doc = document_from_string input
      second_section = Asciidoctor::Section.new doc, nil, true
      doc.blocks.insert 1, second_section
      doc.reindex_sections
      sections = doc.sections
      [0, 1, 2].each do |index|
        assert_equal index, sections[index].index
        assert_equal (index + 1).to_s, sections[index].numeral
        assert_equal index + 1, sections[index].number
      end
    end

    test 'should allow sections to be renumbered using numeral or deprecated number property' do
      input = <<~'EOS'
      == Somewhere in the Middle

      == A Bit Later

      == Nearing the End

      == The End
      EOS

      doc = document_from_string input, attributes: { 'sectnums' => '' }
      doc.sections.each do |sect|
        if sect.numeral.to_i.even?
          sect.numeral.next!
        else
          sect.number += 1
        end
      end

      output = doc.convert standalone: false
      assert_xpath '//h2[text()="2. Somewhere in the Middle"]', output, 1
      assert_xpath '//h2[text()="3. A Bit Later"]', output, 1
      assert_xpath '//h2[text()="4. Nearing the End"]', output, 1
      assert_xpath '//h2[text()="5. The End"]', output, 1
    end
  end

  context 'Links and anchors' do
    test 'should include anchor if sectanchors document attribute is set' do
      input = <<~'EOS'
      == Installation

      Installation section.

      === Linux

      Linux installation instructions.
      EOS

      output = convert_string_to_embedded input, attributes: { 'sectanchors' => '' }
      assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a', output, 1
      assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a[@class="anchor"][@href="#_installation"]', output, 1
      assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a/following-sibling::text()="Installation"', output, true
      assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a', output, 1
      assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a[@class="anchor"][@href="#_linux"]', output, 1
      assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a/following-sibling::text()="Linux"', output, true
    end

    test 'should position after title text if sectanchors is set to after' do
      input = <<~'EOS'
      == Installation

      Installation section.

      === Linux

      Linux installation instructions.
      EOS

      output = convert_string_to_embedded input, attributes: { 'sectanchors' => 'after' }
      assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a', output, 1
      assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a[@class="anchor"][@href="#_installation"]', output, 1
      assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a/preceding-sibling::text()="Installation"', output, true
      assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a', output, 1
      assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a[@class="anchor"][@href="#_linux"]', output, 1
      assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a/preceding-sibling::text()="Linux"', output, true
    end

    test 'should link section if sectlinks document attribute is set' do
      input = <<~'EOS'
      == Installation

      Installation section.

      === Linux

      Linux installation instructions.
      EOS

      output = convert_string_to_embedded input, attributes: { 'sectlinks' => '' }
      assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a', output, 1
      assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a[@class="link"][@href="#_installation"]', output, 1
      assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a[text()="Installation"]', output, 1
      assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a', output, 1
      assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a[@class="link"][@href="#_linux"]', output, 1
      assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a[text()="Linux"]', output, 1
    end
  end

  context 'Special sections' do
    test 'should ignore style if it matches sectN' do
      input = <<~'EOS'
      = Document Title

      [sect1]
      == Section Level 1

      content

      [sect2]
      == Section Level 2

      content
      EOS

      output = convert_string input, backend: :docbook
      assert_xpath '//section', output, 2
      assert_xpath '//sect1', output, 0
      assert_xpath '//sect2', output, 0
    end

    test 'should assign sectname, caption, and numeral to appendix section by default' do
      input = <<~'EOS'
      [appendix]
      == Attribute Options

      Details
      EOS

      appendix = block_from_string input
      assert_equal 'appendix', appendix.sectname
      assert_equal 'Appendix A: ', appendix.caption
      assert_equal 'A', appendix.numeral
      assert_equal 'A', appendix.number
      assert appendix.numbered
    end

    test 'should not promote level-0 special section in book doctype to document title' do
      input = <<~'EOS'
      :doctype: book

      [appendix]
      = Installation

      Installation details here.
      EOS

      doc = document_from_string input
      refute doc.header?
      assert_nil doc.attributes['title']
      appendix = doc.blocks[0]
      assert_equal 'appendix', appendix.sectname
      assert_equal 'Appendix A: ', appendix.caption
    end

    test 'should prefix appendix title by numbered label even when section numbering is disabled' do
      input = <<~'EOS'
      [appendix]
      == Attribute Options

      Details
      EOS

      output = convert_string_to_embedded input
      assert_xpath '//h2[text()="Appendix A: Attribute Options"]', output, 1
    end

    test 'should allow appendix number to be controlled using appendix-number attribute' do
      input = <<~'EOS'
      :appendix-number: α

      [appendix]
      == Attribute Options

      Details

      [appendix]
      == All the Other Stuff

      Details
      EOS

      output = convert_string_to_embedded input
      assert_xpath %(//h2[text()="Appendix #{decode_char 946}: Attribute Options"]), output, 1
      assert_xpath %(//h2[text()="Appendix #{decode_char 947}: All the Other Stuff"]), output, 1
    end

    test 'should use style from last block attribute line above section that defines a style' do
      input = <<~'EOS'
      [glossary]
      [appendix]
      == Attribute Options

      Details
      EOS

      output = convert_string_to_embedded input
      assert_xpath '//h2[text()="Appendix A: Attribute Options"]', output, 1
    end

    test 'setting ID using style shorthand should not clear section style' do
      input = <<~'EOS'
      [appendix]
      [#attribute-options]
      == Attribute Options

      Details
      EOS

      output = convert_string_to_embedded input
      assert_xpath '//h2[@id="attribute-options"][text()="Appendix A: Attribute Options"]', output, 1
    end

    test 'should use custom appendix caption if specified' do
      input = <<~'EOS'
      :appendix-caption: App

      [appendix]
      == Attribute Options

      Details
      EOS

      output = convert_string_to_embedded input
      assert_xpath '//h2[text()="App A: Attribute Options"]', output, 1
    end

    test 'should only assign letter to appendix when numbered is enabled and appendix caption is not set' do
      input = <<~'EOS'
      :numbered:
      :!appendix-caption:

      [appendix]
      == Attribute Options

      Details
      EOS

      output = convert_string_to_embedded input
      assert_xpath '//h2[text()="A. Attribute Options"]', output, 1
    end

    test 'should increment appendix number for each appendix section' do
      input = <<~'EOS'
      [appendix]
      == Attribute Options

      Details

      [appendix]
      == Migration

      Details
      EOS

      output = convert_string_to_embedded input
      assert_xpath '(//h2)[1][text()="Appendix A: Attribute Options"]', output, 1
      assert_xpath '(//h2)[2][text()="Appendix B: Migration"]', output, 1
    end

    test 'should continue numbering after appendix' do
      input = <<~'EOS'
      :numbered:

      == First Section

      content

      [appendix]
      == Attribute Options

      content

      == Migration

      content
      EOS

      output = convert_string_to_embedded input
      assert_xpath '(//h2)[1][text()="1. First Section"]', output, 1
      assert_xpath '(//h2)[2][text()="Appendix A: Attribute Options"]', output, 1
      assert_xpath '(//h2)[3][text()="2. Migration"]', output, 1
    end

    test 'should number appendix subsections using appendix letter' do
      input = <<~'EOS'
      :numbered:

      [appendix]
      == Attribute Options

      Details

      === Optional Attributes

      Details
      EOS

      output = convert_string_to_embedded input
      assert_xpath '(//h2)[1][text()="Appendix A: Attribute Options"]', output, 1
      assert_xpath '(//h3)[1][text()="A.1. Optional Attributes"]', output, 1
    end

    test 'should not number level 4 section by default' do
      input = <<~'EOS'
      :numbered:

      == Level_1

      === Level_2

      ==== Level_3

      ===== Level_4

      text
      EOS
      output = convert_string_to_embedded input
      assert_xpath '//h5', output, 1
      assert_xpath '//h5[text()="Level_4"]', output, 1
    end

    test 'should only number levels up to value defined by sectnumlevels attribute' do
      input = <<~'EOS'
      :numbered:
      :sectnumlevels: 2

      == Level_1

      === Level_2

      ==== Level_3

      ===== Level_4

      text
      EOS
      output = convert_string_to_embedded input
      assert_xpath '//h2', output, 1
      assert_xpath '//h2[text()="1. Level_1"]', output, 1
      assert_xpath '//h3', output, 1
      assert_xpath '//h3[text()="1.1. Level_2"]', output, 1
      assert_xpath '//h4', output, 1
      assert_xpath '//h4[text()="Level_3"]', output, 1
      assert_xpath '//h5', output, 1
      assert_xpath '//h5[text()="Level_4"]', output, 1
    end

    test 'should not number sections or subsections in regions where numbered is off' do
      input = <<~'EOS'
      :numbered:

      == Section One

      :numbered!:

      [appendix]
      == Attribute Options

      Details

      [appendix]
      == Migration

      Details

      === Gotchas

      Details

      [glossary]
      == Glossary

      Terms
      EOS

      output = convert_string_to_embedded input
      assert_xpath '(//h2)[1][text()="1. Section One"]', output, 1
      assert_xpath '(//h2)[2][text()="Appendix A: Attribute Options"]', output, 1
      assert_xpath '(//h2)[3][text()="Appendix B: Migration"]', output, 1
      assert_xpath '(//h3)[1][text()="Gotchas"]', output, 1
      assert_xpath '(//h2)[4][text()="Glossary"]', output, 1
    end

    test 'should not number sections or subsections in toc in regions where numbered is off' do
      input = <<~'EOS'
      :numbered:
      :toc:

      == Section One

      :numbered!:

      [appendix]
      == Attribute Options

      Details

      [appendix]
      == Migration

      Details

      === Gotchas

      Details

      [glossary]
      == Glossary

      Terms
      EOS

      output = convert_string input
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="1. Section One"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Appendix A: Attribute Options"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Appendix B: Migration"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Gotchas"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Glossary"]', output, 1
    end

    test 'should only number sections in toc up to value defined by sectnumlevels attribute' do
      input = <<~'EOS'
      :numbered:
      :toc:
      :sectnumlevels: 2
      :toclevels: 3

      == Level 1

      === Level 2

      ==== Level 3
      EOS

      output = convert_string input
      assert_xpath '//*[@id="toc"]//a[@href="#_level_1"][text()="1. Level 1"]', output, 1
      assert_xpath '//*[@id="toc"]//a[@href="#_level_2"][text()="1.1. Level 2"]', output, 1
      assert_xpath '//*[@id="toc"]//a[@href="#_level_3"][text()="Level 3"]', output, 1
    end

    test 'should not number special sections or their subsections by default except for appendices' do
      input = <<~'EOS'
      :doctype: book
      :sectnums:

      [preface]
      == Preface

      === Preface Subsection

      content

      == Section One

      content

      [appendix]
      == Attribute Options

      Details

      [appendix]
      == Migration

      Details

      === Gotchas

      Details

      [glossary]
      == Glossary

      Terms
      EOS

      output = convert_string_to_embedded input
      assert_xpath '(//h2)[1][text()="Preface"]', output, 1
      assert_xpath '(//h3)[1][text()="Preface Subsection"]', output, 1
      assert_xpath '(//h2)[2][text()="1. Section One"]', output, 1
      assert_xpath '(//h2)[3][text()="Appendix A: Attribute Options"]', output, 1
      assert_xpath '(//h2)[4][text()="Appendix B: Migration"]', output, 1
      assert_xpath '(//h3)[2][text()="B.1. Gotchas"]', output, 1
      assert_xpath '(//h2)[5][text()="Glossary"]', output, 1
    end

    test 'should not number special sections or their subsections in toc by default except for appendices' do
      input = <<~'EOS'
      :doctype: book
      :sectnums:
      :toc:

      [preface]
      == Preface

      === Preface Subsection

      content

      == Section One

      content

      [appendix]
      == Attribute Options

      Details

      [appendix]
      == Migration

      Details

      === Gotchas

      Details

      [glossary]
      == Glossary

      Terms
      EOS

      output = convert_string input
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Preface"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Preface Subsection"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="1. Section One"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Appendix A: Attribute Options"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Appendix B: Migration"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="B.1. Gotchas"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Glossary"]', output, 1
    end

    test 'should number special sections and their subsections when sectnums is all' do
      input = <<~'EOS'
      :doctype: book
      :sectnums: all

      [preface]
      == Preface

      === Preface Subsection

      content

      == Section One

      content

      [appendix]
      == Attribute Options

      Details

      [appendix]
      == Migration

      Details

      === Gotchas

      Details

      [glossary]
      == Glossary

      Terms
      EOS

      output = convert_string_to_embedded input
      assert_xpath '(//h2)[1][text()="1. Preface"]', output, 1
      assert_xpath '(//h3)[1][text()="1.1. Preface Subsection"]', output, 1
      assert_xpath '(//h2)[2][text()="2. Section One"]', output, 1
      assert_xpath '(//h2)[3][text()="Appendix A: Attribute Options"]', output, 1
      assert_xpath '(//h2)[4][text()="Appendix B: Migration"]', output, 1
      assert_xpath '(//h3)[2][text()="B.1. Gotchas"]', output, 1
      assert_xpath '(//h2)[5][text()="3. Glossary"]', output, 1
    end

    test 'should number special sections and their subsections in toc when sectnums is all' do
      input = <<~'EOS'
      :doctype: book
      :sectnums: all
      :toc:

      [preface]
      == Preface

      === Preface Subsection

      content

      == Section One

      content

      [appendix]
      == Attribute Options

      Details

      [appendix]
      == Migration

      Details

      === Gotchas

      Details

      [glossary]
      == Glossary

      Terms
      EOS

      output = convert_string input
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="1. Preface"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="1.1. Preface Subsection"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="2. Section One"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Appendix A: Attribute Options"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="Appendix B: Migration"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="B.1. Gotchas"]', output, 1
      assert_xpath '//*[@id="toc"]/ul//li/a[text()="3. Glossary"]', output, 1
    end

    test 'level 0 special sections in multipart book should be coerced to level 1' do
      input = <<~'EOS'
      = Multipart Book
      Doc Writer
      :doctype: book

      [preface]
      = Preface

      Preface text

      [appendix]
      = Appendix

      Appendix text
      EOS

      output = convert_string input
      assert_xpath '//h2[@id = "_preface"]', output, 1
      assert_xpath '//h2[@id = "_appendix"]', output, 1
    end

    test 'should output docbook elements that correspond to special sections in book doctype' do
      input = <<~'EOS'
      = Multipart Book
      :doctype: book
      :idprefix:

      [abstract]
      = Abstract Title

      Normal chapter (no abstract in book)

      [dedication]
      = Dedication Title

      Dedication content

      [preface]
      = Preface Title

      Preface content

      === Preface sub-section

      Preface subsection content

      = Part 1

      [partintro]
      .Part intro title
      Part intro content

      == Chapter 1

      blah blah

      == Chapter 2

      blah blah

      = Part 2

      [partintro]
      blah blah

      == Chapter 3

      blah blah

      == Chapter 4

      blah blah

      [appendix]
      = Appendix Title

      Appendix content

      === Appendix sub-section

      Appendix sub-section content

      [bibliography]
      = Bibliography Title

      Bibliography content

      [glossary]
      = Glossary Title

      Glossary content

      [colophon]
      = Colophon Title

      Colophon content

      [index]
      = Index Title
      EOS

      output = convert_string input, backend: 'docbook'
      assert_xpath '/book/chapter[@xml:id="abstract_title"]', output, 1
      assert_xpath '/book/chapter[@xml:id="abstract_title"]/title[text()="Abstract Title"]', output, 1
      assert_xpath '/book/chapter/following-sibling::dedication[@xml:id="dedication_title"]', output, 1
      assert_xpath '/book/chapter/following-sibling::dedication[@xml:id="dedication_title"]/title[text()="Dedication Title"]', output, 1
      assert_xpath '/book/dedication/following-sibling::preface[@xml:id="preface_title"]', output, 1
      assert_xpath '/book/dedication/following-sibling::preface[@xml:id="preface_title"]/title[text()="Preface Title"]', output, 1
      assert_xpath '/book/preface/section[@xml:id="preface_sub_section"]', output, 1
      assert_xpath '/book/preface/section[@xml:id="preface_sub_section"]/title[text()="Preface sub-section"]', output, 1
      assert_xpath '/book/preface/following-sibling::part[@xml:id="part_1"]', output, 1
      assert_xpath '/book/preface/following-sibling::part[@xml:id="part_1"]/title[text()="Part 1"]', output, 1
      assert_xpath '/book/part[@xml:id="part_1"]/partintro', output, 1
      assert_xpath '/book/part[@xml:id="part_1"]/partintro/title[text()="Part intro title"]', output, 1
      assert_xpath '/book/part[@xml:id="part_1"]/partintro/following-sibling::chapter[@xml:id="chapter_1"]', output, 1
      assert_xpath '/book/part[@xml:id="part_1"]/partintro/following-sibling::chapter[@xml:id="chapter_1"]/title[text()="Chapter 1"]', output, 1
      assert_xpath '(/book/part)[2]/following-sibling::appendix[@xml:id="appendix_title"]', output, 1
      assert_xpath '(/book/part)[2]/following-sibling::appendix[@xml:id="appendix_title"]/title[text()="Appendix Title"]', output, 1
      assert_xpath '/book/appendix/section[@xml:id="appendix_sub_section"]', output, 1
      assert_xpath '/book/appendix/section[@xml:id="appendix_sub_section"]/title[text()="Appendix sub-section"]', output, 1
      assert_xpath '/book/appendix/following-sibling::bibliography[@xml:id="bibliography_title"]', output, 1
      assert_xpath '/book/appendix/following-sibling::bibliography[@xml:id="bibliography_title"]/title[text()="Bibliography Title"]', output, 1
      assert_xpath '/book/bibliography/following-sibling::glossary[@xml:id="glossary_title"]', output, 1
      assert_xpath '/book/bibliography/following-sibling::glossary[@xml:id="glossary_title"]/title[text()="Glossary Title"]', output, 1
      assert_xpath '/book/glossary/following-sibling::colophon[@xml:id="colophon_title"]', output, 1
      assert_xpath '/book/glossary/following-sibling::colophon[@xml:id="colophon_title"]/title[text()="Colophon Title"]', output, 1
      assert_xpath '/book/colophon/following-sibling::index[@xml:id="index_title"]', output, 1
      assert_xpath '/book/colophon/following-sibling::index[@xml:id="index_title"]/title[text()="Index Title"]', output, 1
    end

    test 'abstract section maps to abstract element in docbook for article doctype' do
      input = <<~'EOS'
      = Article
      :idprefix:

      [abstract]
      == Abstract Title

      Abstract content
      EOS

      output = convert_string input, backend: 'docbook'
      assert_xpath '/article/info/abstract[@xml:id="abstract_title"]', output, 1
      assert_xpath '/article/info/abstract[@xml:id="abstract_title"]/title[text()="Abstract Title"]', output, 1
    end

    test 'should allow a special section to be nested at arbitrary depth in DocBook output' do
      input = <<~'EOS'
      = Document Title
      :doctype: book

      == Glossaries

      [glossary]
      === Glossary A

      Glossaries are optional.
      Glossaries entries are an example of a style of AsciiDoc description lists.

      [glossary]
      A glossary term::
      The corresponding definition.

      A second glossary term::
      The corresponding definition.
      EOS

      output = convert_string input, backend: :docbook
      assert_xpath '//glossary', output, 1
      assert_xpath '//chapter/glossary', output, 1
      assert_xpath '//glossary/title[text()="Glossary A"]', output, 1
      assert_xpath '//glossary/glossentry', output, 2
    end

    test 'should drop title on special section in DocBook output if notitle or untitled option is set' do
      %w(notitle untitled).each do |option|
        input = <<~EOS
        [dedication%#{option}]
        == Dedication

        content
        EOS

        output = convert_string_to_embedded input, backend: :docbook
        assert_xpath '/dedication', output, 1
        assert_xpath '/dedication/title', output, 0
      end
    end
  end

  context 'heading patterns in blocks' do
    test 'should not interpret a listing block as a heading' do
      input = <<~'EOS'
      Section
      -------

      ----
      code
      ----

      fin.
      EOS
      output = convert_string input
      assert_xpath '//h2', output, 1
    end

    test 'should not interpret an open block as a heading' do
      input = <<~'EOS'
      Section
      -------

      --
      ha
      --

      fin.
      EOS
      output = convert_string input
      assert_xpath '//h2', output, 1
    end

    test 'should not interpret an attribute list as a heading' do
      input = <<~'EOS'
      Section
      =======

      preamble

      [TIP]
      ====
      This should be a tip, not a heading.
      ====
      EOS
      output = convert_string input
      assert_xpath '//*[@class="admonitionblock tip"]//p[text() = "This should be a tip, not a heading."]', output, 1
    end

    test 'should not match a heading in a description list' do
      input = <<~'EOS'
      Section
      -------

      term1::
      +
      ----
      list = [1, 2, 3];
      ----
      term2::
      == not a heading
      term3:: def

      //

      fin.
      EOS
      output = convert_string input
      assert_xpath '//h2', output, 1
      assert_xpath '//dl', output, 1
    end

    test 'should not match a heading in a bulleted list' do
      input = <<~'EOS'
      Section
      -------

      * first
      +
      ----
      list = [1, 2, 3];
      ----
      +
      * second
      == not a heading
      * third

      fin.
      EOS
      output = convert_string input
      assert_xpath '//h2', output, 1
      assert_xpath '//ul', output, 1
    end

    test 'should not match a heading in a block' do
      input = <<~'EOS'
      ====

      == not a heading

      ====
      EOS
      output = convert_string input
      assert_xpath '//h2', output, 0
      assert_xpath '//*[@class="exampleblock"]//p[text() = "== not a heading"]', output, 1
    end
  end

  context 'Table of Contents' do
    test 'should output unnumbered table of contents in header if toc attribute is set' do
      input = <<~'EOS'
      = Article
      :toc:

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...

      === Interlude

      While they were waiting...

      == Section Three

      That's all she wrote!
      EOS
      output = convert_string input
      assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/*[@id="toctitle"][text()="Table of Contents"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul[@class="sectlevel1"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]//ul', output, 2
      assert_xpath '//*[@id="header"]//*[@id="toc"]//li', output, 4
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="Section One"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul[@class="sectlevel2"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul/li', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul/li/a[@href="#_interlude"][text()="Interlude"]', output, 1
      assert_xpath '((//*[@id="header"]//*[@id="toc"]/ul)[1]/li)[3]/a[@href="#_section_three"][text()="Section Three"]', output, 1
    end

    test 'should output numbered table of contents in header if toc and numbered attributes are set' do
      input = <<~'EOS'
      = Article
      :toc:
      :numbered:

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...

      === Interlude

      While they were waiting...

      == Section Three

      That's all she wrote!
      EOS
      output = convert_string input
      assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/*[@id="toctitle"][text()="Table of Contents"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]//ul', output, 2
      assert_xpath '//*[@id="header"]//*[@id="toc"]//li', output, 4
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul/li', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul/li/a[@href="#_interlude"][text()="2.1. Interlude"]', output, 1
      assert_xpath '((//*[@id="header"]//*[@id="toc"]/ul)[1]/li)[3]/a[@href="#_section_three"][text()="3. Section Three"]', output, 1
    end

    test 'should output a table of contents that honors numbered setting at position of section in document' do
      input = <<~'EOS'
      = Article
      :toc:
      :numbered:

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...

      === Interlude

      While they were waiting...

      :numbered!:

      == Section Three

      That's all she wrote!
      EOS
      output = convert_string input
      assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/*[@id="toctitle"][text()="Table of Contents"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]//ul', output, 2
      assert_xpath '//*[@id="header"]//*[@id="toc"]//li', output, 4
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
      assert_xpath '((//*[@id="header"]//*[@id="toc"]/ul)[1]/li)[3]/a[@href="#_section_three"][text()="Section Three"]', output, 1
    end

    test 'should not number parts in table of contents for book doctype when numbered attribute is set' do
      input = <<~'EOS'
      = Book
      :doctype: book
      :toc:
      :numbered:

      = Part 1

      == First Section of Part 1

      blah

      == Second Section of Part 1

      blah

      = Part 2

      == First Section of Part 2

      blah
      EOS

      output = convert_string input
      assert_xpath '//*[@id="toc"]', output, 1
      assert_xpath '//*[@id="toc"]/ul', output, 1
      assert_xpath '//*[@id="toc"]/ul[@class="sectlevel0"]', output, 1
      assert_xpath '//*[@id="toc"]/ul[@class="sectlevel0"]/li', output, 2
      assert_xpath '(//*[@id="toc"]/ul[@class="sectlevel0"]/li)[1]/a[text()="Part 1"]', output, 1
      assert_xpath '(//*[@id="toc"]/ul[@class="sectlevel0"]/li)[2]/a[text()="Part 2"]', output, 1
      assert_xpath '(//*[@id="toc"]/ul[@class="sectlevel0"]/li)[1]/ul', output, 1
      assert_xpath '(//*[@id="toc"]/ul[@class="sectlevel0"]/li)[1]/ul[@class="sectlevel1"]', output, 1
      assert_xpath '(//*[@id="toc"]/ul[@class="sectlevel0"]/li)[1]/ul/li', output, 2
      assert_xpath '((//*[@id="toc"]/ul[@class="sectlevel0"]/li)[1]/ul/li)[1]/a[text()="1. First Section of Part 1"]', output, 1
    end

    test 'should output table of contents in header if toc2 attribute is set' do
      input = <<~'EOS'
      = Article
      :toc2:
      :numbered:

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string input
      assert_xpath '//body[@class="article toc2 toc-left"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
    end

    test 'should set toc position if toc attribute is set to position' do
      input = <<~'EOS'
      = Article
      :toc: >
      :numbered:

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string input
      assert_xpath '//body[@class="article toc2 toc-right"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
    end

    test 'should set toc position if toc and toc-position attributes are set' do
      input = <<~'EOS'
      = Article
      :toc:
      :toc-position: right
      :numbered:

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string input
      assert_xpath '//body[@class="article toc2 toc-right"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
    end

    test 'should set toc position if toc2 and toc-position attribute are set' do
      input = <<~'EOS'
      = Article
      :toc2:
      :toc-position: right
      :numbered:

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string input
      assert_xpath '//body[@class="article toc2 toc-right"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
    end

    test 'should set toc position if toc attribute is set to direction' do
      input = <<~'EOS'
      = Article
      :toc: right
      :numbered:

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string input
      assert_xpath '//body[@class="article toc2 toc-right"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
    end

    test 'should set toc placement to preamble if toc attribute is set to preamble' do
      input = <<~'EOS'
      = Article
      :toc: preamble

      Yada yada

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string input
      assert_css '#preamble #toc', output, 1
      assert_css '#preamble .sectionbody + #toc', output, 1
    end

    test 'should use document attributes toc-class, toc-title and toclevels to create toc' do
      input = <<~'EOS'
      = Article
      :toc:
      :toc-title: Contents
      :toc-class: toc2
      :toclevels: 1

      == Section 1

      === Section 1.1

      ==== Section 1.1.1

      ==== Section 1.1.2

      === Section 1.2

      == Section 2

      Fin.
      EOS
      output = convert_string input
      assert_css '#header #toc', output, 1
      assert_css '#header #toc.toc2', output, 1
      assert_css '#header #toc li', output, 2
      assert_css '#header #toc #toctitle', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/*[@id="toctitle"][text()="Contents"]', output, 1
    end

    test 'should not output table of contents if toc-placement attribute is unset' do
      input = <<~'EOS'
      = Article
      :toc:
      :toc-placement!:

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string input
      assert_xpath '//*[@id="toc"]', output, 0
    end

    test 'should output table of contents at location of toc macro' do
      input = <<~'EOS'
      = Article
      :toc:
      :toc-placement: macro

      Once upon a time...

      toc::[]

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string input
      assert_css '#preamble #toc', output, 1
      assert_css '#preamble .paragraph + #toc', output, 1
    end

    test 'should output table of contents at location of toc macro in embedded document' do
      input = <<~'EOS'
      = Article
      :toc:
      :toc-placement: macro

      Once upon a time...

      toc::[]

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string_to_embedded input
      assert_css '#preamble:root #toc', output, 1
      assert_css '#preamble:root .paragraph + #toc', output, 1
    end

    test 'should output table of contents at default location in embedded document if toc attribute is set' do
      input = <<~'EOS'
      = Article
      :showtitle:
      :toc:

      Once upon a time...

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string_to_embedded input
      assert_css 'h1:root', output, 1
      assert_css 'h1:root + #toc:root', output, 1
      assert_css 'h1:root + #toc:root + #preamble:root', output, 1
    end

    test 'should not activate toc macro if toc-placement is not set' do
      input = <<~'EOS'
      = Article
      :toc:

      Once upon a time...

      toc::[]

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string input

      assert_css '#toc', output, 1
      assert_css '#toctitle', output, 1
      assert_css '.toc', output, 1
      assert_css '#content .toc', output, 0
    end

    test 'should only output toc at toc macro if toc is macro' do
      input = <<~'EOS'
      = Article
      :toc: macro

      Once upon a time...

      toc::[]

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...
      EOS

      output = convert_string input

      assert_css '#toc', output, 1
      assert_css '#toctitle', output, 1
      assert_css '.toc', output, 1
      assert_css '#content .toc', output, 1
    end

    test 'should use global attributes for toc-title, toc-class and toclevels for toc macro' do
      input = <<~'EOS'
      = Article
      :toc:
      :toc-placement: macro
      :toc-title: Contents
      :toc-class: contents
      :toclevels: 1

      Preamble.

      toc::[]

      == Section 1

      === Section 1.1

      ==== Section 1.1.1

      ==== Section 1.1.2

      === Section 1.2

      == Section 2

      Fin.
      EOS

      output = convert_string input
      assert_css '#toc', output, 1
      assert_css '#toctitle', output, 1
      assert_css '#preamble #toc', output, 1
      assert_css '#preamble #toc.contents', output, 1
      assert_xpath '//*[@id="toc"]/*[@class="title"][text() = "Contents"]', output, 1
      assert_css '#toc li', output, 2
      assert_xpath '(//*[@id="toc"]//li)[1]/a[text() = "Section 1"]', output, 1
      assert_xpath '(//*[@id="toc"]//li)[2]/a[text() = "Section 2"]', output, 1
    end

    test 'should honor id, title, role and level attributes on toc macro' do
      input = <<~'EOS'
      = Article
      :toc:
      :toc-placement: macro
      :toc-title: Ignored
      :toc-class: ignored
      :toclevels: 5
      :tocdepth: 1

      Preamble.

      [[contents]]
      [role="contents"]
      .Contents
      toc::[levels={tocdepth}]

      == Section 1

      === Section 1.1

      ==== Section 1.1.1

      ==== Section 1.1.2

      === Section 1.2

      == Section 2

      Fin.
      EOS

      output = convert_string input
      assert_css '#toc', output, 0
      assert_css '#toctitle', output, 0
      assert_css '#preamble #contents', output, 1
      assert_css '#preamble #contents.contents', output, 1
      assert_xpath '//*[@id="contents"]/*[@class="title"][text() = "Contents"]', output, 1
      assert_css '#contents li', output, 2
      assert_xpath '(//*[@id="contents"]//li)[1]/a[text() = "Section 1"]', output, 1
      assert_xpath '(//*[@id="contents"]//li)[2]/a[text() = "Section 2"]', output, 1
    end

    test 'child toc levels should not have additional bullet at parent level in html' do
      input = <<~'EOS'
      = Article
      :toc:

      == Section One

      It was a dark and stormy night...

      == Section Two

      They couldn't believe their eyes when...

      === Interlude

      While they were waiting...

      == Section Three

      That's all she wrote!
      EOS
      output = convert_string input
      assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/*[@id="toctitle"][text()="Table of Contents"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]//ul', output, 2
      assert_xpath '//*[@id="header"]//*[@id="toc"]//li', output, 4
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[2]/a[@href="#_section_two"][text()="Section Two"]', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul/li', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[2]/ul/li', output, 1
      assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul/li/a[@href="#_interlude"][text()="Interlude"]', output, 1
      assert_xpath '((//*[@id="header"]//*[@id="toc"]/ul)[1]/li)[3]/a[@href="#_section_three"][text()="Section Three"]', output, 1
    end

    test 'should not display a table of contents if document has no sections' do
      input_src = <<~'EOS'
      = Document Title
      :toc:

      toc::[]

      This document has no sections.

      It only has content.
      EOS

      ['', 'left', 'preamble', 'macro'].each do |placement|
        input = input_src.gsub ':toc:', "\\& #{placement}"
        output = convert_string input
        assert_css '#toctitle', output, 0
      end
    end

    test 'should drop anchors from contents of entries in table of contents' do
      input = <<~'EOS'
      = Document Title
      :toc:

      == [[un]]Section One

      content

      == [[two]][[deux]]Section Two

      content

      == Plant Trees by https://ecosia.org[Searching]

      content
      EOS

      output = convert_string_to_embedded input
      assert_xpath '/*[@id="toc"]', output, 1
      toc_links = xmlnodes_at_xpath '/*[@id="toc"]//li', output
      assert_equal 3, toc_links.size
      assert_equal '<a href="#_section_one">Section One</a>', toc_links[0].inner_html
      assert_equal '<a href="#_section_two">Section Two</a>', toc_links[1].inner_html
      assert_equal '<a href="#_plant_trees_by_searching">Plant Trees by Searching</a>', toc_links[2].inner_html
    end

    test 'should not remove non-anchor tags from contents of entries in table of contents' do
      input = <<~'EOS'
      = Document Title
      :toc:
      :icons: font

      == `run` command

      content

      == icon:bug[] Issues

      content

      == https://ecosia.org[_Sustainable_ Searches]

      content
      EOS

      output = convert_string_to_embedded input, safe: :safe
      assert_xpath '/*[@id="toc"]', output, 1
      toc_links = xmlnodes_at_xpath '/*[@id="toc"]//li', output
      assert_equal 3, toc_links.size
      assert_equal '<a href="#_run_command"><code>run</code> command</a>', toc_links[0].inner_html
      assert_equal '<a href="#_issues"><span class="icon"><i class="fa fa-bug"></i></span> Issues</a>', toc_links[1].inner_html
      assert_equal '<a href="#_sustainable_searches"><em>Sustainable</em> Searches</a>', toc_links[2].inner_html
    end
  end

  context 'article doctype' do
    test 'should create only sections in docbook backend' do
      input = <<~'EOS'
      = Article
      Doc Writer

      == Section 1

      The adventure.

      === Subsection One

      It was a dark and stormy night...

      === Subsection Two

      They couldn't believe their eyes when...

      == Section 2

      The return.

      === Subsection Three

      While they were returning...

      === Subsection Four

      That's all she wrote!
      EOS

      output = convert_string input, backend: 'docbook'
      assert_xpath '//part', output, 0
      assert_xpath '//chapter', output, 0
      assert_xpath '/article/section', output, 2
      assert_xpath '/article/section[1]/title[text() = "Section 1"]', output, 1
      assert_xpath '/article/section[2]/title[text() = "Section 2"]', output, 1
      assert_xpath '/article/section/section', output, 4
      assert_xpath '/article/section[1]/section[1]/title[text() = "Subsection One"]', output, 1
      assert_xpath '/article/section[2]/section[1]/title[text() = "Subsection Three"]', output, 1
    end
  end

  context 'book doctype' do
    test 'document title with level 0 headings' do
      input = <<~'EOS'
      = Book
      Doc Writer
      :doctype: book

      = Chapter One

      [partintro]
      It was a dark and stormy night...

      == Scene One

      Someone's gonna get axed.

      = Chapter Two

      [partintro]
      They couldn't believe their eyes when...

      == Interlude

      While they were waiting...

      = Chapter Three

      == Scene One

      That's all she wrote!
      EOS

      output = convert_string input
      assert_css 'body.book', output, 1
      assert_css 'h1', output, 4
      assert_css '#header h1', output, 1
      assert_css '#content h1', output, 3
      assert_css '#content h1.sect0', output, 3
      assert_css 'h2', output, 3
      assert_css '#content h2', output, 3
      assert_xpath '//h1[@id="_chapter_one"][text() = "Chapter One"]', output, 1
      assert_xpath '//h1[@id="_chapter_two"][text() = "Chapter Two"]', output, 1
      assert_xpath '//h1[@id="_chapter_three"][text() = "Chapter Three"]', output, 1
      assert_css '#_chapter_one + .openblock.partintro p', output, 1
      assert_css '#_chapter_two + .openblock.partintro p', output, 1
    end

    test 'should print error if level 0 section comes after nested section and doctype is not book' do
      input = <<~'EOS'
      = Document Title

      == Level 1 Section

      === Level 2 Section

      = Level 0 Section
      EOS

      using_memory_logger do |logger|
        convert_string input
        assert_message logger, :ERROR, '<stdin>: line 7: level 0 sections can only be used when doctype is book', Hash
      end
    end

    test 'should add class matching role to part' do
      input = <<~'EOS'
      = Book Title
      :doctype: book

      [.newbie]
      = Part 1

      == Chapter A

      content

      = Part 2

      == Chapter B

      content
      EOS

      result = convert_string_to_embedded input
      assert_css 'h1.sect0', result, 2
      assert_css 'h1.sect0.newbie', result, 1
      assert_css 'h1.sect0.newbie#_part_1', result, 1
    end

    test 'should assign appropriate sectname for section type' do
      input = <<~'EOS'
      = Book Title
      :doctype: book
      :idprefix:
      :idseparator: -

      = Part Title

      == Chapter Title

      === Section Title

      content

      [appendix]
      == Appendix Title

      === Appendix Section Title

      content
      EOS

      doc = document_from_string input
      assert_equal 'header', doc.header.sectname
      assert_equal 'part', (doc.find_by id: 'part-title')[0].sectname
      assert_equal 'chapter', (doc.find_by id: 'chapter-title')[0].sectname
      assert_equal 'section', (doc.find_by id: 'section-title')[0].sectname
      assert_equal 'appendix', (doc.find_by id: 'appendix-title')[0].sectname
      assert_equal 'section', (doc.find_by id: 'appendix-section-title')[0].sectname
    end

    test 'should allow part intro to be defined using special section' do
      input = <<~'EOS'
      = Book
      :doctype: book

      = Part 1

      [partintro]
      == Part Intro

      Part intro content

      == Chapter 1

      Chapter content
      EOS

      output = convert_string input, backend: 'docbook'
      assert_xpath '/book/part[@xml:id="_part_1"]', output, 1
      assert_xpath '/book/part[@xml:id="_part_1"]/partintro', output, 1
      assert_xpath '/book/part[@xml:id="_part_1"]/partintro[@xml:id="_part_intro"]', output, 1
      assert_xpath '/book/part[@xml:id="_part_1"]/partintro[@xml:id="_part_intro"]/title[text()="Part Intro"]', output, 1
      assert_xpath '/book/part[@xml:id="_part_1"]/partintro[@xml:id="_part_intro"]/following-sibling::chapter[@xml:id="_chapter_1"]', output, 1
    end

    test 'should add partintro style to child paragraph of part' do
      input = <<~'EOS'
      = Book
      :doctype: book

      = Part 1

      part intro--a summary

      == Chapter 1
      EOS

      doc = document_from_string input
      partintro = doc.blocks.first.blocks.first
      assert_equal :open, partintro.context
      assert_equal :compound, partintro.content_model
      assert_empty partintro.lines
      assert_empty partintro.subs
      assert_equal 'partintro', partintro.style
      assert_equal :paragraph, partintro.blocks[0].context
      assert_equal ['part intro--a summary'], partintro.blocks[0].lines
      assert_include 'part intro&#8212;&#8203;a summary', partintro.convert
    end

    test 'should preserve title on partintro defined as partintro paragraph' do
      input = <<~'EOS'
      = Book
      :doctype: book

      = Part 1

      .Intro
      [partintro]
      Read this first.

      == Chapter 1
      EOS

      doc = document_from_string input
      partintro = doc.blocks.first.blocks.first
      assert_equal :open, partintro.context
      assert_equal 'Intro', partintro.title
    end

    test 'should not promote title on partintro defined as normal paragraph' do
      input = <<~'EOS'
      = Book
      :doctype: book

      = Part 1

      .Intro
      Read this first.

      == Chapter 1
      EOS

      doc = document_from_string input
      partintro = doc.blocks.first.blocks.first
      assert_equal :open, partintro.context
      assert_nil partintro.title
      assert_equal 'Intro', partintro.blocks[0].title
    end

    test 'should add partintro style to child open block of part' do
      input = <<~'EOS'
      = Book
      :doctype: book

      = Part 1

      --
      part intro
      --

      == Chapter 1
      EOS

      doc = document_from_string input
      partintro = doc.blocks.first.blocks.first
      assert_equal :open, partintro.context
      assert_equal :compound, partintro.content_model
      assert_equal 'partintro', partintro.style
      assert_equal :paragraph, partintro.blocks[0].context
    end

    test 'should wrap child paragraphs of part in partintro open block' do
      input = <<~'EOS'
      = Book
      :doctype: book

      = Part 1

      part intro

      more part intro

      == Chapter 1
      EOS

      doc = document_from_string input
      partintro = doc.blocks.first.blocks.first
      assert_equal :open, partintro.context
      assert_equal :compound, partintro.content_model
      assert_equal 'partintro', partintro.style
      assert_equal 2, partintro.blocks.size
      assert_equal :paragraph, partintro.blocks[0].context
      assert_equal :paragraph, partintro.blocks[1].context
    end

    test 'should wrap abstract in implicit part intro in info tag when converting to DocBook' do
      input = <<~'EOS'
      = Book
      :doctype: book

      = Part 1

      [abstract]
      Abstract of part.

      more part intro

      == Chapter 1
      EOS

      output = convert_string input, backend: 'docbook'
      assert_xpath '//abstract', output, 1
      assert_xpath '//partintro/info/abstract', output, 1
    end

    test 'should wrap abstract in part intro section in info tag when converting to DocBook' do
      input = <<~'EOS'
      = Book
      :doctype: book

      = Part 1

      [partintro]
      == Part Intro

      [abstract]
      Abstract of part.

      more part intro

      == Chapter 1
      EOS

      output = convert_string input, backend: 'docbook'
      assert_xpath '//abstract', output, 1
      assert_xpath '//partintro/info/abstract', output, 1
      assert_xpath '//partintro/simpara', output, 1
    end

    test 'should warn if part has no sections' do
      input = <<~'EOS'
      = Book
      :doctype: book

      = Part 1

      [partintro]
      intro
      EOS

      using_memory_logger do |logger|
        document_from_string input
        assert_message logger, :ERROR, '<stdin>: line 8: invalid part, must have at least one section (e.g., chapter, appendix, etc.)', Hash
      end
    end

    test 'should create parts and chapters in docbook backend' do
      input = <<~'EOS'
      = Book
      Doc Writer
      :doctype: book

      = Part 1

      [partintro]
      The adventure.

      == Chapter One

      It was a dark and stormy night...

      == Chapter Two

      They couldn't believe their eyes when...

      = Part 2

      [partintro]
      The return.

      == Chapter Three

      While they were returning...

      == Chapter Four

      That's all she wrote!
      EOS

      output = convert_string input, backend: 'docbook'
      assert_xpath '//chapter/chapter', output, 0
      assert_xpath '/book/part', output, 2
      assert_xpath '/book/part[1]/title[text() = "Part 1"]', output, 1
      assert_xpath '/book/part[2]/title[text() = "Part 2"]', output, 1
      assert_xpath '/book/part/chapter', output, 4
      assert_xpath '/book/part[1]/chapter[1]/title[text() = "Chapter One"]', output, 1
      assert_xpath '/book/part[2]/chapter[1]/title[text() = "Chapter Three"]', output, 1
    end

    test 'subsections in preface and appendix should start at level 2' do
      input = <<~'EOS'
      = Multipart Book
      Doc Writer
      :doctype: book

      [preface]
      = Preface

      Preface content

      === Preface subsection

      Preface subsection content

      = Part 1

      .Part intro title
      [partintro]
      Part intro content

      == Chapter 1

      content

      [appendix]
      = Appendix

      Appendix content

      === Appendix subsection

      Appendix subsection content
      EOS

      output = nil
      using_memory_logger do |logger|
        output = convert_string input, backend: 'docbook'
        assert_empty logger
      end
      assert_xpath '/book/preface', output, 1
      assert_xpath '/book/preface/section', output, 1
      assert_xpath '/book/part', output, 1
      assert_xpath '/book/part/partintro', output, 1
      assert_xpath '/book/part/partintro/title', output, 1
      assert_xpath '/book/part/partintro/simpara', output, 1
      assert_xpath '/book/appendix', output, 1
      assert_xpath '/book/appendix/section', output, 1
    end
  end
end
