# encoding: UTF-8
unless defined? ASCIIDOCTOR_PROJECT_DIR
  $: << File.dirname(__FILE__); $:.uniq!
  require 'test_helper'
end

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 '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 '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
      reftext = doc.catalog[:ids]['install']
      refute_nil reftext
      assert_equal 'Install Procedure', reftext
    end

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

content
      EOS

      doc = document_from_string input
      reftext = doc.catalog[:ids]['_install']
      refute_nil reftext
      assert_equal 'Install Procedure', reftext
    end

    test 'should substitute attributes when registering reftext for section' do
      input = <<-EOS
:platform-name: Linux

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

content
      EOS

      doc = document_from_string input
      reftext = doc.catalog[:ids]['install']
      refute_nil reftext
      assert_equal 'install on Linux', reftext
    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
        reftext = doc.catalog[:ids]['install']
        refute_nil reftext
        assert_equal 'First Install', reftext
        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
        reftext = doc.catalog[:ids]['_do_not_repeat_yourself']
        refute_nil reftext
        assert_equal 'Do Not Repeat Yourself', reftext
        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
        reftext = doc.catalog[:ids]['install']
        refute_nil reftext
        assert_equal 'First Install', reftext
        assert_message logger, :WARN, '<stdin>: line 7: id assigned to block already in use: install', Hash
      end
    end
  end

  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 logger.empty?
        logger.clear
        output = convert_string(title + "\n" + chars + "\n")
        assert_xpath '//h1', output, 0
        refute logger.empty?
      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 logger.empty?
        logger.clear
        output = convert_string(title + "\n" + chars + "\n")
        assert_xpath '//h1', output, 0
        refute logger.empty?
      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 logger.empty?
      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 discard style, role and options shorthand attributes defined on document title' do
      input = <<-EOS
[style#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 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 logger.empty?
      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("== 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 = <<-EOS
== Asciidoctor in 中文
      EOS
      output = convert_string input
      if ::RUBY_MIN_VERSION_1_9
        assert_xpath '//h2[@id="_asciidoctor_in_中文"][text()="Asciidoctor in 中文"]', output
      else
        assert_xpath '//h2[@id="_asciidoctor_in"][text()="Asciidoctor in 中文"]', output
      end
    end

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

    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 if ::RUBY_MIN_VERSION_1_9
  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

  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 logger.empty?
      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 = <<-EOS
# Document Title
      EOS
      output = convert_string input
      assert_xpath "//h1[not(@id)][text() = 'Document Title']", output, 1
    end

    test 'atx document title with symmetric markers' do
      input = <<-EOS
# Document Title #
      EOS
      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 = <<-EOS
=#= My Title
      EOS
      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[:ids].has_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[:ids].has_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
      reftext = doc.catalog[:ids]['install']
      refute_nil reftext
      assert_equal 'Install Procedure', reftext
    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
      reftext = doc.catalog[:ids]['_install']
      refute_nil reftext
      assert_equal 'Install Procedure', reftext
    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 block.id, 'install'
      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
= Master Document
Doc Writer

text in master

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

text in standalone

// 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
= Master Document
Doc Writer

Master 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 Master

Master section text.
      EOS

      output = nil
      using_memory_logger do |logger|
        output = convert_string input
        assert logger.empty?
      end

      assert_match(/Master 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 Master"]', output, 1
    end

    test 'level offset should be added to discrete heading' do
      input = <<-EOS
= Master 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
= Master Document
Doc Writer

Master 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
= Master Document
Doc Writer

Master 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 
      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 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 '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 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, sections[index].numeral
        assert_equal index + 1, sections[index].number
      end
    end

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

== The End
      EOS

      doc = document_from_string input, :attributes => { 'sectnums' => '' }
      doc.sections.each do |sect|
        sect.number += 1
      end

      output = doc.convert :header_footer => false
      assert_xpath '//h2[text()="2. Somewhere in the Middle"]', output, 1
      assert_xpath '//h2[text()="3. 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 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_equal true, appendix.numbered
    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 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_to_embedded input, :backend => 'docbook45'
      assert_xpath '/chapter[@id="abstract_title"]', output, 1
      assert_xpath '/chapter[@id="abstract_title"]/title[text()="Abstract Title"]', output, 1
      assert_xpath '/chapter/following-sibling::dedication[@id="dedication_title"]', output, 1
      assert_xpath '/chapter/following-sibling::dedication[@id="dedication_title"]/title[text()="Dedication Title"]', output, 1
      assert_xpath '/dedication/following-sibling::preface[@id="preface_title"]', output, 1
      assert_xpath '/dedication/following-sibling::preface[@id="preface_title"]/title[text()="Preface Title"]', output, 1
      assert_xpath '/preface/section[@id="preface_sub_section"]', output, 1
      assert_xpath '/preface/section[@id="preface_sub_section"]/title[text()="Preface sub-section"]', output, 1
      assert_xpath '/preface/following-sibling::part[@id="part_1"]', output, 1
      assert_xpath '/preface/following-sibling::part[@id="part_1"]/title[text()="Part 1"]', output, 1
      assert_xpath '/part[@id="part_1"]/partintro', output, 1
      assert_xpath '/part[@id="part_1"]/partintro/title[text()="Part intro title"]', output, 1
      assert_xpath '/part[@id="part_1"]/partintro/following-sibling::chapter[@id="chapter_1"]', output, 1
      assert_xpath '/part[@id="part_1"]/partintro/following-sibling::chapter[@id="chapter_1"]/title[text()="Chapter 1"]', output, 1
      assert_xpath '(/part)[2]/following-sibling::appendix[@id="appendix_title"]', output, 1
      assert_xpath '(/part)[2]/following-sibling::appendix[@id="appendix_title"]/title[text()="Appendix Title"]', output, 1
      assert_xpath '/appendix/section[@id="appendix_sub_section"]', output, 1
      assert_xpath '/appendix/section[@id="appendix_sub_section"]/title[text()="Appendix sub-section"]', output, 1
      assert_xpath '/appendix/following-sibling::bibliography[@id="bibliography_title"]', output, 1
      assert_xpath '/appendix/following-sibling::bibliography[@id="bibliography_title"]/title[text()="Bibliography Title"]', output, 1
      assert_xpath '/bibliography/following-sibling::glossary[@id="glossary_title"]', output, 1
      assert_xpath '/bibliography/following-sibling::glossary[@id="glossary_title"]/title[text()="Glossary Title"]', output, 1
      assert_xpath '/glossary/following-sibling::colophon[@id="colophon_title"]', output, 1
      assert_xpath '/glossary/following-sibling::colophon[@id="colophon_title"]/title[text()="Colophon Title"]', output, 1
      assert_xpath '/colophon/following-sibling::index[@id="index_title"]', output, 1
      assert_xpath '/colophon/following-sibling::index[@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_to_embedded input, :backend => 'docbook45'
      assert_xpath '/abstract[@id="abstract_title"]', output, 1
      assert_xpath '/abstract[@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 untitled option is set' do
      input = <<-EOS
[dedication%untitled]
== Dedication

content
      EOS

      output = convert_string_to_embedded input, :backend => :docbook
      assert_xpath '/dedication', output, 1
      assert_xpath '/dedication/title', output, 0
    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
    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 add partintro style to child paragraph 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 'partintro', partintro.style
    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 'partintro', partintro.style
    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 '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 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 logger.empty?
      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
