[DRE-commits] [SCM] ruby-asciidoctor.git branch, master, updated. upstream/0.1.1-8-g46a976d

Per Andersson avtobiff at gmail.com
Fri Jun 7 01:36:24 UTC 2013


The following commit has been merged in the master branch:
commit 879251870ff4b13c85d432f56942441aec486322
Author: Per Andersson <avtobiff at gmail.com>
Date:   Fri Jun 7 03:01:45 2013 +0200

    Imported Upstream version 0.1.3~ds1

diff --git a/Gemfile b/Gemfile
index e45e65f..efb0d3c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,2 +1,12 @@
-source :rubygems
+source 'https://rubygems.org'
+
 gemspec
+
+# enable this group to use Guard for continuous testing
+# after removing comments, run `bundle install` then `guard` 
+#group :guardtest do
+#  gem 'guard'
+#  gem 'guard-test'
+#  gem 'libnotify'
+#  gem 'listen', :github => 'guard/listen'
+#end
diff --git a/Guardfile b/Guardfile
new file mode 100644
index 0000000..be32090
--- /dev/null
+++ b/Guardfile
@@ -0,0 +1,18 @@
+# use `guard start -n f` to disable notifications
+# or set the environment variable GUARD_NOTIFY=false
+notification :libnotify,
+  :display_message => true,
+  :timeout => 5, # in seconds
+  :append => false,
+  :transient => true,
+  :urgency => :critical
+
+guard :test do
+  watch(%r{^lib/(.+)\.rb$}) do |m|
+    "test/#{m[1]}_test.rb"
+  end
+  watch(%r{^test.+_test\.rb$})
+  watch('test/test_helper.rb') do
+    "test"
+  end
+end
diff --git a/LICENSE b/LICENSE
index aa169d8..6c8b493 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License
 
-Copyright (c) Ryan Waldron
+Copyright (C) 2012-2013 Dan Allen and Ryan Waldron
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
\ No newline at end of file
+THE SOFTWARE.
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..b6f29a7
--- /dev/null
+++ b/README.adoc
@@ -0,0 +1,505 @@
+= Asciidoctor
+:awestruct-layout: base
+:homepage: http://asciidoctor.org
+:asciidoc: http://asciidoc.org
+:sources: https://github.com/asciidoctor/asciidoctor
+:issues: https://github.com/asciidoctor/asciidoctor/issues
+:forum: http://discuss.asciidoctor.org
+:org: https://github.com/asciidoctor
+:contributors: https://github.com/asciidoctor/asciidoctor/graphs/contributors
+:templates: https://github.com/asciidoctor/asciidoctor/blob/master/lib/asciidoctor/backends
+:gitscm-next: https://github.com/github/gitscm-next
+:seed-contribution: https://github.com/github/gitscm-next/commits/master/lib/asciidoc.rb
+:tilt: https://github.com/rtomayko/tilt
+:freesoftware: http://www.gnu.org/philosophy/free-sw.html
+:gist: https://gist.github.com
+:fork: https://help.github.com/articles/fork-a-repo
+:branch: http://learn.github.com/p/branching.html
+:pr: https://help.github.com/articles/using-pull-requests
+:license: https://github.com/asciidoctor/asciidoctor/blob/master/LICENSE
+:idprefix:
+:idseparator: -
+
+{homepage}[Asciidoctor] is a pure Ruby processor for converting
+{asciidoc}[AsciiDoc] source files and strings into HTML 5, DocBook 4.5
+and other formats. It's http://rubygems.org/gems/asciidoctor[published
+as a RubyGem] and is available under the MIT open source license.
+
+ifndef::awestruct[]
+image::https://travis-ci.org/asciidoctor/asciidoctor.png?branch=master[Build Status, link="https://travis-ci.org/asciidoctor/asciidoctor"]
+endif::awestruct[]
+
+Asciidoctor uses a set of built-in ERB templates to render the document
+to HTML 5 or DocBook 4.5. We've matched the rendered output as close as
+possible to the default output of the native Python processor. You can
+override this behavior by providing {tilt}[Tilt]-compatible templates.
+See the <<usage>> section for more details.
+
+Asciidoctor currently works (read as 'tested') with Ruby 1.8.7, Ruby
+1.9.3, Ruby 2.0.0, JRuby 1.7.4 and Rubinius nightly (on Linux, Mac and
+Windows). We expect it will work with other versions of Ruby as well and
+would welcome help in testing it out.
+
+The initial code from which Asciidoctor emerged was written by
+http://github.com/nickh[Nick Hengeveld] to process the git man pages for
+the {gitscm-next}[Git project site]. Refer to commit history of
+{seed-contribution}[asciidoc.rb] to view the initial contributions and
+contributors.
+
+The source code can now be found in the {sources}[Asciidoctor source
+repository] on GitHub.
+
+== Installation
+
+NOTE: We're working hard to make Asciidoctor a drop-in replacement for
+AsciiDoc. We're very close, with over 700 tests that ensure
+compatibility. The march is on towards full compliance and beyond.
+
+To install the gem:
+
+ gem install asciidoctor
+
+Or if you prefer bundler, add the asciidoctor gem to your Gemfile,
+
+ source 'https://rubygems.org'
+ gem 'asciidoctor'
+
+then install it using bundler:
+
+ bundle install
+
+If you're running Fedora, you can install the gem using yum:
+
+ sudo yum install rubygem-asciidoctor
+
+The benefit of installing the gem via yum is that yum will also install
+Ruby if it's not already on your machine.
+
+== Usage
+
+Asciidoctor has both a command line interface (CLI) and an API. The CLI
+is a drop-in replacement for the +asciidoc.py+ command from the Python
+implementation. The API is intended for integration with other software
+projects and is suitable for server-side applications, such as Rails,
+Sinatra and GitHub.
+
+=== Command line interface (CLI)
+
+After installing the +asciidoctor+ gem, the +asciidoctor+ commandline
+interface should be available on your PATH. To invoke it, simply execute:
+
+ asciidoctor <asciidoc_file>
+
+This will use the built-in defaults for options and create a new file in
+the same directory as the input file, with the same base name, but with
+the .html extension.
+
+There are many other options available and full help is provided via:
+
+ asciidoctor --help
+
+or in the http://asciidoctor.org/man/asciidoctor[man page].
+
+There is also an +asciidoctor-safe+ command, which turns on safe mode by
+default, preventing access to files outside the parent directory of the
+source file. This mode is very similar to the safe mode of
++asciidoc.py+.
+
+=== Ruby API
+
+To use Asciidoctor in your application, you first need to require the
+gem:
+
+ require 'asciidoctor'
+
+With that in place, you can start processing AsciiDoc documents.
+
+.Loading a document
+To parse a file into an +Asciidoctor::Document+ object:
+
+ doc = Asciidoctor.load_file('your_file.asciidoc')
+
+You can get information about the document:
+
+ puts doc.doctitle
+ puts doc.attributes
+
+More than likely, you want to just render the document.
+
+.Rendering files
+To render a file containing AsciiDoc markup to HTML 5:
+
+ Asciidoctor.render_file('your_file.asciidoc', :in_place => true)
+
+The command will output to the file +your_file.html+ in the same
+directory. You can render the file to DocBook 4.5 by setting the
++backend+ attribute to 'docbook':
+
+ Asciidoctor.render_file('your_file.asciidoc', :in_place => true,
+   :attributes => {'backend' => 'docbook'})
+
+The command will output to the file +your_file.xml+ in the same
+directory. (If you're on Linux, you can view the file using yelp).
+
+.Rendering strings
+To render an AsciiDoc-formatted string:
+
+ puts Asciidoctor.render('*This* is it.')
+
+When rendering a string, the header and footer are excluded by default
+to make Asciidoctor consistent with other lightweight markup engines
+like Markdown. If you want the header and footer, just enable it using
+the +:header_footer+ option:
+
+ puts Asciidoctor.render('*This* is it.', :header_footer => true)
+
+Now you'll get a full HTML 5 file. As before, you can also produce
+DocBook 4.5:
+
+ puts Asciidoctor.render('*This* is it.', :header_footer => true,
+   :attributes => {'backend' => 'docbook'})
+
+If you don't like the output you see, you can change it. Any of it!
+
+.Custom templates
+Asciidoctor allows you to override the {templates}[built-in templates]
+used to render almost any individual AsciiDoc element. If you provide a
+directory of {tilt}[Tilt]-compatible templates, named in such a way that
+Asciidoctor can figure out which template goes with which element,
+Asciidoctor will use the templates in this directory instead of its
+built-in templates for any elements for which it finds a matching
+template. It will fallback to its default templates for everything else.
+
+ puts Asciidoctor.render('*This* is it.', :header_footer => true,
+   :template_dir => 'templates')
+
+The Document and Section templates should begin with +document.+ and
++section.+, respectively. The file extension is used by Tilt to
+determine which view framework it will use to use to render the
+template. For instance, if you want to write the template in ERB, you'd
+name these two templates +document.html.erb+ and +section.html.erb+. To
+use Haml, you'd name them +document.html.haml+ and +section.html.haml+.
+
+Templates for block elements, like a Paragraph or Sidebar, would begin
+with +block_<style>.+. For instance, to override the default Paragraph
+template with an ERB template, put a file named
++block_paragraph.html.erb+ in the template directory you pass to the
++Document+ constructor using the +:template_dir+ option.
+
+For more usage examples, see the (massive) test suite.
+
+== Differences from AsciiDoc
+
+While Asciidoctor aims to be compliant with the AsciiDoc syntax, there
+are some differences which are important to keep in mind. In some cases,
+it's to enforce a rule we believe is too lax or ambiguous in AsciiDoc.
+In other cases, it's a tradeoff for speed, smarter processing or a
+feature we just haven't yet implemented. (You'll also notice that
+Asciidoctor executes about 25x as fast as AsciiDoc).
+
+Here are the known cases where Asciidoctor differs from AsciiDoc:
+
+* Asciidoctor enables safe mode by default when using the API
+  (+SafeMode::SECURE+)
+
+* Asciidoctor safe mode is even more safe than AsciiDoc's safe mode
+
+* Asciidoctor enforces symmetric block delimiters (the length of start
+  and end delimiters for a block must match!)
+
+* Section title underlines must be within 1 character of the length of
+  the title (AsciiDoc allows an offset of 3)
+
+* Asciidoctor's default HTML backend matches AsciiDoc's HTML 5 backend
+  (whereas XHTML 1.1 is the default HTML backend in AsciiDoc)
+
+* Asciidoctor adds viewport meta tag to +<head>+ to optimize mobile viewing
+
+* Asciidoctor handles inline anchors more cleanly
+
+** AsciiDoc adds an +<a>+ tag in the line and that markup gets caught in
+   the generated id
+
+** Asciidoctor promotes the id of the anchor as the section id
+
+* Asciidoctor strips XML entities from the section title before
+  generating the id (makes for cleaner section ids)
+
+* Asciidoctor uses +<code>+ instead of +<span class="monospace">+ around
+  inline literal text in the HTML backend
+
+* Asciidoctor does not wrap email next to author name in header in
+  angle brackets
+
+* Asciidoctor allows email field to be a URL and renders it as such
+
+* Asciidoctor is much more lenient about attribute list parsing (double
+  quotes are rarely needed, though you may want to keep them for
+  compatibility)
+
+* Asciidoctor adds the type attribute on ordered lists to provide hint
+  for numbering style when stylesheet is absent (such as in embedded mode)
+
+* Asciidoctor recognizes +opts+ as an alias for the +options+ attribute.
+
+* Asciidoctor creates xref labels using the text from the linked section
+  title when rendering HTML to match how DocBook works
+
+* Asciidoctor allows commas to be used in xref labels, whereas AsciiDoc
+  cuts off the label at the location of the first comma
+
+* Asciidoctor removes indentation for non-literal paragraphs in a list
+  item
++
+NOTE: In general, Asciidoctor handles whitespace much more intelligently
++
+
+* Asciidoctor does not output an empty +<dd>+ for labeled list items
+  that don't have a definition
+
+* In Asciidoctor, a horizontal ruler can have attributes
+
+* Asciidoctor wraps +<col>+ elements in +<colgroup>+ in tables
+
+* Asciidoctor uses +<code>+ around content in monospaced table cells
+
+* Asciidoctor skips over line comments in tables, whereas AsciiDoc doesn't
+
+* Asciidoctor uses its own API rather than a command line invocation to
+  handle table cells that have AsciiDoc content
+
+* Asciidoctor supports resolving variables from parent document in table
+  cells with AsciiDoc content
+
+* AsciiDoc doesn't carry over the doctype attribute passed from the
+  commandline when rendering AsciiDoc table cells, whereas Asciidoctor does
+
+* Asciidoctor does not require commas between attributes with quoted
+  values in a block attribute list
+
+* Asciidoctor strips the file extension from the target image when
+  generating alt text if no alt text is provided
+
+* Asciidoctor reifies the toc in the header of the document instead of
+  relying on JavaScript to create it
+
+* Asciidoctor sets CSS class on toc element, read from the +toc-class+
+  attribute; defaults to toc attribute name (+toc+ or +toc2+).
+
+* Asciidoctor honors the id, title, role and levels attributes set on
+  the toc macro.
+
+* Asciidoctor does not output two TOCs with the same id.
+
+* Asciidoctor is nice about using a section title syntax inside a
+  delimited block by simply ignoring it (AsciiDoc issues warnings)
+
+* Asciidoctor honors the alternate style name "discrete" for a floating
+  title (i.e., +[discrete]+)
+
+* Asciidoctor supports the +pass+ style on open blocks and paragraphs
+
+* Asciidoctor supports syntax highlighting of listing, literal or open blocks
+  that have the "source" style out of the box
+
+** Asciidoctor honors the source-highlighter values +coderay+ and
+   +highlightjs+, using CodeRay or highlight.js, respectively
+
+** Asciidoctor does not currently support Pygments for source
+   highlighting
+
+** Asciidoctor gracefully falls back to listing block if no source language
+   is specified
+
+* Asciidoctor sets these additional intrinsic attributes
+
+  +asciidoctor+::
+    indicates Asciidoctor is being used; useful for conditional
+    processing
+
+  +asciidoctor-version+::
+    indicates which version of Asciidoctor is in use
+
+* Asciidoctor does not support deprecated tables (you don't want them
+  anyway)
+
+* Use can set the extension for icons using the +icontype+ attribute
+  (AsciiDoc defaults to .png)
+
+* Asciidoctor uses the +<blockquote>+ for the content and +<cite>+ tag for
+  attribution title in the HTML output for quote blocks, requiring some
+  additional styling to match AsciiDoc
++
+ blockquote.content { padding: 0; margin; 0 }
+ cite { color: navy; }
++
+
+* Asciidoctor supports markdown-style blockquotes as well as a shorthand
+  for a blockquote paragraph.
+
+* Asciidoctor supports markdown-style headings (section titles)
+
+* Asciidoctor does not support the deprecated index term syntax (`++`
+  and `+++`)
+
+* Asciidoctor includes a modern default stylesheet based on Foundation.
+
+* Asciidoctor links to, rather than embeds, the default stylesheet into
+  the document by default (e.g., +linkcss+). To include the default
+  stylesheet, you can either use the +copycss+ attribute to tell
+  Asciidoctor to copy it to the output directory, or you can embed it
+  into the document using the +linkcss!+ attribute. You can also provide
+  your own stylesheet using the +stylesheet+ attribute.
+
+* Asciidoctor introduces the +hardbreaks+ attribute, which inserts a
+  line break character after each line of wrapped text
+
+* Asciidoctor introduces the +idseparator+ attribute to customize the
+  separator used in generated section ids (AsciiDoc hardcodes +_+)
+
+* Asciidoctor does not support system evaluation macros
+
+* Asciidoctor does not support displaying comments
+
+* Asciidoctor properly calculates author initials if attribute reference
+  is used in name
+
+* Asciidoctor allows the author and revision attributes to be referenced
+  in subsequent attribute entries in header (unlike AsciiDoc)
+
+* Asciidoctor allows multiple authors to be defined, separated by
+  semicolon. In DocBook backend, the authors are listed in an
+  +<authorgroup>+ element.
+
+* Asciidoctor allows the document id to be set using [[id]] above the
+  document header (adds id attribute to +<body>+ tag)
+
+* Assigning value to the +listing-caption+ attribute will enable
+  automatic captions for listings (like examples, tables and figures)
+
+* The +ifeval::[]+ macro is constrained for the strict purpose of
+  comparing values of attributes
+
+* The +include::[]+ macro is converted to a link to the target document
+  when SafeMode is SECURE or greater (this makes for a friendly
+  experience on GitHub)
+
+* Asciidoctor supports up to 6 section levels (to cover all heading levels in
+  HTML) whereas AsciiDoc stops at 5; note the 6 section level is only available
+  using the single-line section title syntax
+
+* Admonition block style is added to class of outer div in html5 backend
+  in Asciidoctor
+
+* Admonition block caption can be overridden in Asciidoctor using the
+  +caption+ block attribute
+
+* Asciidoctor will parse attributes in link macros if the +linkattrs+
+  attribute is set on the document.
+
+If there's a difference you don't see in this list, check the {issues}[issue
+tracker] to see if it's an outstanding feature, or file an issue to report the
+difference.
+
+== Contributing
+
+In the spirit of {freesoftware}[free software], 'everyone' is encouraged to
+help improve this project.
+
+Here are some ways *you* can contribute:
+
+* by using alpha, beta, and prerelease versions
+* by reporting bugs
+* by suggesting new features
+* by writing or editing documentation
+* by writing specifications
+* by writing code -- 'No patch is too small.'
+** fix typos
+** add comments
+** clean up inconsistent whitespace
+** write tests!
+* by refactoring code
+* by fixing {issues}[issues]
+* by reviewing patches
+
+== Submitting an Issue
+
+We use the {issues}[GitHub issue tracker] associated with this project
+to track bugs and features. Before submitting a bug report or feature
+request, check to make sure it hasn't already been submitted. When
+submitting a bug report, please include a {gist}[Gist] that includes any
+details that may help reproduce the bug, including your gem version,
+Ruby version, and operating system.
+
+Most importantly, since Asciidoctor is a text processor, reproducing
+most bugs requires that we have some snippet of text on which
+Asciidoctor exhibits the bad behavior.
+
+An ideal bug report would include a pull request with failing specs.
+
+== Submitting a Pull Request
+
+. {fork}[Fork the repository].
+. {branch}[Create a topic branch].
+. Add tests for your unimplemented feature or bug fix.
+. Run +bundle exec rake+.
+If your tests pass, return to step 3.
+. Implement your feature or bug fix.
+. Run +bundle exec rake+.
+If your tests fail, return to step 5.
+. Add documentation for your feature or bug fix.
+. If your changes are not 100% documented, go back to step 7.
+. Add, commit, and push your changes.
+. {pr}[Submit a pull request].
+
+== Supported Ruby Versions
+
+This library aims to support the following Ruby implementations:
+
+* Ruby 1.8.7
+* Ruby 1.9.3
+* Ruby 2.0.0
+* JRuby 1.7.4
+* Rubinius nightly
+
+If something doesn't work on one of these interpreters, it should be
+considered a bug.
+
+If you would like this library to support another Ruby version, you may
+volunteer to be a maintainer. Being a maintainer entails making sure all
+tests run and pass on that implementation. When something breaks on your
+implementation, you will be personally responsible for providing patches
+in a timely fashion. If critical issues for a particular implementation
+exist at the time of a major release, support for that Ruby version may
+be dropped.
+
+== Resources
+
+Project home page:: {homepage}
+
+Source repository:: {sources}
+
+Issue tracker:: {issues}
+
+Mailinglist / forum:: {forum}
+
+GitHub organization:: {org}
+
+== Authors
+
+*Asciidoctor* was written by https://github.com/mojavelinux[Dan Allen],
+https://github.com/erebor[Ryan Waldron],
+https://github.com/lightguard[Jason Porter], https://github.com/nickh[Nick
+Hengeveld] and {contributors}[other contributors].
+
+*AsciiDoc* was written by Stuart Rackham and has received contributions
+from many other individuals.
+
+== Copyright
+
+Copyright (C) 2012-2013 Dan Allen and Ryan Waldron. Free use of this
+software is granted under the terms of the MIT License.
+
+See the {license}[LICENSE] file for details.
+
+// vim: tw=72
diff --git a/README.asciidoc b/README.asciidoc
deleted file mode 100644
index 411e680..0000000
--- a/README.asciidoc
+++ /dev/null
@@ -1,296 +0,0 @@
-Asciidoctor
-===========
-:asciidoctor: http://asciidoctor.org
-:asciidoctor-source: http://github.com/asciidoctor/asciidoctor
-:asciidoc: http://asciidoc.org
-:gitscm-next: https://github.com/github/gitscm-next
-:asciidoctor-seed: https://github.com/github/gitscm-next/commits/master/lib/asciidoc.rb
-:templates: https://github.com/asciidoctor/asciidoctor/blob/master/lib/asciidoctor/backends
-:tilt: https://github.com/rtomayko/tilt
-:freesoftware: http://www.fsf.org/licensing/essays/free-sw.html
-:issues: https://github.com/asciidoctor/asciidoctor/issues
-:gist: https://gist.github.com
-:fork: http://help.github.com/fork-a-repo/
-:branch: http://learn.github.com/p/branching.html
-:pr: http://help.github.com/send-pull-requests/
-:license: https://github.com/asciidoctor/asciidoctor/blob/master/LICENSE
-:idprefix:
-
-{asciidoctor}[Asciidoctor] is a pure Ruby processor for converting
-{asciidoc}[AsciiDoc] source files and strings into HTML 5, DocBook 4.5
-and other formats. It's
-http://rubygems.org/gems/asciidoctor[published as a RubyGem] and is
-available under the MIT open source license.
-
-image::https://travis-ci.org/asciidoctor/asciidoctor.png?branch=master["Build Status", link="https://travis-ci.org/asciidoctor/asciidoctor"]
-
-Asciidoctor uses a set of built-in ERB templates to render the document
-to HTML 5 or DocBook 4.5. We've matched the rendered output as close as
-possible to the default output of the native Python processor. You can
-override this behavior by providing {tilt}[Tilt]-compatible templates.
-See the xref:usage[Usage section] for more details.
-
-Asciidoctor currently works (read as 'tested') with Ruby 1.8.7, Ruby
-1.9.3 and JRuby 1.7.2 (on Linux, Mac and Windows). We expect it will
-work with other versions of Ruby as well and would welcome help in
-testing it out.
-
-The initial code from which Asciidoctor started emerged from the
-{gitscm-next}[Git SCM site repo]. Refer to commit history of
-{asciidoctor-seed}[asciidoc.rb] to view the initial contributions and
-individual contributors.
-
-The source code can now be found in the {asciidoctor-source}[Asciidoctor
-source repository] on GitHub.
-
-== Installation
-
-NOTE: We are working hard to make Asciidoctor a drop-in replacement for
-AsciiDoc. We're very close, with nearly 600 tests that ensure
-compatibility. The march is on towards full compliance and beyond.
-
-To install the gem:
-
- gem install asciidoctor
-
-Or if you prefer bundler:
-
- bundle install asciidoctor
-
-== Usage
-
-Asciidoctor has both a command line interface (CLI) and an API. The
-CLI is a drop-in replacement for the `asciidoc.py` command from the
-python implementation. The API is intended for integration with other
-software projects and is suitable for server-side applications, such
-as Rails, Sinatra and GitHub.
-
-=== Command line interface (CLI)
-
-After installing the `asciidoctor` gem, the `asciidoctor` commandline
-interface should be available on your PATH after installing the gem.
-To invoke it, simply execute:
-
- asciidoctor <asciidoc_file>
-
-This will use the built-in defaults for options and create a new file
-in the same directory as the input file, with the same base name, but
-with the .html extension.
-
-There are many other options available and full help is provided via:
-
- asciidoctor --help
-
-or in the http://asciidoctor.org/man/asciidoctor[man page].
-
-There is also an `asciidoctor-safe` command, which turns on safe mode
-by default, preventing access to files outside the parent directory of
-the source file. This mode is very similar to the safe mode of `asciidoc.py`.
-
-=== Ruby API
-
-To use Asciidoctor in your application, you first need to require the
-gem:
-
- require 'asciidoctor'
-
-With that in place, you can start processing AsciiDoc documents.
-
-.Loading a document
-To parse a file into an `Asciidoctor::Document` object:
-
- doc = Asciidoctor.load_file('your_file.asciidoc')
-
-You can get information about the document:
-
- puts doc.doctitle
- puts doc.attributes
-
-More than likely, you want to just render the document.
-
-.Rendering files
-To render a file containing AsciiDoc markup to HTML 5:
-
- Asciidoctor.render_file('your_file.asciidoc', :in_place => true)
-
-The command will output to the file `your_file.html` in the same
-directory. You can render the file to DocBook 4.5 by setting the
-`backend` attribute to 'docbook':
-
- Asciidoctor.render_file('your_file.asciidoc', :in_place => true,
-   :attributes => {'backend' => 'docbook'})
-
-The command will output to the file `your_file.xml` in the same
-directory. (If you're on Linux, you can view the file using yelp).
-
-.Rendering strings
-To render an AsciiDoc-formatted string:
-
- puts Asciidoctor.render('*This* is it.')
-
-When rendering a string, the header and footer are excluded by default
-to make Asciidoctor consistent with other lightweight markup engines
-like Markdown. If you want the header and footer, just declare it as
-an option:
-
- puts Asciidoctor.render('*This* is it.', :header_footer => true)
-
-Now you'll get a full HTML 5 file. As before, you can also produce
-DocBook 4.5:
-
- puts Asciidoctor.render('*This* is it.', :header_footer => true,
-   :attributes => {'backend' => 'docbook'})
-
-If you don't like the output you see, you can change it. Any of it!
-
-.Custom templates
-Asciidoctor allows you to override the {templates}[built-in templates]
-used to render almost any individual AsciiDoc element. If you provide a
-directory of {tilt}[Tilt]-compatible templates, named in such a way that
-Asciidoctor can figure out which template goes with which element,
-Asciidoctor will use the templates in this directory instead of its
-built-in templates for any elements for which it finds a matching
-template. It will fallback to its default templates for everything else.
-
- puts Asciidoctor.render('*This* is it.', :header_footer => true,
-   :template_dir => 'templates')
-
-The Document and Section templates should begin with `document.` and
-`section.`, respectively. The file extension is used by Tilt to
-determine which view framework it will use to use to render the
-template. For instance, if you want to write the template in ERB, you'd
-name these two templates `document.html.erb` and `section.html.erb`. To
-use Haml, you'd name them `document.html.haml` and `section.html.haml`.
-
-Templates for block elements, like a Paragraph or Sidebar, would begin
-with `block_<style>.`. For instance, to override the default Paragraph
-template with an ERB template, put a file named
-`block_paragraph.html.erb` in the template directory you pass to the
-`Document` constructor using the `template_dir` option.
-
-For more usage examples, see the (massive) test suite.
-
-== Differences from AsciiDoc
-
-While Asciidoctor aims to be compliant with the AsciiDoc syntax, there are some differences which are important to keep in mind. In some cases, it's to enforce a rule we believe is too lax or ambiguous in AsciiDoc. In other cases, it's a tradeoff for speed, smarter processing or a feature we just haven't yet implemented. (You'll also notice that Asciidoctor is about 20x faster than AsciiDoc).
-
-Here are the known cases where Asciidoctor differs from AsciiDoc:
-
-* In Asciidoctor, safe mode is on by default when using the API (safe mode level SECURE),
-* Asciidoctor safe mode is even more safe than AsciiDoc's safe mode
-* Asciidoctor enforces symmetric block delimiters (the length of start and end delimiters for a block must match)
-* Section title underlines must be within +/- 1 of the length of the title (AsciiDoc is +/- 3)
-* Asciidoctor's default HTML backend matches AsciiDoc's HTML 5 backend (whereas XHTML 1.1 is the default HTML backend in AsciiDoc)
-* Asciidoctor handles inline anchors more cleanly
-** AsciiDoc adds an `<a>` tag in the line and that markup gets caught in the generated id
-** Asciidoctor promotes the id of the anchor as the section id
-* Asciidoctor strips XML entities from the section title before generating the id (makes for cleaner section ids)
-* Asciidoctor use `<tt>` instead of `<span class="monospace">` around inline literal text in the HTML backend
-* Asciidoctor is much more lenient about attribute list parsing (double quotes are rarely needed)
-* Asciidoctor creates xref labels using the text from the linked section title when rendering HTML to match how DocBook works
-* Asciidoctor allows commas to be used in xref labels, whereas AsciiDoc cuts off the label at the location of the first comma
-* Asciidoctor removes indentation for non-literal paragraphs in a list item
-** In general, Asciidoctor handles whitespace much more intelligently
-* In Asciidoctor, a ruler can have attributes
-* Asciidoctor skips over line comments in tables, whereas AsciiDoc does not
-* Asciidoctor uses its own API rather than a command line invocation to handle table cells that have AsciiDoc content
-* Asciidoctor supports resolving variables from parent document in table cells with AsciiDoc content
-* AsciiDoc doesn't carry over the doctype attribute passed from the commandline when rendering AsciiDoc content cells, whereas Asciidoctor does
-* Asciidoctor strips the file extension from the target image when generating alt text if no alt text is provided
-* Asciidoctor reifies the toc in the header of the document instead of relying on JavaScript to create it
-* Asciidoctor is nice about using a section title syntax inside a delimited block by simply ignoring it (AsciiDoc issues warnings)
-* Asciidoctor honors the alternate style name "discrete" for a floating title (i.e., [discrete])
-* Asciidoctor supports syntax highlighting of listing or literal blocks that have the "source" style out of the box
-** Asciidoctor honors the source-highlighter values `coderay` and `highlightjs`, using CodeRay or highlight.js, respectively
-** Asciidoctor does not currently support Pygments for source highlighting
-* Asciidoctor sets these additional intrinsic attributes
-`asciidoctor`:: indicates Asciidoctor is being used; useful for conditional processing
-`asciidoctor-version`:: indicates which version of Asciidoctor is in use
-* Asciidoctor does not support deprecated tables (you don't want them anyway)
-
-If there's a difference you don't see in this list, check the {issues}[issue tracker] to see if it's an outstanding feature, or file an issue to report the difference.
-
-== Contributing
-
-In the spirit of {freesoftware}[free software], 'everyone' is
-encouraged to help improve this project.
-
-Here are some ways *you* can contribute:
-
-* by using alpha, beta, and prerelease versions
-* by reporting bugs
-* by suggesting new features
-* by writing or editing documentation
-* by writing specifications
-* by writing code -- 'No patch is too small.'
-** fix typos
-** add comments
-** clean up inconsistent whitespace
-** write tests!
-* by refactoring code
-* by fixing {issues}[issues]
-* by reviewing patches
-
-== Submitting an Issue
-
-We use the {issues}[GitHub issue tracker] associated with this project
-to track bugs and features.  Before submitting a bug report or feature
-request, check to make sure it hasn't already been submitted. When
-submitting a bug report, please include a {gist}[Gist] that includes
-any details that may help reproduce the bug, including your gem
-version, Ruby version, and operating system.
-
-Most importantly, since Asciidoctor is a text processor, reproducing
-most bugs requires that we have some snippet of text on which
-Asciidoctor exhibits the bad behavior.
-
-An ideal bug report would include a pull request with failing specs.
-
-== Submitting a Pull Request
-
-. {fork}[Fork the repository].
-. {branch}[Create a topic branch].
-. Add tests for your unimplemented feature or bug fix.
-. Run `bundle exec rake`. If your tests pass, return to step 3.
-. Implement your feature or bug fix.
-. Run `bundle exec rake`. If your tests fail, return to step 5.
-. Add documentation for your feature or bug fix.
-. If your changes are not 100% documented, go back to step 7.
-. Add, commit, and push your changes.
-. {pr}[Submit a pull request].
-
-== Supported Ruby Versions
-
-This library aims to support the following Ruby implementations:
-
-* Ruby 1.8.7
-* Ruby 1.9.3
-* JRuby 1.7.2
-* Rubinius 1.2.4
-
-If something doesn't work on one of these interpreters, it should be
-considered a bug.
-
-If you would like this library to support another Ruby version, you
-may volunteer to be a maintainer. Being a maintainer entails making
-sure all tests run and pass on that implementation. When something
-breaks on your implementation, you will be personally responsible for
-providing patches in a timely fashion. If critical issues for a
-particular implementation exist at the time of a major release,
-support for that Ruby version may be dropped.
-
-== Authors
-
-*Asciidoctor* was written by Ryan Waldron, Dan Allen and
-https://github.com/asciidoctor/asciidoctor/graphs/contributors[other
-contributors].
-
-*AsciiDoc* was written by Stuart Rackham and has received
-contributions from many other individuals.
-
-== Copyright
-
-Copyright (C) 2012 Ryan Waldron.
-See {license}[LICENSE] for details.
-
-// vim: tw=72
diff --git a/Rakefile b/Rakefile
index 02e476d..0143290 100644
--- a/Rakefile
+++ b/Rakefile
@@ -47,12 +47,22 @@ task :default => :test
 
 require 'rake/testtask'
 Rake::TestTask.new(:test) do |test|
+  puts "LANG: #{ENV['LANG']}"
   test.libs << 'lib' << 'test'
   test.pattern = 'test/**/*_test.rb'
   test.warning = true
   test.verbose = true
 end
 
+# Run tests with Encoding::default_external set to US-ASCII
+Rake::TestTask.new(:test_us_ascii) do |test|
+  test.libs << 'lib' << 'test'
+  test.pattern = 'test/**/*_test.rb'
+  test.warning = true
+  test.verbose = true
+  test.ruby_opts << '-EUS-ASCII' if RUBY_VERSION >= '1.9'
+end
+
 desc "Generate RCov test coverage and open in your browser"
 task :coverage do
   require 'rcov'
diff --git a/asciidoctor.gemspec b/asciidoctor.gemspec
index 4deec6c..daa21d8 100644
--- a/asciidoctor.gemspec
+++ b/asciidoctor.gemspec
@@ -1,77 +1,64 @@
-## This is the rakegem gemspec template. Make sure you read and understand
-## all of the comments. Some sections require modification, and others can
-## be deleted if you don't need them. Once you understand the contents of
-## this file, feel free to delete any comments that begin with two hash marks.
-## You can find comprehensive Gem::Specification documentation, at
-## http://docs.rubygems.org/read/chapter/20
 Gem::Specification.new do |s|
   s.specification_version = 2 if s.respond_to? :specification_version=
-  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
-  s.rubygems_version = '1.3.5'
+  s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
+  s.rubygems_version = '1.8.5'
 
-  ## Leave these as is they will be modified for you by the rake gemspec task.
-  ## If your rubyforge_project name is different, then edit it and comment out
-  ## the sub! line in the Rakefile
+  ## name, version, date and rubyforge_project are updated automatically by the
+  ## Rake build (see the validate task)
   s.name              = 'asciidoctor'
-  s.version           = '0.1.1'
-  s.date              = '2013-02-26'
+  s.version           = '0.1.3'
+  s.date              = '2013-05-30'
   s.rubyforge_project = 'asciidoctor'
 
-  ## Make sure your summary is short. The description may be as long
-  ## as you like.
-  s.summary     = "Pure Ruby Asciidoc to HTML rendering."
-  s.description = "A pure Ruby processor to turn Asciidoc-formatted documents into HTML (and, eventually, other formats perhaps)."
+  s.summary     = 'A native Ruby AsciiDoc syntax processor and publishing toolchain'
+  s.description = <<-EOS
+An open source text processor and publishing toolchain written in Ruby for converting AsciiDoc markup into HTML 5, DocBook 4.5 and custom formats.
+EOS
+  s.license     = 'MIT'
+  s.authors  = ['Dan Allen', 'Ryan Waldron', 'Jeremy McAnally', 'Jason Porter', 'Nick Hengeveld']
+  s.email    = ['dan.j.allen at gmail.com', 'rew at erebor.com']
+  s.homepage = 'http://asciidoctor.org'
 
-  ## List the primary authors. If there are a bunch of authors, it's probably
-  ## better to set the email to an email list or something. If you don't have
-  ## a custom homepage, consider using your GitHub URL or the like.
-  s.authors  = ["Ryan Waldron", "Dan Allen", "Jeremy McAnally"]
-  s.email    = 'rew at erebor.com'
-  s.homepage = 'http://github.com/asciidoctor'
-
-  ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
-  ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
   s.require_paths = %w[lib]
+  s.executables = ['asciidoctor', 'asciidoctor-safe']
 
-  ## If your gem includes any executables, list them here.
-  s.executables = ["asciidoctor"]
-
-  ## Specify any RDoc options here. You'll want to add your README and
-  ## LICENSE files to the extra_rdoc_files list.
-  s.rdoc_options = ["--charset=UTF-8"]
+  s.has_rdoc = true
+  s.rdoc_options = ['--charset=UTF-8']
   s.extra_rdoc_files = %w[LICENSE]
 
   ## List your runtime dependencies here. Runtime dependencies are those
   ## that are needed for an end user to actually USE your code.
+  #s.add_dependency 'foo', '~> 1.0.0'
 
-  ## List your development dependencies here. Development dependencies are
-  ## those that are only needed during development
+  ## Development dependencies are libraries only needed during development or
+  ## testing
   s.add_development_dependency('coderay')
   s.add_development_dependency('erubis')
-  s.add_development_dependency('htmlentities')
-  s.add_development_dependency('mocha')
   s.add_development_dependency('nokogiri')
   s.add_development_dependency('pending')
   s.add_development_dependency('rake')
   s.add_development_dependency('rdoc', '~> 3.12')
   s.add_development_dependency('tilt')
 
-  ## Leave this section as-is. It will be automatically generated from the
-  ## contents of your Git repository via the gemspec task. DO NOT REMOVE
-  ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
+  ## The manifest section is automatically generated by the Rake build
+  ## based on the contents of the Git repository (see the gemspec task).
+  ## DO NOT REMOVE THE = MANIFEST = DELIMITERS!
   # = MANIFEST =
   s.files = %w[
     Gemfile
+    Guardfile
     LICENSE
-    README.asciidoc
+    README.adoc
     Rakefile
     asciidoctor.gemspec
     bin/asciidoctor
     bin/asciidoctor-safe
+    compat/asciidoc.conf
     lib/asciidoctor.rb
     lib/asciidoctor/abstract_block.rb
     lib/asciidoctor/abstract_node.rb
     lib/asciidoctor/attribute_list.rb
+    lib/asciidoctor/backends/_stylesheets.rb
     lib/asciidoctor/backends/base_template.rb
     lib/asciidoctor/backends/docbook45.rb
     lib/asciidoctor/backends/html5.rb
@@ -81,11 +68,11 @@ Gem::Specification.new do |s|
     lib/asciidoctor/cli/options.rb
     lib/asciidoctor/debug.rb
     lib/asciidoctor/document.rb
-    lib/asciidoctor/errors.rb
     lib/asciidoctor/helpers.rb
     lib/asciidoctor/inline.rb
     lib/asciidoctor/lexer.rb
     lib/asciidoctor/list_item.rb
+    lib/asciidoctor/path_resolver.rb
     lib/asciidoctor/reader.rb
     lib/asciidoctor/renderer.rb
     lib/asciidoctor/section.rb
@@ -100,11 +87,17 @@ Gem::Specification.new do |s|
     test/fixtures/asciidoc.txt
     test/fixtures/asciidoc_index.txt
     test/fixtures/ascshort.txt
+    test/fixtures/basic-docinfo.html
+    test/fixtures/basic-docinfo.xml
+    test/fixtures/basic.asciidoc
+    test/fixtures/docinfo.html
+    test/fixtures/docinfo.xml
     test/fixtures/dot.gif
     test/fixtures/encoding.asciidoc
     test/fixtures/include-file.asciidoc
     test/fixtures/list_elements.asciidoc
     test/fixtures/sample.asciidoc
+    test/fixtures/stylesheets/custom.css
     test/fixtures/tip.gif
     test/invoker_test.rb
     test/lexer_test.rb
@@ -112,6 +105,7 @@ Gem::Specification.new do |s|
     test/lists_test.rb
     test/options_test.rb
     test/paragraphs_test.rb
+    test/paths_test.rb
     test/preamble_test.rb
     test/reader_test.rb
     test/renderer_test.rb
@@ -123,7 +117,6 @@ Gem::Specification.new do |s|
   ]
   # = MANIFEST =
 
-  ## Test files will be grabbed from the file list. Make sure the path glob
-  ## matches what you actually use.
+  ## Test files are selected from the file list using the path glob here
   s.test_files = s.files.select { |path| path =~ /^test\/.*_test\.rb/ }
 end
diff --git a/compat/asciidoc.conf b/compat/asciidoc.conf
new file mode 100644
index 0000000..ba4d485
--- /dev/null
+++ b/compat/asciidoc.conf
@@ -0,0 +1,256 @@
+# This file is an AsciiDoc configuration file that makes
+# AsciiDoc conform with Asciidoctor's fixes and customizations.
+#
+# Place this file in the same directory as your AsciiDoc document and the
+# AsciiDoc processor (asciidoc) will automatically use it.
+
+[miscellaneous]
+newline=\n
+
+[attributes]
+# make html5 the default html backend
+backend-alias-html=html5
+linkcss=
+apostrophe='
+asterisk=*
+caret=^
+backtick=`
+# plus introduced in AsciiDoc 8.6.9
+plus=+
+space=" "
+tilde=~
+
+# enables markdown-style headings
+[titles]
+sect0=^(=|#) +(?P<title>[\S].*?)(?: +\1)?$
+sect1=^(==|##) +(?P<title>[\S].*?)(?: +\1)?$
+sect2=^(===|###) +(?P<title>[\S].*?)(?: +\1)?$
+sect3=^(====|####) +(?P<title>[\S].*?)(?: +\1)?$
+sect4=^(=====|#####) +(?P<title>[\S].*?)(?: +\1)?$
+
+# enables fenced code blocks
+# FIXME I haven't sorted out yet how to do syntax highlighting
+[blockdef-fenced-code]
+delimiter=^```\w*$
+template::[blockdef-listing]
+
+# enables blockquotes to be defined using two double quotes
+[blockdef-air-quote]
+delimiter=^""$
+template::[blockdef-quote]
+
+# markdown-style blockquote (paragraph only)
+# FIXME does not strip leading > on subsequent lines
+[paradef-markdown-quote]
+delimiter=(?s)>\s*(?P<text>\S.*)
+style=quote
+quote-style=template="quoteparagraph",posattrs=("style","attribution","citetitle")
+
+# fix regex for callout list to require number; also makes markdown-style blockquote work
+[listdef-callout]
+posattrs=style
+delimiter=^<?(?P<index>\d+>) +(?P<text>.+)$
+type=callout
+tags=callout
+style=arabic
+
+# enables literal block to be used as code block
+[blockdef-literal]
+template::[source-filter-style]
+
+[tabledef-csv]
+template::[tabledef-default]
+delimiter=^,={3,}$
+format=csv
+
+[tabledef-dsv]
+template::[tabledef-default]
+delimiter=^:={3,}$
+format=dsv
+
+[macros]
+# btn:[Save]
+(?su)(?<!\w)\\?btn:\[(?P<attrlist>(?:\\\]|[^\]])+?)\]=button
+
+# kbd:[F11] or kbd:[Ctrl+T] or kbd:[Ctrl,T]
+(?su)(?<!\w)\\?kbd:\[(?P<attrlist>(?:\\\]|[^\]])+?)\]=keyboard
+
+# menu:Search[] or menu:File[New...] or menu:View[Page Style, No Style]
+# TODO implement menu:View[Page Style > No Style] syntax
+(?su)(?<!\w)[\\]?(?P<name>menu):(?P<target>\w|\w.*?\S)?\[(?P<attrlist>.*?)\]=
+
+ifdef::basebackend-html[]
+
+[button-inlinemacro]
+<b class="button">{1}</b>
+
+[keyboard-inlinemacro]
+{set2:keys:{eval:re.split(r'(?<!\+ |.\+)\+', '{1}')}}
+{2%}{eval:len({keys}) == 1}<kbd>{1}</kbd>
+{2%}{eval:len({keys}) == 2}<kbd class="combo"><kbd>{eval:{keys}[0].strip()}</kbd>+<kbd>{eval:{keys}[1].strip()}</kbd></kbd>
+{2%}{eval:len({keys}) == 3}<kbd class="combo"><kbd>{eval:{keys}[0].strip()}</kbd>+<kbd>{eval:{keys}[1].strip()}</kbd>+<kbd>{eval:{keys}[2].strip()}</kbd></kbd>
+{2#}{3%}<kbd class="combo"><kbd>{1}</kbd>+<kbd>{2}</kbd></kbd>
+{3#}<kbd class="combo"><kbd>{1}</kbd>+<kbd>{2}</kbd>+<kbd>{3}</kbd></kbd>
+
+[menu-inlinemacro]
+{1%}<span class="menu">{target}</span>
+{1#}{2%}<span class="menuseq"><span class="menu">{target}</span> ▸ <span class="menuitem">{1}</span></span>
+{2#}{3%}<span class="menuseq"><span class="menu">{target}</span> ▸ <span class="submenu">{1}</span> ▸ <span class="menuitem">{2}</span></span>
+{3#}<span class="menuseq"><span class="menu">{target}</span> ▸ <span class="submenu">{1}</span> ▸ <span class="submenu">{2}</span> ▸ <span class="menuitem">{3}</span></span>
+
+[literal-inlinemacro]
+<code>{passtext}</code>
+
+[tags]
+emphasis=<em{1? class="{1}"}>|</em>
+strong=<strong{1? class="{1}"}>|</strong>
+monospaced=<code{1? class="{1}"}>|</code>
+superscript=<sup{1? class="{1}"}>|</sup>
+subscript=<sub{1? class="{1}"}>|</sub>
+
+[monospacedwords]
+<code>{words}</code>
+
+[listtags-numbered]
+list=<div class="olist{style? {style}}{compact-option? compact}{role? {role}}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<ol class="{style}"{style at loweralpha: type="a"}{style at lowerroman: type="i"}{style at upperalpha: type="A"}{style at upperroman: type="I"}{start? start="{start}"}>|</ol></div>
+
+[tabletags-monospaced]
+paragraph=<p class="tableblock"><code>|</code></p>
+
+[sect0]
+<h1{id? id="{id}"} class="sect0">{title}</h1>
+|
+
+# support for document title in embedded documents
+ifeval::[not config.header_footer]
+[preamble]
+<h1>{title={doctitle}}</h1>{set:title-rendered:}
+<div id="preamble">
+<div class="sectionbody">
+|
+</div>
+{toc,toc2#}{toc-placement$preamble:}{template:toc}
+</div>
+
+[sect1]
+{title-rendered%}<h1>{doctitle}</h1>
+<div class="sect1{style? {style}}{role? {role}}">
+<h2{id? id="{id}"}>{numbered?{sectnum} }{title}</h2>
+<div class="sectionbody">
+|
+</div>
+</div>
+endif::[]
+
+# override to add the admonition name to the class attribute of the outer element
+[admonitionblock]
+<div class="admonitionblock {name}{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<table><tr>
+<td class="icon">
+{data-uri%}{icons#}<img src="{icon={iconsdir}/{name}.png}" alt="{caption}">
+{data-uri#}{icons#}<img alt="{caption}" src="data:image/png;base64,
+{data-uri#}{icons#}{sys:"{python}" -u -c "import base64,sys; base64.encode(sys.stdin,sys.stdout)" < "{eval:os.path.join(r"{indir={outdir}}",r"{icon={iconsdir}/{name}.png}")}"}">
+{icons%}<div class="title">{caption}</div>
+</td>
+<td class="content">
+<div class="title">{title}</div>
+|
+</td>
+</tr></table>
+</div>
+
+# a common template for emitting the attribute for a quote or verse block
+# don't output attribution div if attribution or citetitle are both empty
+[attribution]
+{attribution,citetitle#}<div class="attribution">
+<cite>{citetitle}</cite>{attribution?<br>}
+— {attribution}
+{attribution,citetitle#}</div>
+
+# override to use blockquote element for content and cite element for cite title
+[quoteblock]
+<div class="quoteblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="title">{title}</div>
+<blockquote>
+|
+</blockquote>
+template::[attribution]
+</div>
+
+# override to use cite element for cite title
+[verseblock]
+<div class="verseblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="title">{title}</div>
+<pre class="content">
+|
+</pre>
+template::[attribution]
+</div>
+
+# override tabletags to support cellbgcolor
+[tabletags-default]
+headdata=<th class="tableblock halign-{halign=left} valign-{valign=top}"{colspan at 1:: colspan="{colspan}"}{rowspan at 1:: rowspan="{rowspan}"}{cellbgcolor? style="background-color:{cellbgcolor};"}>|</th>
+bodydata=<td class="tableblock halign-{halign=left} valign-{valign=top}"{colspan at 1:: colspan="{colspan}"}{rowspan at 1:: rowspan="{rowspan}"}{cellbgcolor? style="background-color:{cellbgcolor};"}>|</td>
+
+[toc]
+<div id="toc">
+<div id="toctitle">{toc-title}</div>
+ifdef::toc2[]
+<script type="text/javascript">
+document.body.className += ' toc2';
+document.getElementById('toc').className = 'toc2';
+</script>
+endif::toc2[]
+<noscript><p><b>JavaScript must be enabled in your browser to display the table of contents.</b></p></noscript>
+</div>
+
+endif::basebackend-html[]
+
+# Override docinfo to support subtitle
+ifdef::basebackend-docbook[]
+
+[button-inlinemacro]
+<guibutton>{1}</guibutton>
+
+[keyboard-inlinemacro]
+{set2:keys:{eval:re.split(r'(?<!\+ |.\+)\+', '{1}')}}
+{2%}{eval:len({keys}) == 1}<keycap>{1}</keycap>
+{2%}{eval:len({keys}) == 2}<keycombo><keycap>{eval:{keys}[0].strip()}</keycap><keycap>{eval:{keys}[1].strip()}</keycap></keycombo>
+{2%}{eval:len({keys}) == 3}<keycombo><keycap>{eval:{keys}[0].strip()}</keycap><keycap>{eval:{keys}[1].strip()}</keycap><keycap>{eval:{keys}[2].strip()}</keycap></keycombo>
+{2#}{3%}<keycombo><keycap>{1}</keycap><keycap>{2}</keycap></keycombo>
+{3#}<keycombo><keycap>{1}</keycap><keycap>{2}</keycap><keycap>{3}</keycap></keycombo>
+
+[menu-inlinemacro]
+{1%}<guimenu>{target}</guimenu>
+{1#}{2%}<menuchoice><guimenu>{target}</guimenu> <guimenuitem>{1}</guimenuitem></menuchoice>
+{2#}{3%}<menuchoice><guimenu>{target}</guimenu> <guisubmenu>{1}</guisubmenu> <guimenuitem>{2}</guimenuitem></menuchoice>
+{3#}<menuchoice><guimenu>{target}</guimenu> <guisubmenu>{1}</guisubmenu> <guisubmenu>{2}</guisubmenu> <guimenuitem>{3}</guimenuitem></menuchoice>
+
+# override tabletags to support cellbgcolor
+[tabletags-default]
+headdata=<entry align="{halign}" valign="{valign}"{colspan at 1:: namest="col_{colstart}" nameend="col_{colend}"}{morerows at 0:: morerows="{morerows}"}>{cellbgcolor?<?dbfo bgcolor="{cellbgcolor}"?>}|</entry>
+bodydata=<entry align="{halign}" valign="{valign}"{colspan at 1:: namest="col_{colstart}" nameend="col_{colend}"}{morerows at 0:: morerows="{morerows}"}>{cellbgcolor?<?dbfo bgcolor="{cellbgcolor}"?>}|</entry>
+
+[docinfo]
+ifndef::notitle[]
+{set2:subtitle_offset:{eval:'{doctitle}'.rfind(': ')}}
+{eval:{subtitle_offset} != -1}<title>{eval:'{doctitle}'[0:{subtitle_offset}]}</title>
+{eval:{subtitle_offset} != -1}<subtitle>{eval:'{doctitle}'[{subtitle_offset} + 2:]}</subtitle>
+{eval:{subtitle_offset} < 0}<title>{doctitle}</title>
+endif::notitle[]
+<date>{revdate}</date>
+# To ensure valid articleinfo/bookinfo when there is no AsciiDoc header.
+{doctitle%}{revdate%}<date>{docdate}</date>
+{authored#}<author>
+<firstname>{firstname}</firstname>
+<othername>{middlename}</othername>
+<surname>{lastname}</surname>
+<email>{email}</email>
+{authored#}</author>
+<authorinitials>{authorinitials}</authorinitials>
+<revhistory><revision>{revnumber?<revnumber>{revnumber}</revnumber>}<date>{revdate}</date>{authorinitials?<authorinitials>{authorinitials}</authorinitials>}{revremark?<revremark>{revremark}</revremark>}</revision></revhistory>
+{docinfo1,docinfo2#}{include:{docdir}/docinfo.xml}
+{docinfo,docinfo2#}{include:{docdir}/{docname}-docinfo.xml}
+<orgname>{orgname}</orgname>
+
+endif::basebackend-docbook[]
diff --git a/lib/asciidoctor.rb b/lib/asciidoctor.rb
index 57aedf4..55427ba 100755
--- a/lib/asciidoctor.rb
+++ b/lib/asciidoctor.rb
@@ -1,8 +1,10 @@
-require 'rubygems'
+if RUBY_VERSION < '1.9'
+  require 'rubygems'
+end
 require 'strscan'
+require 'set'
 
 $:.unshift(File.dirname(__FILE__))
-#$:.unshift(File.join(File.dirname(__FILE__), '..', 'vendor'))
 
 # Public: Methods for parsing Asciidoc input files and rendering documents
 # using eRuby templates.
@@ -87,6 +89,15 @@ module Asciidoctor
 
   end
 
+  # The root path of the Asciidoctor gem
+  ROOT_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+
+  # Flag to indicate whether encoding of external strings needs to be forced to UTF-8
+  # _All_ input data must be force encoded to UTF-8 if Encoding.default_external is *not* UTF-8
+  # Address failures performing string operations that are reported as "invalid byte sequence in US-ASCII" 
+  # Ruby 1.8 doesn't seem to experience this problem (perhaps because it isn't validating the encodings)
+  FORCE_ENCODING = RUBY_VERSION > '1.9' && Encoding.default_external != Encoding::UTF_8
+
   # The default document type
   # Can influence markup generated by render templates
   DEFAULT_DOCTYPE = 'article'
@@ -94,6 +105,10 @@ module Asciidoctor
   # The backend determines the format of the rendered output, default to html5
   DEFAULT_BACKEND = 'html5'
 
+  DEFAULT_STYLESHEET_KEYS = ['', 'DEFAULT'].to_set
+
+  DEFAULT_STYLESHEET_NAME = 'asciidoctor.css'
+
   # Pointers to the preferred version for a given backend.
   BACKEND_ALIASES = {
     'html' => 'html5',
@@ -113,19 +128,36 @@ module Asciidoctor
     'markdown' => '.md'
   }
 
+  SECTION_LEVELS = {
+    '=' => 0,
+    '-' => 1,
+    '~' => 2,
+    '^' => 3,
+    '+' => 4
+  }
+
+  ADMONITION_STYLES = ['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION'].to_set
+
+  PARAGRAPH_STYLES = ['comment', 'example', 'literal', 'listing', 'normal', 'pass', 'quote', 'sidebar', 'source', 'verse', 'abstract', 'partintro'].to_set
+
+  VERBATIM_STYLES = ['literal', 'listing', 'source', 'verse'].to_set
+
   DELIMITED_BLOCKS = {
-    '--'   => :open,
-    '----' => :listing,
-    '....' => :literal,
-    '====' => :example,
-    '****' => :sidebar,
-    '____' => :quote,
-    '++++' => :pass,
-    '|===' => :table,
-    '!===' => :table,
-    '////' => :comment,
-    '```'  => :fenced_code,
-    '~~~'  => :fenced_code
+    '--'   => [:open, ['comment', 'example', 'literal', 'listing', 'pass', 'quote', 'sidebar', 'source', 'verse', 'admonition', 'abstract', 'partintro'].to_set],
+    '----' => [:listing, ['literal', 'source'].to_set],
+    '....' => [:literal, ['listing', 'source'].to_set],
+    '====' => [:example, ['admonition'].to_set],
+    '****' => [:sidebar, Set.new],
+    '____' => [:quote, ['verse'].to_set],
+    '""'   => [:quote, ['verse'].to_set],
+    '++++' => [:pass, Set.new],
+    '|===' => [:table, Set.new],
+    ',===' => [:table, Set.new],
+    ':===' => [:table, Set.new],
+    '!===' => [:table, Set.new],
+    '////' => [:comment, Set.new],
+    '```'  => [:fenced_code, Set.new],
+    '~~~'  => [:fenced_code, Set.new]
   }
 
   BREAK_LINES = {
@@ -147,15 +179,48 @@ module Asciidoctor
     :upperroman => /[IVX]+\)/
   }
 
+  ORDERED_LIST_KEYWORDS = {
+    'loweralpha' => 'a',
+    'lowerroman' => 'i',
+    'upperalpha' => 'A',
+    'upperroman' => 'I'
+  }
+
   LIST_CONTINUATION = '+'
 
   LINE_BREAK = ' +'
 
   # NOTE allows for empty space in line as it could be left by the template engine
-  BLANK_LINES_PATTERN = /^\s*\n/
+  BLANK_LINE_PATTERN = /^[[:blank:]]*\n/
 
   LINE_FEED_ENTITY = '
' # or &#x0A;
 
+  # Flags to control compliance with the behavior of AsciiDoc
+  COMPLIANCE = {
+    # AsciiDoc terminates paragraphs adjacent to
+    # block content (delimiter or block attribute list)
+    # Compliance value: true
+    # TODO what about literal paragraph?
+    :block_terminates_paragraph => true,
+
+    # AsciiDoc does not treat paragraphs labeled with a
+    # verbatim style (literal, listing, source, verse)
+    # as verbatim; override this behavior
+    # Compliance value: false
+    :strict_verbatim_paragraphs => true,
+
+    # AsciiDoc allows start and end delimiters around
+    # a block to be different lengths
+    # this option requires that they be the same
+    # Compliance value: false
+    :congruent_block_delimiters => true,
+
+    # AsciiDoc will recognize commonly-used Markdown syntax
+    # to the degree it does not interfere with existing
+    # AsciiDoc behavior.
+    :markdown_syntax => true
+  }
+
   # The following pattern, which appears frequently, captures the contents between square brackets,
   # ignoring escaped closing brackets (closing brackets prefixed with a backslash '\' character)
   #
@@ -164,8 +229,11 @@ module Asciidoctor
   # Matches:
   # [enclosed text here] or [enclosed [text\] here]
   REGEXP = {
+    # NOTE: this is a inline admonition note
+    :admonition_inline => /^(#{ADMONITION_STYLES.to_a * '|'}):\s/,
+
     # [[Foo]]
-    :anchor           => /^\[\[([^\[\]]+)\]\]$/,
+    :anchor           => /^\[\[([^\s\[\]]+)\]\]$/,
 
     # Foowhatevs [[Bar]]
     :anchor_embedded  => /^(.*?)\s*\[\[([^\[\]]+)\]\]$/,
@@ -173,10 +241,19 @@ module Asciidoctor
     # [[ref]] (anywhere inline)
     :anchor_macro     => /\\?\[\[([\w":].*?)\]\]/,
 
-    # matches any block delimiter:
-    #   open, listing, example, literal, comment, quote, sidebar, passthrough, table
-    # NOTE position the most common blocks towards the front of the pattern
-    :any_blk          => %r{^(?:--|(?:-|\.|=|\*|_|\+|/){4,}|[\|!]={3,}|(?:`|~){3,}.*)$},
+    # matches any unbounded block delimiter:
+    #   listing, literal, example, sidebar, quote, passthrough, table, fenced code
+    # does not include open block or air quotes
+    # TIP position the most common blocks towards the front of the pattern
+    :any_blk          => %r{^(?:(?:-|\.|=|\*|_|\+|/){4,}|[\|,;!]={3,}|(?:`|~){3,}.*)$},
+
+    # detect a list item of any sort
+    # [[:graph:]] is a non-blank character
+    :any_list         => /^(?:
+                             <?\d+>[[:blank:]]+[[:graph:]]|
+                             [[:blank:]]*(?:(?:-|\*|\.){1,5}|\d+\.|[A-Za-z]\.|[IVXivx]+\))[[:blank:]]+[[:graph:]]|
+                             [[:blank:]]*.*?(?::{2,4}|;;)(?:[[:blank:]]+[[:graph:]]|$)
+                           )/x,
 
     # :foo: bar
     # :Author: Dan
@@ -204,19 +281,19 @@ module Asciidoctor
     # [NOTE, caption="Good to know"]
     # Can be defined by an attribute
     # [{lead}]
-    :blk_attr_list    => /^\[([\w\{].*)\]$/,
+    :blk_attr_list    => /^\[(|[[:blank:]]*[\w\{,.#"'].*)\]$/,
 
     # block attribute list or block id (bulk query)
-    :attr_line        => /^\[([\w\{].*|\[[^\[\]]+\])\]$/,
+    :attr_line        => /^\[(|[[:blank:]]*[\w\{,.#"'].*|\[[^\[\]]*\])\]$/,
 
     # attribute reference
     # {foo}
     # {counter:pcount:1}
-    :attr_ref         => /(\\?)\{(\w+(?:[\-:]\w+)*)(\\?)\}/,
+    :attr_ref         => /(\\)?\{((set|counter2?):.+?|\w+(?:[\-]\w+)*)(\\)?\}/,
 
     # The author info line the appears immediately following the document title
     # John Doe <john at anonymous.com>
-    :author_info      => /^\s*(\w[\w\-'.]*)(?: +(\w[\w\-'.]*))?(?: +(\w[\w\-'.]*))?(?: +<([^>]+)>)?$/,
+    :author_info      => /^(\w[\w\-'.]*)(?: +(\w[\w\-'.]*))?(?: +(\w[\w\-'.]*))?(?: +<([^>]+)>)?$/,
 
     # [[[Foo]]] (anywhere inline)
     :biblio_macro     => /\\?\[\[\[([\w:][\w:.-]*?)\]\]\]/,
@@ -229,7 +306,7 @@ module Asciidoctor
     :callout_scan     => /\\?<(\d+)>/,
 
     # <1> Foo
-    :colist           => /^<?(\d+)> (.*)/,
+    :colist           => /^<?(\d+)>[[:blank:]]+(.*)/,
 
     # ////
     # comment block
@@ -239,10 +316,18 @@ module Asciidoctor
     # // (and then whatever)
     :comment          => %r{^//(?:[^/]|$)},
 
-    # one,two
-    # one, two
-    # one , two
-    :csv_delimiter    => /[[:space:]]*,[[:space:]]*/,
+    # one,two;three;four
+    :ssv_or_csv_delim   => /,|;/,
+
+    # one two	three
+    :space_delim      => /([^\\])[[:blank:]]+/,
+
+    # Ctrl + Alt+T
+    # Ctrl,T
+    :kbd_delim        => /(?:\+|,)(?=[[:blank:]]*[^\1])/,
+
+    # one\ two\	three
+    :escaped_space    => /\\([[:blank:]])/,
 
     # 29
     :digits           => /^\d+$/,
@@ -255,27 +340,43 @@ module Asciidoctor
     #   That which precedes 'bar' (see also, <<bar>>)
     # The term may be an attribute reference
     # {term_foo}:: {def_foo}
-    :dlist            => /^\s*(.*?)(:{2,4}|;;)(?:[[:blank:]]+(.*))?$/,
+    # REVIEW leading space has already been stripped, so may not need in regex
+    :dlist            => /^[[:blank:]]*(.*?)(:{2,4}|;;)(?:[[:blank:]]+(.*))?$/,
     :dlist_siblings   => {
                            # (?:.*?[^:])? - a non-capturing group which grabs longest sequence of characters that doesn't end w/ colon
-                           '::' => /^\s*((?:.*[^:])?)(::)(?:[[:blank:]]+(.*))?$/,
-                           ':::' => /^\s*((?:.*[^:])?)(:::)(?:[[:blank:]]+(.*))?$/,
-                           '::::' => /^\s*((?:.*[^:])?)(::::)(?:[[:blank:]]+(.*))?$/,
-                           ';;' => /^\s*(.*)(;;)(?:[[:blank:]]+(.*))?$/
+                           '::' => /^[[:blank:]]*((?:.*[^:])?)(::)(?:[[:blank:]]+(.*))?$/,
+                           ':::' => /^[[:blank:]]*((?:.*[^:])?)(:::)(?:[[:blank:]]+(.*))?$/,
+                           '::::' => /^[[:blank:]]*((?:.*[^:])?)(::::)(?:[[:blank:]]+(.*))?$/,
+                           ';;' => /^[[:blank:]]*(.*)(;;)(?:[[:blank:]]+(.*))?$/
                          },
-    # ====
-    #:example          => /^={4,}$/,
 
     # footnote:[text]
     # footnoteref:[id,text]
     # footnoteref:[id]
-    :footnote_macro   => /\\?(footnote|footnoteref):\[((?:\\\]|[^\]])*?)\]/m,
+    :footnote_macro   => /\\?(footnote|footnoteref):\[((?:\\\]|[^\]])*?)\]/,
+
+    # kbd:[F3]
+    # kbd:[Ctrl+Shift+T]
+    # kbd:[Ctrl+\]]
+    # kbd:[Ctrl,T]
+    # btn:[Save]
+    :kbd_btn_macro    => /\\?(?:kbd|btn):\[((?:\\\]|[^\]])+?)\]/,
+
+    # menu:File[New...]
+    # menu:View[Page Style > No Style]
+    # menu:View[Page Style, No Style]
+    :menu_macro       => /\\?menu:(\w|\w.*?\S)\[[[:blank:]]*(.+?)?\]/,
+
+    # "File > New..."
+    :menu_inline_macro  => /\\?"(\w[^"]*?[[:blank:]]*>[[:blank:]]*[^"[:blank:]][^"]*)"/,
 
     # image::filename.png[Caption]
-    :image_blk        => /^image::(\S+?)\[(.*?)\]$/,
+    # video::http://youtube.com/12345[Cats vs Dogs]
+    :media_blk_macro  => /^(image|video|audio)::(\S+?)\[((?:\\\]|[^\]])*?)\]$/,
 
     # image:filename.png[Alt Text]
-    :image_macro      => /\\?image:([^\[]+)(?:\[([^\]]*)\])/,
+    # image:filename.png[More [Alt\] Text] (alt text becomes "More [Alt] Text")
+    :image_macro      => /\\?image:([^:\[]+)\[((?:\\\]|[^\]])*?)\]/,
 
     # indexterm:[Tigers,Big cats]
     # (((Tigers,Big cats)))
@@ -288,6 +389,9 @@ module Asciidoctor
     # whitespace at the beginning of the line
     :leading_blanks   => /^([[:blank:]]*)/,
 
+    # leading parent directory references in path
+    :leading_parent_dirs => /^(?:\.\.\/)*/,
+
     # +   From the Asciidoc User Guide: "A plus character preceded by at
     #     least one space character at the end of a non-blank line forces
     #     a line break. It generates a line break (br) tag for HTML outputs.
@@ -299,39 +403,32 @@ module Asciidoctor
 
     # inline link and some inline link macro
     # FIXME revisit!
-    :link_inline      => %r{(^|link:|\s|>|<|[\(\)\[\]])(\\?(?:https?|ftp)://[^\s\[<]*[^\s.\)\[<])(?:\[((?:\\\]|[^\]])*?)\])?},
+    :link_inline      => %r{(^|link:|\s|>|<|[\(\)\[\]])(\\?(?:https?|ftp|irc)://[^\s\[<]*[^\s.,\[<])(?:\[((?:\\\]|[^\]])*?)\])?},
 
     # inline link macro
     # link:path[label]
-    :link_macro       => /\\?link:([^\s\[]+)(?:\[((?:\\\]|[^\]])*?)\])/,
-
-    # ----
-    #:listing          => /^\-{4,}$/,
+    :link_macro       => /\\?(?:link|mailto):([^\s\[]+)(?:\[((?:\\\]|[^\]])*?)\])/,
 
-    # ....
-    #:literal          => /^\.{4,}$/,
+    # inline email address
+    # doc.writer at asciidoc.org
+    :email_inline     => /[\\>:]?\w[\w.%+-]*@[[:alnum:]][[:alnum:].-]*\.[[:alpha:]]{2,4}\b/, 
 
     # <TAB>Foo  or one-or-more-spaces-or-tabs then whatever
     :lit_par          => /^([[:blank:]]+.*)$/,
 
-    # --
-    #:open_blk         => /^\-\-$/,
-
     # . Foo (up to 5 consecutive dots)
     # 1. Foo (arabic, default)
     # a. Foo (loweralpha)
     # A. Foo (upperalpha)
     # i. Foo (lowerroman)
     # I. Foo (upperroman)
-    :olist            => /^\s*(\d+\.|[a-z]\.|[ivx]+\)|\.{1,5}) +(.*)$/i,
+    # REVIEW leading space has already been stripped, so may not need in regex
+    :olist            => /^[[:blank:]]*(\.{1,5}|\d+\.|[A-Za-z]\.|[IVXivx]+\))[[:blank:]]+(.*)$/,
 
     # ''' (ruler)
     # <<< (pagebreak)
     :break_line        => /^('|<){3,}$/,
 
-    # ++++
-    #:pass             => /^\+{4,}$/,
-
     # inline passthrough macros
     # +++text+++
     # $$text$$
@@ -347,22 +444,13 @@ module Asciidoctor
     :pass_lit         => /(^|[^`\w])(\\?`([^`\s]|[^`\s].*?\S)`)(?![`\w])/m,
 
     # placeholder for extracted passthrough text
-    :pass_placeholder => /\x0(\d+)\x0/,
-
-    # ____
-    #:quote            => /^_{4,}$/,
+    :pass_placeholder => /\e(\d+)\e/,
 
     # The document revision info line the appears immediately following the
     # document title author info line, if present
     # v1.0, 2013-01-01: Ring in the new year release
     :revision_info    => /^(?:\D*(.*?),)?(?:\s*(?!:)(.*?))(?:\s*(?!^):\s*(.*))?$/,
 
-    # '''
-    #:ruler            => /^'{3,}$/,
-
-    # ****
-    #:sidebar_blk      => /^\*{4,}$/,
-
     # \' within a word
     :single_quote_esc => /(\w)\\'(\w)/,
     # an alternative if our backend generated single-quoted html/xml attributes
@@ -371,16 +459,6 @@ module Asciidoctor
     # used for sanitizing attribute names
     :illegal_attr_name_chars => /[^\w\-]/,
 
-    # |===
-    # |table
-    # |===
-    #:table            => /^\|={3,}$/,
-
-    # !===
-    # !table
-    # !===
-    #:table_nested     => /^!={3,}$/,
-
     # 1*h,2*,^3e
     :table_colspec    => /^(?:(\d+)\*)?([<^>](?:\.[<^>]?)?|(?:[<^>]?\.)?[<^>])?(\d+)?([a-z])?$/,
 
@@ -394,8 +472,10 @@ module Asciidoctor
     # .Foo   but not  . Foo or ..Foo
     :blk_title        => /^\.([^\s.].*)$/,
 
+    # matches double quoted text, capturing quote char and text (single-line)
     :dbl_quoted       => /^("|)(.*)\1$/,
 
+    # matches double quoted text, capturing quote char and text (multi-line)
     :m_dbl_quoted     => /^("|)(.*)\1$/m,
 
     # == Foo
@@ -411,17 +491,23 @@ module Asciidoctor
     # match[1] is the delimiter, whose length determines the level
     # match[2] is the title itself
     # match[3] is an inline anchor, which becomes the section id
-    :section_title     => /^(={1,5})\s+(\S.*?)(?:\s*\[\[([^\[]+)\]\])?(?:\s+\1)?$/,
+    :section_title     => /^((?:=|#){1,6})\s+(\S.*?)(?:\s*\[\[([^\[]+)\]\])?(?:\s+\1)?$/,
 
     # does not begin with a dot and has at least one alphanumeric character
     :section_name      => /^((?=.*\w+.*)[^.].*?)$/,
 
     # ======  || ------ || ~~~~~~ || ^^^^^^ || ++++++
-    :section_underline => /^([=\-~^\+])+$/,
+    # TODO build from SECTION_LEVELS keys
+    :section_underline => /^(?:=|-|~|\^|\+)+$/,
+
+    # toc::[]
+    # toc::[levels=2]
+    :toc              => /^toc::\[(.*?)\]$/,
 
     # * Foo (up to 5 consecutive asterisks)
     # - Foo
-    :ulist            => /^ \s* (- | \*{1,5}) \s+ (.*) $/x,
+    # REVIEW leading space has already been stripped, so may not need in regex
+    :ulist            => /^[[:blank:]]*(-|\*{1,5})[[:blank:]]+(.*)$/,
 
     # inline xref macro
     # <<id,reftext>> (special characters have already been escaped, hence the entity references)
@@ -443,15 +529,16 @@ module Asciidoctor
     #:eval_expr        => /^(true|false|("|'|)\{\w+(?:\-\w+)*\}\2|("|')[^\3]*\3|\-?\d+(?:\.\d+)*)[[:blank:]]*(==|!=|<=|>=|<|>)[[:blank:]]*(true|false|("|'|)\{\w+(?:\-\w+)*\}\6|("|')[^\7]*\7|\-?\d+(?:\.\d+)*)$/,
 
     # include::chapter1.ad[]
-    :include_macro    => /^\\?include::([^\[]+)\[\]$/,
+    # include::example.txt[lines=1;2;5..10]
+    :include_macro    => /^\\?include::([^\[]+)\[(.*?)\]$/,
 
     # http://domain
     # https://domain
     # data:info
-    :uri_sniff        => /^[[:alpha:]][[:alnum:].+-]*:/i
-  }
+    :uri_sniff        => /^[[:alpha:]][[:alnum:].+-]*:/i,
 
-  ADMONITION_STYLES = ['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION']
+    :uri_encode_chars => /[^\w\-.!~*';:@=+$,()\[\]]/
+  }
 
   INTRINSICS = Hash.new{|h,k| STDERR.puts "Missing intrinsic: #{k.inspect}"; "{#{k}}"}.merge(
     {
@@ -493,6 +580,7 @@ module Asciidoctor
   }
 
   SPECIAL_CHARS_PATTERN = /[#{SPECIAL_CHARS.keys.join}]/
+  #SPECIAL_CHARS_PATTERN = /(?:<|>|&(?![[:alpha:]]{2,};|#[[:digit:]]{2,}+;|#x[[:alnum:]]{2,}+;))/
 
   # unconstrained quotes:: can appear anywhere
   # constrained quotes:: must be bordered by non-word characters
@@ -545,33 +633,29 @@ module Asciidoctor
   # order is significant
   REPLACEMENTS = [
     # (C)
-    [/(^|[^\\])\(C\)/, '\1©'], 
+    [/\\?\(C\)/, '©', :none],
     # (R)
-    [/(^|[^\\])\(R\)/, '\1®'],
+    [/\\?\(R\)/, '®', :none],
     # (TM)
-    [/(^|[^\\])\(TM\)/, '\1™'],
+    [/\\?\(TM\)/, '™', :none],
     # foo -- bar
-    [/(^|\n| )--( |\n|$)/, ' — '],
+    [/(^|\n| |\\)--( |\n|$)/, ' — ', :none],
     # foo--bar
-    [/(\w)--(?=\w)/, '\1—'],
+    [/(\w)\\?--(?=\w)/, '—', :leading],
     # ellipsis
-    [/(^|[^\\])\.\.\./, '\1…'],
+    [/\\?\.\.\./, '…', :leading],
     # single quotes
-    [/(\w)'(\w)/, '\1’\2'],
-    # escaped single quotes
-    [/(\w)\\'(\w)/, '\1\'\2'],
+    [/(\w)\\?'(\w)/, '’', :bounding],
     # right arrow ->
-    [/(^|[^\\])->/, '\1→'],
+    [/\\?->/, '→', :none],
     # right double arrow =>
-    [/(^|[^\\])=>/, '\1⇒'],
+    [/\\?=>/, '⇒', :none],
     # left arrow <-
-    [/(^|[^\\])<-/, '\1←'],
+    [/\\?<-/, '←', :none],
     # right left arrow <=
-    [/(^|[^\\])<=/, '\1⇐'],
-    # and so on...
-    
-    # restore entities; TODO needs cleanup
-    [/(^|[^\\])&(#[a-z0-9]+;)/i, '\1&\2']
+    [/\\?<=/, '⇐', :none],
+    # restore entities
+    [/\\?(&)amp;((?:[[:alpha:]]+|#[[:digit:]]+|#x[[:alnum:]]+);)/, '', :bounding]
   ]
 
   # Public: Parse the AsciiDoc source input into an Asciidoctor::Document
@@ -581,16 +665,42 @@ module Asciidoctor
   # Document object.
   #
   # input   - the AsciiDoc source as a IO, String or Array.
-  # options - a Hash of options to control processing (default: {})
-  #           see Asciidoctor::Document#initialize for details
+  # options - a String, Array or Hash of options to control processing (default: {})
+  #           String and Array values are converted into a Hash.
+  #           See Asciidoctor::Document#initialize for details about options.
   # block   - a callback block for handling include::[] directives
   #
   # returns the Asciidoctor::Document
   def self.load(input, options = {}, &block)
+    if (monitor = options.fetch(:monitor, false))
+      start = Time.now
+    end
+
+    attrs = (options[:attributes] ||= {})
+    if attrs.is_a? Hash
+      # all good; placed here as optimization
+    elsif attrs.is_a? Array
+      attrs = options[:attributes] = attrs.inject({}) do |accum, entry|
+        k, v = entry.split '=', 2
+        accum[k] = v || ''
+        accum
+      end
+    elsif attrs.is_a? String
+      # convert non-escaped spaces into null character, so we split on the
+      # correct spaces chars, and restore escaped spaces
+      attrs = attrs.gsub(REGEXP[:space_delim], "\\1\0").gsub(REGEXP[:escaped_space], '\1')
+
+      attrs = options[:attributes] = attrs.split("\0").inject({}) do |accum, entry|
+        k, v = entry.split '=', 2
+        accum[k] = v || ''
+        accum
+      end
+    else
+      raise ArgumentError, 'illegal type for attributes option'
+    end
+
     lines = nil
-    if input.is_a?(File)
-      options[:attributes] ||= {}
-      attrs = options[:attributes]
+    if input.is_a? File
       lines = input.readlines
       input_mtime = input.mtime
       input_path = File.expand_path(input.path)
@@ -612,7 +722,19 @@ module Asciidoctor
       raise "Unsupported input type: #{input.class}"
     end
 
-    Document.new(lines, options, &block) 
+    if monitor
+      read_time = Time.now - start
+      start = Time.now
+    end
+
+    doc = Document.new(lines, options, &block) 
+    if monitor
+      parse_time = Time.now - start
+      monitor[:read] = read_time
+      monitor[:parse] = parse_time
+      monitor[:load] = read_time + parse_time
+    end
+    doc
   end
 
   # Public: Parse the contents of the AsciiDoc source file into an Asciidoctor::Document
@@ -622,8 +744,9 @@ module Asciidoctor
   # attributes on the Document.
   #
   # input   - the String AsciiDoc source filename
-  # options - a Hash of options to control processing (default: {})
-  #           see Asciidoctor::Document#initialize for details
+  # options - a String, Array or Hash of options to control processing (default: {})
+  #           String and Array values are converted into a Hash.
+  #           See Asciidoctor::Document#initialize for details about options.
   # block   - a callback block for handling include::[] directives
   #
   # returns the Asciidoctor::Document
@@ -654,22 +777,27 @@ module Asciidoctor
   # default and the rendered output is returned.
   #
   # input   - the String AsciiDoc source filename
-  # options - a Hash of options to control processing (default: {})
-  #           see Asciidoctor::Document#initialize for details
+  # options - a String, Array or Hash of options to control processing (default: {})
+  #           String and Array values are converted into a Hash.
+  #           See Asciidoctor::Document#initialize for details about options.
   # block   - a callback block for handling include::[] directives
   #
-  # returns nothing if the rendered output String is written to a file,
-  # otherwise the rendered output String is returned
+  # returns the Document object if the rendered result String is written to a
+  # file, otherwise the rendered result String
   def self.render(input, options = {}, &block)
     in_place = options.delete(:in_place) || false
     to_file = options.delete(:to_file)
     to_dir = options.delete(:to_dir)
     mkdirs = options.delete(:mkdirs) || false
+    monitor = options.fetch(:monitor, false)
 
     write_in_place = in_place && input.is_a?(File)
     write_to_target = to_file || to_dir
+    stream_output = !to_file.nil? && to_file.respond_to?(:write)
 
-    raise ArgumentError, ':in_place with input file must not accompany :to_dir or :to_file' if write_in_place && write_to_target
+    if write_in_place && write_to_target
+      raise ArgumentError, 'the option :in_place cannot be used with either the :to_dir or :to_file option'
+    end
 
     if !options.has_key?(:header_footer) && (write_in_place || write_to_target)
       options[:header_footer] = true
@@ -677,21 +805,26 @@ module Asciidoctor
 
     doc = Asciidoctor.load(input, options, &block)
 
-    if write_in_place
+    if to_file == '/dev/null'
+      return doc
+    elsif write_in_place
       to_file = File.join(File.dirname(input.path), "#{doc.attributes['docname']}#{doc.attributes['outfilesuffix']}")
-    elsif write_to_target
+    elsif !stream_output && write_to_target
+      working_dir = options.has_key?(:base_dir) ? File.expand_path(options[:base_dir]) : File.expand_path(Dir.pwd)
+      # QUESTION should the jail be the working_dir or doc.base_dir???
+      jail = doc.safe >= SafeMode::SAFE ? working_dir : nil
       if to_dir
-        to_dir = doc.normalize_asset_path(to_dir, 'to_dir', false)
+        to_dir = doc.normalize_system_path(to_dir, working_dir, jail, :target_name => 'to_dir', :recover => false)
         if to_file
-          # normalize again, to_file could have dirty bits
-          to_file = doc.normalize_asset_path(File.expand_path(to_file, to_dir), 'to_file', false)
+          to_file = doc.normalize_system_path(to_file, to_dir, nil, :target_name => 'to_dir', :recover => false)
           # reestablish to_dir as the final target directory (in the case to_file had directory segments)
           to_dir = File.dirname(to_file)
         else
           to_file = File.join(to_dir, "#{doc.attributes['docname']}#{doc.attributes['outfilesuffix']}")
         end
       elsif to_file
-        to_file = doc.normalize_asset_path(to_file, 'to_file', false)
+        to_file = doc.normalize_system_path(to_file, working_dir, jail, :target_name => 'to_dir', :recover => false)
+        # establish to_dir as the final target directory (in the case to_file had directory segments)
         to_dir = File.dirname(to_file)
       end
 
@@ -705,11 +838,48 @@ module Asciidoctor
       end
     end
 
+    start = Time.now if monitor
+    output = doc.render
+
+    if monitor
+      render_time = Time.now - start
+      monitor[:render] = render_time
+      monitor[:load_render] = monitor[:load] + render_time
+    end
+
     if to_file
-      File.open(to_file, 'w') {|file| file.write doc.render }
-      nil
+      start = Time.now if monitor
+      if stream_output
+        to_file.write output.rstrip
+        # ensure there's a trailing endline
+        to_file.write "\n"
+      else
+        File.open(to_file, 'w') {|file| file.write output }
+        # these assignments primarily for testing, diagnostics or reporting
+        doc.attributes['outfile'] = outfile = File.expand_path(to_file)
+        doc.attributes['outdir'] = File.dirname(outfile)
+      end
+      if monitor
+        write_time = Time.now - start
+        monitor[:write] = write_time
+        monitor[:total] = monitor[:load_render] + write_time
+      end
+
+      # NOTE document cannot control this behavior if safe >= SafeMode::SERVER
+      if !stream_output && doc.attr?('basebackend-html') && doc.attr?('copycss') &&
+          doc.attr?('linkcss') && DEFAULT_STYLESHEET_KEYS.include?(doc.attr('stylesheet'))
+        Helpers.require_library 'fileutils'
+        outdir = doc.attr('outdir')
+        stylesdir = doc.normalize_system_path(doc.attr('stylesdir'), outdir,
+            doc.safe >= SafeMode::SAFE ? outdir : nil)
+        Helpers.mkdir_p stylesdir
+        File.open(File.join(stylesdir, DEFAULT_STYLESHEET_NAME), 'w') {|f|
+          f.write Asciidoctor::HTML5.default_asciidoctor_stylesheet
+        }
+      end
+      doc
     else
-      doc.render
+      output
     end
   end
 
@@ -717,24 +887,17 @@ module Asciidoctor
   # and render it to the specified backend format
   #
   # input   - the String AsciiDoc source filename
-  # options - a Hash of options to control processing (default: {})
-  #           see Asciidoctor::Document#initialize for details
+  # options - a String, Array or Hash of options to control processing (default: {})
+  #           String and Array values are converted into a Hash.
+  #           See Asciidoctor::Document#initialize for details about options.
   # block   - a callback block for handling include::[] directives
   #
-  # returns nothing if the rendered output String is written to a file,
-  # otherwise the rendered output String is returned
+  # returns the Document object if the rendered result String is written to a
+  # file, otherwise the rendered result String
   def self.render_file(filename, options = {}, &block)
     Asciidoctor.render(File.new(filename), options, &block)
   end
 
-  # NOTE still contemplating this method
-  #def self.parse_document_header(input, options = {})
-  #  document = Document.new [], options
-  #  reader = Reader.new input, document, true
-  #  Lexer.parse_document_header reader, document
-  #  document
-  #end
-
   # modules
   require 'asciidoctor/debug'
   require 'asciidoctor/substituters'
@@ -750,10 +913,10 @@ module Asciidoctor
   require 'asciidoctor/block'
   require 'asciidoctor/callouts'
   require 'asciidoctor/document'
-  #require 'asciidoctor/errors'
   require 'asciidoctor/inline'
   require 'asciidoctor/lexer'
   require 'asciidoctor/list_item'
+  require 'asciidoctor/path_resolver'
   require 'asciidoctor/reader'
   require 'asciidoctor/renderer'
   require 'asciidoctor/section'
diff --git a/lib/asciidoctor/abstract_block.rb b/lib/asciidoctor/abstract_block.rb
index 73b6086..c82d4a3 100644
--- a/lib/asciidoctor/abstract_block.rb
+++ b/lib/asciidoctor/abstract_block.rb
@@ -9,11 +9,15 @@ class AbstractBlock < AbstractNode
   # Public: Set the String block title.
   attr_writer :title
 
+  # Public: Get/Set the caption for this block
+  attr_accessor :caption
+
   def initialize(parent, context)
     super(parent, context)
     @blocks = []
     @id = nil
     @title = nil
+    @caption = nil
     if context == :document
       @level = 0
     elsif !parent.nil? && !self.is_a?(Section)
@@ -63,7 +67,7 @@ class AbstractBlock < AbstractNode
   # whether this Block *can* have block content
   # that should be the option 'sectionbody'
   def blocks?
-    !blocks.empty?
+    !@blocks.empty?
   end
 
   # Public: Get the element at i in the array of blocks.
@@ -189,6 +193,47 @@ class AbstractBlock < AbstractNode
     }
   end
 
+  # Public: Generate a caption and assign it to this block if one
+  # is not already assigned.
+  #
+  # If the block has a title and a caption prefix is available
+  # for this block, then build a caption from this information,
+  # assign it a number and store it to the caption attribute on
+  # the block.
+  #
+  # If an explicit caption has been specified on this block, then
+  # do nothing.
+  #
+  # key         - The prefix of the caption and counter attribute names.
+  #               If not provided, the name of the context for this block
+  #               is used. (default: nil).
+  #
+  # returns nothing
+  def assign_caption(caption = nil, key = nil)
+    unless title? || @caption.nil?
+      return nil
+    end
+
+    if caption.nil?
+      if @document.attr?('caption')
+        @caption = @document.attr('caption')
+      elsif title?
+        key ||= @context.to_s
+        caption_key = "#{key}-caption"
+        if @document.attributes.has_key?(caption_key)
+          caption_title = @document.attributes["#{key}-caption"]
+          caption_num = @document.counter_increment("#{key}-number", self)
+          @caption = "#{caption_title} #{caption_num}. "
+        end
+      else
+        @caption = caption
+      end
+    else
+      @caption = caption
+    end
+    nil
+  end
+
   # Internal: Assign the next index (0-based) to this section
   #
   # Assign the next index of this section within the parent
diff --git a/lib/asciidoctor/abstract_node.rb b/lib/asciidoctor/abstract_node.rb
index f765af9..ca66e97 100644
--- a/lib/asciidoctor/abstract_node.rb
+++ b/lib/asciidoctor/abstract_node.rb
@@ -43,14 +43,17 @@ class AbstractNode
   # Document node and return the value of the attribute if found. Otherwise,
   # return the default value, which defaults to nil.
   #
-  # name    - the name of the attribute to lookup as a String or Symbol
-  # default - the value to return if the attribute is not found (default: nil)
+  # name    - the String or Symbol name of the attribute to lookup
+  # default - the Object value to return if the attribute is not found (default: nil)
+  # inherit - a Boolean indicating whether to check for the attribute on the
+  #           AsciiDoctor::Document if not found on this node (default: false)
   #
   # return the value of the attribute or the default value if the attribute
   # is not found in the attributes of this node or the document node
-  def attr(name, default = nil)
+  def attr(name, default = nil, inherit = true)
     name = name.to_s if name.is_a?(Symbol)
-    if self == @document
+    inherit = false if self == @document
+    if !inherit
       default.nil? ? @attributes[name] : @attributes.fetch(name, default)
     else
       default.nil? ? @attributes.fetch(name, @document.attr(name)) :
@@ -59,26 +62,29 @@ class AbstractNode
   end
 
   # Public: Check if the attribute is defined, optionally performing a
-  # comparison of its value
+  # comparison of its value if expected is not nil
   #
   # Check if the attribute is defined. First look in the attributes on this
   # node. If not found, and this node is a child of the Document node, look in
   # the attributes of the Document node. If the attribute is found and a
-  # comparison value is specified, return whether the two values match.
+  # comparison value is specified (not nil), return whether the two values match.
   # Otherwise, return whether the attribute was found.
   #
-  # name   - the name of the attribute to lookup as a String or Symbol
-  # expect - the expected value of the attribute (default: nil)
+  # name    - the String or Symbol name of the attribute to lookup
+  # expect  - the expected Object value of the attribute (default: nil)
+  # inherit - a Boolean indicating whether to check for the attribute on the
+  #           AsciiDoctor::Document if not found on this node (default: false)
   #
   # return a Boolean indicating whether the attribute exists and, if a
   # comparison value is specified, whether the value of the attribute matches
   # the comparison value
-  def attr?(name, expect = nil)
+  def attr?(name, expect = nil, inherit = true)
     name = name.to_s if name.is_a?(Symbol)
+    inherit = false if self == @document
     if expect.nil?
       if @attributes.has_key? name
         true
-      elsif self != @document
+      elsif inherit
         @document.attributes.has_key? name
       else
         false
@@ -86,7 +92,7 @@ class AbstractNode
     else
       if @attributes.has_key? name
         @attributes[name] == expect
-      elsif self != @document && @document.attributes.has_key?(name)
+      elsif inherit && @document.attributes.has_key?(name)
         @document.attributes[name] == expect
       else
         false
@@ -94,6 +100,27 @@ class AbstractNode
     end
   end
 
+  # Public: Assign the value to the specified key in this
+  # block's attributes hash.
+  #
+  # key - The attribute key (or name)
+  # val - The value to assign to the key
+  #
+  # returns a flag indicating whether the assignment was performed
+  def set_attr(key, val, overwrite = nil)
+    if overwrite.nil?
+      @attributes[key] = val
+      true
+    else
+      if overwrite || @attributes.has_key?(key)
+        @attributes[key] = val
+        true
+      else
+        false
+      end
+    end
+  end
+
   # Public: Get the execution context of this object (via Kernel#binding).
   #
   # This method is used to set the 'self' reference as well as local variables
@@ -156,11 +183,35 @@ class AbstractNode
     if attr? 'icon'
       image_uri(attr('icon'), nil)
     else
-      image_uri(name + '.' + @document.attr('icontype', 'png'), 'iconsdir')
+      image_uri("#{name}.#{@document.attr('icontype', 'png')}", 'iconsdir')
+    end
+  end
+
+  # Public: Construct a URI reference to the target media.
+  #
+  # If the target media is a URI reference, then leave it untouched.
+  #
+  # The target media is resolved relative to the directory retrieved from the
+  # specified attribute key, if provided.
+  #
+  # The return value can be safely used in a media tag (img, audio, video).
+  #
+  # target        - A String reference to the target media
+  # asset_dir_key - The String attribute key used to lookup the directory where
+  #                 the media is located (default: 'imagesdir')
+  #
+  # Returns A String reference for the target media
+  def media_uri(target, asset_dir_key = 'imagesdir')
+    if target.include?(':') && target.match(Asciidoctor::REGEXP[:uri_sniff])
+      target
+    elsif asset_dir_key && attr?(asset_dir_key)
+      normalize_web_path(target, @document.attr(asset_dir_key))
+    else
+      normalize_web_path(target)
     end
   end
 
-  # Public: Construct a reference or data URI to the target image.
+  # Public: Construct a URI reference or data URI to the target image.
   #
   # If the target image is a URI reference, then leave it untouched.
   #
@@ -185,9 +236,9 @@ class AbstractNode
     elsif @document.safe < Asciidoctor::SafeMode::SECURE && @document.attr?('data-uri')
       generate_data_uri(target_image, asset_dir_key)
     elsif asset_dir_key && attr?(asset_dir_key)
-      File.join(@document.attr(asset_dir_key), target_image)
+      normalize_web_path(target_image, @document.attr(asset_dir_key))
     else
-      target_image
+      normalize_web_path(target_image)
     end
   end
 
@@ -206,11 +257,21 @@ class AbstractNode
   def generate_data_uri(target_image, asset_dir_key = nil)
     Helpers.require_library 'base64'
 
-    mimetype = 'image/' + File.extname(target_image)[1..-1]
+    ext = File.extname(target_image)[1..-1]
+    mimetype = 'image/' + ext
+    mimetype = "#{mimetype}+xml" if ext == 'svg'
     if asset_dir_key
-      image_path = File.join(normalize_asset_path(@document.attr(asset_dir_key, '.'), asset_dir_key), target_image)
+      #asset_dir_path = normalize_system_path(@document.attr(asset_dir_key), nil, nil, :target_name => asset_dir_key)
+      #image_path = normalize_system_path(target_image, asset_dir_path, nil, :target_name => 'image')
+      image_path = normalize_system_path(target_image, @document.attr(asset_dir_key), nil, :target_name => 'image')
     else
-      image_path = normalize_asset_path(target_image)
+      image_path = normalize_system_path(target_image)
+    end
+
+    if !File.readable? image_path
+      puts "asciidoctor: WARNING: image to embed not found or not readable: #{image_path}"
+      return "data:#{mimetype}:base64,"
+      #return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
     end
 
     bindata = nil
@@ -219,88 +280,82 @@ class AbstractNode
     else
       bindata = File.open(image_path, 'rb') {|file| file.read }
     end
-    'data:' + mimetype + ';base64,' + Base64.encode64(bindata).delete("\n")
+    "data:#{mimetype};base64,#{Base64.encode64(bindata).delete("\n")}"
   end
 
-  # Public: Normalize the asset file or directory to a concrete and rinsed path
-  #
-  # The most important functionality in this method is to prevent the asset
-  # reference from resolving to a directory outside of the chroot directory
-  # (which defaults to the directory of the source file, stored in the base_dir
-  # instance variable on Document) if the document safe level is set to
-  # SafeMode::SAFE or greater (a condition which is true by default).
-  #
-  # asset_ref    - the String asset file or directory referenced in the document
-  #                or configuration attribute
-  # asset_name   - the String name of the file or directory being resolved (for use in
-  #                the warning message) (default: 'path')
-  #
-  # Examples
-  #
-  #  # given these fixtures
-  #  document.base_dir
-  #  # => "/path/to/chroot"
-  #  document.safe >= Asciidoctor::SafeMode::SAFE
-  #  # => true
-  #
-  #  # then
-  #  normalize_asset_path('images')
-  #  # => "/path/to/chroot/images"
-  #  normalize_asset_path('/etc/images')
-  #  # => "/path/to/chroot/images"
-  #  normalize_asset_path('../images')
-  #  # => "/path/to/chroot/images"
-  #
-  #  # given these fixtures
-  #  document.base_dir
-  #  # => "/path/to/chroot"
-  #  document.safe >= Asciidoctor::SafeMode::SAFE
-  #  # => false
-  #
-  #  # then
-  #  normalize_asset_path('images')
-  #  # => "/path/to/chroot/images"
-  #  normalize_asset_path('/etc/images')
-  #  # => "/etc/images"
-  #  normalize_asset_path('../images')
-  #  # => "/path/to/images"
-  #
-  # Returns The normalized asset file or directory as a String path
-  #--
-  # TODO this method is missing a coordinate; it should be able to resolve
-  # both the directory reference and the path to an asset in it; callers
-  # of this method are still doing a File.join to finish the task
-  def normalize_asset_path(asset_ref, asset_name = 'path', autocorrect = true)
-    # TODO we may use pathname enough to make it a top-level require
-    Helpers.require_library 'pathname'
-
-    input_path = @document.base_dir
-    asset_path = Pathname.new(asset_ref)
-    
-    if asset_path.relative?
-      asset_path = File.expand_path(File.join(input_path, asset_ref))
+  # Public: Read the contents of the file at the specified path.
+  # This method assumes that the path is safe to read. It checks
+  # that the file is readable before attempting to read it.
+  #
+  # path            - the String path from which to read the contents
+  # warn_on_failure - a Boolean that controls whether a warning is issued if
+  #                   the file cannot be read
+  #
+  # returns the contents of the file at the specified path, or nil
+  # if the file does not exist.
+  def read_asset(path, warn_on_failure = false)
+    if File.readable? path
+      File.read(path).chomp
     else
-      asset_path = asset_path.cleanpath.to_s
+      puts "asciidoctor: WARNING: file does not exist or cannot be read: #{path}" if warn_on_failure
+      nil
     end
+  end
 
-    if @document.safe >= SafeMode::SAFE
-      relative_asset_path = Pathname.new(asset_path).relative_path_from(Pathname.new(input_path)).to_s
-      if relative_asset_path.start_with?('..')
-        if autocorrect
-          puts "asciidoctor: WARNING: #{asset_name} has illegal reference to ancestor of base directory"
-        else
-          raise SecurityError, "#{asset_name} has reference to path outside of base directory, disallowed in safe mode: #{asset_path}"
-        end
-        relative_asset_path.sub!(/^(?:\.\.\/)*/, '')
-        # just to be absolutely sure ;)
-        if relative_asset_path[0..0] == '.'
-          raise 'Substitution of parent path references failed for ' + relative_asset_path
-        end
-        asset_path = File.expand_path(File.join(input_path, relative_asset_path))
-      end
+  # Public: Normalize the web page using the PathResolver.
+  #
+  # See PathResolver::web_path(target, start) for details.
+  #
+  # target - the String target path
+  # start  - the String start (i.e, parent) path (optional, default: nil)
+  #
+  # returns the resolved String path 
+  def normalize_web_path(target, start = nil)
+    PathResolver.new.web_path(target, start)
+  end
+
+  # Public: Resolve and normalize a secure path from the target and start paths
+  # using the PathResolver.
+  #
+  # See PathResolver::system_path(target, start, jail, opts) for details.
+  #
+  # The most important functionality in this method is to prevent resolving a
+  # path outside of the jail (which defaults to the directory of the source
+  # file, stored in the base_dir instance variable on Document) if the document
+  # safe level is set to SafeMode::SAFE or greater (a condition which is true
+  # by default).
+  #
+  # target - the String target path
+  # start  - the String start (i.e., parent) path
+  # jail   - the String jail path to confine the resolved path
+  # opts   - an optional Hash of options to control processing (default: {}):
+  #          * :recover is used to control whether the processor should auto-recover
+  #              when an illegal path is encountered
+  #          * :target_name is used in messages to refer to the path being resolved
+  #
+  # raises a SecurityError if a jail is specified and the resolved path is
+  # outside the jail.
+  #
+  # returns a String path resolved from the start and target paths, with any
+  # parent references resolved and self references removed. If a jail is provided,
+  # this path will be guaranteed to be contained within the jail.
+  def normalize_system_path(target, start = nil, jail = nil, opts = {})
+    if start.nil?
+      start = @document.base_dir
     end
+    if jail.nil? && @document.safe >= SafeMode::SAFE
+      jail = @document.base_dir
+    end
+    PathResolver.new.system_path(target, start, jail, opts)
+  end
 
-    asset_path
+  # Public: Normalize the asset file or directory to a concrete and rinsed path
+  #
+  # Delegates to normalize_system_path, with the start path set to the value of
+  # the base_dir instance variable on the Document object.
+  def normalize_asset_path(asset_ref, asset_name = 'path', autocorrect = true)
+    normalize_system_path(asset_ref, @document.base_dir, nil,
+        :target_name => asset_name, :recover => autocorrect)
   end
 
 end
diff --git a/lib/asciidoctor/attribute_list.rb b/lib/asciidoctor/attribute_list.rb
index 6859298..fe7c632 100644
--- a/lib/asciidoctor/attribute_list.rb
+++ b/lib/asciidoctor/attribute_list.rb
@@ -45,9 +45,6 @@ class AttributeList
   # TODO named attributes cannot contain dash characters
   NAME_PATTERN = /[A-Za-z:_][A-Za-z:_\-\.]*/
 
-  # Public: A regular expression for splitting a comma-separated string
-  CSV_SPLIT_PATTERN = /[ \t]*,[ \t]*/
-
   def initialize(source, block = nil, quotes = ['\'', '"'], delimiter = ',', escape_char = '\\')
     @scanner = ::StringScanner.new source
     @block = block
@@ -84,6 +81,7 @@ class AttributeList
 
   def self.rekey(attributes, pos_attrs)
     pos_attrs.each_with_index do |key, index|
+      next if key.nil?
       pos = index + 1
       unless (val = attributes[pos]).nil?
         attributes[key] = val
@@ -166,9 +164,11 @@ class AttributeList
     else
       resolved_value = value
       # example: options="opt1,opt2,opt3"
-      if name == 'options'
-        resolved_value.split(CSV_SPLIT_PATTERN).each do |o|
-          @attributes[o + '-option'] = nil
+      # opts is an alias for options
+      if name == 'options' || name == 'opts'
+        name = 'options'
+        resolved_value.split(',').each do |o|
+          @attributes[o.strip + '-option'] = ''
         end
       elsif single_quoted_value && !@block.nil?
         resolved_value = @block.apply_normal_subs(value)
diff --git a/lib/asciidoctor/backends/_stylesheets.rb b/lib/asciidoctor/backends/_stylesheets.rb
new file mode 100644
index 0000000..25501ea
--- /dev/null
+++ b/lib/asciidoctor/backends/_stylesheets.rb
@@ -0,0 +1,324 @@
+module Asciidoctor
+module HTML5
+  # Internal: Generate the default stylesheet for CodeRay
+  #
+  # returns the default CodeRay stylesheet as a String
+  def self.default_coderay_stylesheet
+    ::Asciidoctor::Helpers.require_library 'coderay'
+    ::CodeRay::Encoders[:html]::CSS.new(:default).stylesheet
+  end
+
+  # Internal: Generate the default stylesheet for Asciidoctor
+  #
+  # returns the default Asciidoctor stylesheet as a String
+  def self.default_asciidoctor_stylesheet
+    <<'DEFAULT_ASCIIDOCTOR_STYLESHEET'
+/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */
+article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; }
+audio, canvas, video { display: inline-block; }
+audio:not([controls]) { display: none; height: 0; }
+[hidden] { display: none; }
+html { font-family: sans-serif; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+body { margin: 0; }
+a:focus { outline: thin dotted; }
+a:active, a:hover { outline: 0; }
+h1 { font-size: 2em; margin: 0.67em 0; }
+abbr[title] { border-bottom: 1px dotted; }
+b, strong { font-weight: bold; }
+dfn { font-style: italic; }
+hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; }
+mark { background: #ff0; color: #000; }
+code, tt, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; }
+pre { white-space: pre-wrap; }
+q { quotes: "\201C" "\201D" "\2018" "\2019"; }
+small { font-size: 80%; }
+sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
+sup { top: -0.5em; }
+sub { bottom: -0.25em; }
+img { border: 0; }
+svg:not(:root) { overflow: hidden; }
+figure { margin: 0; }
+fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; }
+legend { border: 0; padding: 0; }
+button, input, select, textarea { font-family: inherit; font-size: 100%; margin: 0; }
+button, input { line-height: normal; }
+button, select { text-transform: none; }
+button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; cursor: pointer; }
+button[disabled], html input[disabled] { cursor: default; }
+input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; }
+input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; }
+input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
+button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
+textarea { overflow: auto; vertical-align: top; }
+table { border-collapse: collapse; border-spacing: 0; }
+*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }
+html, body { font-size: 100%; }
+body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; }
+a:focus { outline: none; }
+img, object, embed { max-width: 100%; height: auto; }
+object, embed { height: 100%; }
+img { -ms-interpolation-mode: bicubic; }
+#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; }
+.left { float: left !important; }
+.right { float: right !important; }
+.text-left { text-align: left !important; }
+.text-right { text-align: right !important; }
+.text-center { text-align: center !important; }
+.text-justify { text-align: justify !important; }
+.hide { display: none; }
+.antialiased, body { -webkit-font-smoothing: antialiased; }
+img { display: inline-block; }
+textarea { height: auto; min-height: 50px; }
+select { width: 100%; }
+p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; }
+.subheader, .admonitionblock td.content > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, .sidebarblock > .title, .tableblock > .title, .verseblock > .title, .ulist > .title, .olist > .title, .dlist > .title, .qlist > .title, .tableblock > caption { line-height: 1.4; color: #7a2518; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; }
+div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; }
+a { color: #005498; text-decoration: underline; line-height: inherit; }
+a:hover, a:focus { color: #00467f; }
+a img { border: none; }
+p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; }
+p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; }
+h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: Georgia, "URW Bookman L", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; color: #ba3925; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; }
+h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #e99b8f; line-height: 0; }
+h1 { font-size: 2.125em; }
+h2 { font-size: 1.6875em; }
+h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; }
+h4 { font-size: 1.125em; }
+h5 { font-size: 1.125em; }
+h6 { font-size: 1em; }
+hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; }
+em, i { font-style: italic; line-height: inherit; }
+strong, b { font-weight: bold; line-height: inherit; }
+small { font-size: 60%; line-height: inherit; }
+code, tt { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: normal; color: #6d180b; }
+ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; }
+ul li ul, ul li ol { margin-left: 1.5em; margin-bottom: 0; font-size: 1em; }
+ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; }
+ul.square { list-style-type: square; }
+ul.circle { list-style-type: circle; }
+ul.disc { list-style-type: disc; }
+ul.no-bullet { list-style: none; }
+ol li ul, ol li ol { margin-left: 1.5em; margin-bottom: 0; }
+dl dt { margin-bottom: 0.3125em; font-weight: bold; }
+dl dd { margin-bottom: 1.25em; }
+abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; }
+abbr { text-transform: none; }
+blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; }
+blockquote cite { display: block; font-size: inherit; color: #555555; }
+blockquote cite:before { content: "\2014 \0020"; }
+blockquote cite a, blockquote cite a:visited { color: #555555; }
+blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; }
+.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; }
+.vcard li { margin: 0; display: block; }
+.vcard .fn { font-weight: bold; font-size: 0.9375em; }
+.vevent .summary { font-weight: bold; }
+.vevent abbr { cursor: default; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; }
+ at media only screen and (min-width: 48em) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; }
+  h1 { font-size: 2.75em; }
+  h2 { font-size: 2.3125em; }
+  h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; }
+  h4 { font-size: 1.4375em; } }
+.print-only { display: none !important; }
+ at media print { * { background: transparent !important; color: #000 !important; box-shadow: none !important; text-shadow: none !important; }
+  a, a:visited { text-decoration: underline; }
+  a[href]:after { content: " (" attr(href) ")"; }
+  abbr[title]:after { content: " (" attr(title) ")"; }
+  .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; }
+  pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
+  thead { display: table-header-group; }
+  tr, img { page-break-inside: avoid; }
+  img { max-width: 100% !important; }
+  @page { margin: 0.5cm; }
+  p, h2, h3, #toctitle, .sidebarblock > .content > .title { orphans: 3; widows: 3; }
+  h2, h3, #toctitle, .sidebarblock > .content > .title { page-break-after: avoid; }
+  .hide-on-print { display: none !important; }
+  .print-only { display: block !important; }
+  .hide-for-print { display: none !important; }
+  .show-for-print { display: inherit !important; } }
+table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; }
+table thead, table tfoot { background: whitesmoke; font-weight: bold; }
+table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; }
+table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; }
+table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; }
+table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.6; }
+pre > code, pre > tt { color: #222222; }
+tt { font-size: 0.9375em; padding: 1px 3px 0; white-space: nowrap; background-color: #f2f2f2; border: 1px solid #cccccc; -webkit-border-radius: 4px; border-radius: 4px; text-shadow: none; }
+kbd.keyseq { color: #555555; }
+kbd:not(.keyseq) { display: inline-block; color: #222222; font-size: 0.75em; line-height: 1.4; background-color: #F7F7F7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px white inset; margin: -0.15em 0.15em 0 0.15em; padding: 0.2em 0.6em 0.2em 0.5em; vertical-align: middle; white-space: nowrap; }
+kbd kbd:first-child { margin-left: 0; }
+kbd kbd:last-child { margin-right: 0; }
+.menuseq, .menu { color: #090909; }
+p a > tt { text-decoration: underline; }
+p a > tt:hover { color: #561309; }
+#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; }
+#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; }
+#header:after, #content:after, #footnotes:after, #footer:after { clear: both; }
+#header { margin-bottom: 2.5em; }
+#header > h1 { color: black; font-weight: normal; border-bottom: 1px solid #dddddd; margin-bottom: -28px; padding-bottom: 32px; }
+#header span { color: #6f6f6f; }
+#header #revnumber { text-transform: capitalize; }
+#header br { display: none; }
+#header br + span { padding-left: 3px; }
+#header br + span:before { content: "\2013 \0020"; }
+#toc { border-bottom: 3px double #ebebeb; padding-bottom: 1.25em; }
+#toc > ol { margin-left: 0.25em; }
+#toc ol.sectlevel0 > li > a { font-style: italic; }
+#toc ol.sectlevel0 ol.sectlevel1 { margin-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; }
+#toc ol { list-style-type: none; }
+#toctitle { color: #7a2518; }
+ at media only screen and (min-width: 80em) { body.toc2 { padding-left: 20em; }
+  #toc.toc2 { position: fixed; width: 20em; left: 0; top: 0; border-right: 1px solid #ebebeb; border-bottom: 0; z-index: 1000; padding: 1em; height: 100%; overflow: auto; }
+  #toc.toc2 #toctitle { margin-top: 0; }
+  #toc.toc2 > ol { font-size: .95em; }
+  #toc.toc2 ol ol { margin-left: 0; padding-left: 1em; }
+  #toc.toc2 ol.sectlevel0 ol.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } }
+#footer { max-width: 100%; background-color: #222222; padding: 1.25em; }
+#footer-text { color: #dddddd; line-height: 1.44; }
+.sect1 { border-bottom: 3px double #ebebeb; padding-bottom: 1.25em; }
+.sect1:last-of-type { border-bottom: 0; }
+#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; width: 1em; margin-left: -1em; display: block; text-decoration: none; visibility: hidden; text-align: center; font-weight: normal; }
+#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: '\00A7'; font-size: .85em; vertical-align: text-top; display: block; margin-top: 0.05em; }
+#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; }
+#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #ba3925; text-decoration: none; }
+#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #a53221; }
+.admonitionblock td.content > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, .sidebarblock > .title, .tableblock > .title, .verseblock > .title, .ulist > .title, .olist > .title, .dlist > .title, .qlist > .title { text-align: left; font-weight: bold; }
+.tableblock > caption { text-align: left; font-weight: bold; white-space: nowrap; overflow: visible; max-width: 0; }
+table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; }
+.admonitionblock > table { border: 0; background: none; width: 100%; }
+.admonitionblock > table td.icon { text-align: center; width: 80px; }
+.admonitionblock > table td.icon img { max-width: none; }
+.admonitionblock > table td.icon .title { font-weight: bold; text-transform: uppercase; }
+.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #6f6f6f; }
+.admonitionblock > table td.content > .paragraph:last-child > p { margin-bottom: 0; }
+.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 4px; border-radius: 4px; }
+.exampleblock > .content h1, .exampleblock > .content h2, .exampleblock > .content h3, .exampleblock > .content #toctitle, .sidebarblock.exampleblock > .content > .title, .exampleblock > .content h4, .exampleblock > .content h5, .exampleblock > .content h6, .exampleblock > .content p { color: #333333; }
+.exampleblock > .content > :first-child { margin-top: 0; }
+.exampleblock > .content > :last-child { margin-bottom: 0; }
+.exampleblock > .content h1, .exampleblock > .content h2, .exampleblock > .content h3, .exampleblock > .content #toctitle, .sidebarblock.exampleblock > .content > .title, .exampleblock > .content h4, .exampleblock > .content h5, .exampleblock > .content h6 { line-height: 1; margin-bottom: 0.625em; }
+.exampleblock > .content h1.subheader, .exampleblock > .content h2.subheader, .exampleblock > .content h3.subheader, .exampleblock > .content .subheader#toctitle, .sidebarblock.exampleblock > .content > .subheader.title, .exampleblock > .content h4.subheader, .exampleblock > .content h5.subheader, .exampleblock > .content h6.subheader { line-height: 1.4; }
+.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; }
+.exampleblock.result > .content { -webkit-box-shadow: 0 1px 8px #d9d9d9; box-shadow: 0 1px 8px #d9d9d9; }
+.imageblock { margin-bottom: 1.25em; }
+.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 4px; border-radius: 4px; }
+.sidebarblock h1, .sidebarblock h2, .sidebarblock h3, .sidebarblock #toctitle, .sidebarblock > .content > .title, .sidebarblock h4, .sidebarblock h5, .sidebarblock h6, .sidebarblock p { color: #333333; }
+.sidebarblock > :first-child { margin-top: 0; }
+.sidebarblock > :last-child { margin-bottom: 0; }
+.sidebarblock h1, .sidebarblock h2, .sidebarblock h3, .sidebarblock #toctitle, .sidebarblock > .content > .title, .sidebarblock h4, .sidebarblock h5, .sidebarblock h6 { line-height: 1; margin-bottom: 0.625em; }
+.sidebarblock h1.subheader, .sidebarblock h2.subheader, .sidebarblock h3.subheader, .sidebarblock .subheader#toctitle, .sidebarblock > .content > .subheader.title, .sidebarblock h4.subheader, .sidebarblock h5.subheader, .sidebarblock h6.subheader { line-height: 1.4; }
+.sidebarblock > .content > .title { color: #7a2518; margin-top: 0; line-height: 1.6; }
+.sidebarblock > .content > .paragraph:last-child p { margin-bottom: 0; }
+pre { color: inherit; font-family: Consolas, "Liberation Mono", Courier, monospace; overflow-x: auto; line-height: 1.6; }
+.verseblock { margin-bottom: 1.25em; }
+.literalblock, .listingblock { margin-bottom: 1.25em; }
+.literalblock > .content > pre, .listingblock > .content > pre { background: none; color: inherit; font-family: Consolas, "Liberation Mono", Courier, monospace; border-width: 1px 0; border-style: dotted; border-color: #bfbfbf; -webkit-border-radius: 4px; border-radius: 4px; padding: 0.75em 0.75em 0.5em 0.75em; white-space: pre; overflow-x: auto; line-height: 1.6; }
+.literalblock > .content > pre > code, .literalblock > .content > pre > tt, .listingblock > .content > pre > code, .listingblock > .content > pre > tt { color: inherit; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0; background: none; font-weight: normal; }
+ at media only screen { .literalblock > .content > pre, .listingblock > .content > pre { font-size: 0.8em; } }
+ at media only screen and (min-width: 48em) { .literalblock > .content > pre, .listingblock > .content > pre { font-size: 0.9em; } }
+ at media only screen and (min-width: 80em) { .literalblock > .content > pre, .listingblock > .content > pre { font-size: 1em; } }
+.listingblock:hover .xml:before { content: "xml"; text-transform: uppercase; float: right; font-size: 0.9em; color: #999; }
+.listingblock:hover .html:before { content: "html"; text-transform: uppercase; float: right; font-size: 0.9em; color: #999; }
+.listingblock:hover .ruby:before { content: "ruby"; text-transform: uppercase; float: right; font-size: 0.9em; color: #999; }
+.listingblock:hover .asciidoc:before { content: "asciidoc"; text-transform: uppercase; float: right; font-size: 0.9em; color: #999; }
+.listingblock:hover .java:before { content: "java"; text-transform: uppercase; float: right; font-size: 0.9em; color: #999; }
+.listingblock:hover .javascript:before { content: "javascript"; text-transform: uppercase; float: right; font-size: 0.9em; color: #999; }
+.listingblock:hover .css:before { content: "css"; text-transform: uppercase; float: right; font-size: 0.9em; color: #999; }
+.listingblock:hover .scss:before { content: "scss"; text-transform: uppercase; float: right; font-size: 0.9em; color: #999; }
+.quoteblock { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; }
+.quoteblock blockquote { margin: 0 0 1.25em 0; padding: 0 0 0.5625em 0; border: 0; }
+.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; }
+.quoteblock .attribution { margin-top: -.25em; padding-bottom: 0.5625em; font-size: inherit; color: #555555; }
+.quoteblock .attribution br { display: none; }
+.quoteblock .attribution cite { display: block; margin-bottom: 0.625em; }
+table thead th, table tfoot th { font-weight: bold; }
+table.tableblock.grid-all { border-collapse: separate; border-spacing: 1px; -webkit-border-radius: 4px; border-radius: 4px; border-top: 1px solid #dddddd; border-bottom: 1px solid #dddddd; }
+table.tableblock.frame-topbot, table.tableblock.frame-none { border-left: 0; border-right: 0; }
+table.tableblock.frame-sides, table.tableblock.frame-none { border-top: 0; border-bottom: 0; }
+table.tableblock td .paragraph:last-child p, table.tableblock td > p:last-child { margin-bottom: 0; }
+th.tableblock.halign-left, td.tableblock.halign-left { text-align: left; }
+th.tableblock.halign-right, td.tableblock.halign-right { text-align: right; }
+th.tableblock.halign-center, td.tableblock.halign-center { text-align: center; }
+th.tableblock.halign-top, td.tableblock.halign-top { vertical-align: top; }
+th.tableblock.halign-bottom, td.tableblock.halign-bottom { vertical-align: bottom; }
+th.tableblock.halign-middle, td.tableblock.halign-middle { vertical-align: middle; }
+p.tableblock.header { color: #222222; font-weight: bold; }
+td > div.verse { white-space: pre; }
+ul { margin-left: 1.75em; }
+ol { margin-left: 1.875em; }
+dl dd { margin-left: 1.125em; }
+dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; }
+.unstyled dl dt { font-weight: normal; font-style: normal; }
+ol > li p, ul > li p, ul dd, ol dd { margin-bottom: 0.625em; }
+ol.arabic { list-style-type: decimal; }
+ol.loweralpha { list-style-type: lower-alpha; }
+ol.upperalpha { list-style-type: upper-alpha; }
+ol.lowerroman { list-style-type: lower-roman; }
+ol.upperroman { list-style-type: upper-roman; }
+.hdlist > table, .colist > table { border: 0; background: none; }
+.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; }
+.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; }
+.colist > table tr > td:first-of-type { padding: 0 .8em; line-height: 1; }
+.colist > table tr > td:last-of-type { padding: 0.25em 0; }
+td.hdlist1 { vertical-align: top; padding-right: .8em; font-weight: bold; }
+.qanda > ol > li > p:first-child { color: #00467f; }
+span.footnote, span.footnoteref { vertical-align: super; font-size: 0.875em; }
+span.footnote a, span.footnoteref a { text-decoration: none; }
+#footnotes { padding: 0.75em 0.375em; margin-bottom: 1.25em; #border-top: 1px solid #dddddd; }
+#footnotes hr { width: 20%; min-width: 6.25em; margin: -.25em 0 .75em 0; border-width: 1px 0 0 0; }
+#footnotes .footnote { line-height: 1.3; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.2em; margin-bottom: .2em; }
+#footnotes .footnote a { font-weight: bold; text-decoration: none; }
+#footnotes .footnote:last-of-type { margin-bottom: 0; }
+.gist .file-data > table { border: none; background: #fff; width: 100%; margin-bottom: 0; }
+.gist .file-data > table td.line-data { width: 99%; }
+div.unbreakable { page-break-inside: avoid; }
+.big { font-size: larger; }
+.small { font-size: smaller; }
+.underline { text-decoration: underline; }
+.overline { text-decoration: overline; }
+.line-through { text-decoration: line-through; }
+.aqua { color: #00bfbf; }
+.aqua-background { background-color: #00fafa; }
+.black { color: black; }
+.black-background { background-color: black; }
+.blue { color: #0000bf; }
+.blue-background { background-color: #0000fa; }
+.fuchsia { color: #bf00bf; }
+.fuchsia-background { background-color: #fa00fa; }
+.gray { color: #606060; }
+.gray-background { background-color: #7d7d7d; }
+.green { color: #006000; }
+.green-background { background-color: #007d00; }
+.lime { color: #00bf00; }
+.lime-background { background-color: #00fa00; }
+.maroon { color: #600000; }
+.maroon-background { background-color: #7d0000; }
+.navy { color: #000060; }
+.navy-background { background-color: #00007d; }
+.olive { color: #606000; }
+.olive-background { background-color: #7d7d00; }
+.purple { color: #600060; }
+.purple-background { background-color: #7d007d; }
+.red { color: #bf0000; }
+.red-background { background-color: #fa0000; }
+.silver { color: #909090; }
+.silver-background { background-color: #bcbcbc; }
+.teal { color: #006060; }
+.teal-background { background-color: #007d7d; }
+.white { color: #bfbfbf; }
+.white-background { background-color: #fafafa; }
+.yellow { color: #bfbf00; }
+.yellow-background { background-color: #fafa00; }
+.admonitionblock td.icon [class^="icon-"]:before { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; }
+.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #005498; color: #003f72; }
+.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; }
+.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; }
+.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; }
+.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; }
+.conum { display: inline-block; color: white !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; width: 20px; height: 20px; font-size: 12px; font-weight: bold; line-height: 20px; font-family: Arial, sans-serif; font-style: normal; position: relative; top: -2px; letter-spacing: -1px; }
+.conum * { color: white !important; }
+.conum:empty { display: none; }
+pre .comment .conum { left: -20px; }
+.literalblock > .content > pre, .listingblock > .content > pre { -webkit-border-radius: 0; border-radius: 0; }
+DEFAULT_ASCIIDOCTOR_STYLESHEET
+  end
+end
+end
diff --git a/lib/asciidoctor/backends/base_template.rb b/lib/asciidoctor/backends/base_template.rb
index ae2adec..a396ad5 100644
--- a/lib/asciidoctor/backends/base_template.rb
+++ b/lib/asciidoctor/backends/base_template.rb
@@ -40,15 +40,17 @@ class BaseTemplate
   # locals - A Hash of additional variables. Not currently in use.
   def render(node = Object.new, locals = {})
     tmpl = template
-    if tmpl.equal? :content
+    case tmpl
+    when :invoke_result
+      return result(node)
+    when :content
       result = node.content
-    #elsif tmpl.is_a?(String)
-    #  result = tmpl
     else
       result = tmpl.result(node.get_binding(self))
     end
 
-    if (@view == 'document' || @view == 'embedded') && node.renderer.compact
+    if (@view == 'document' || @view == 'embedded') &&
+        node.renderer.compact && !node.document.nested?
       compact result
     else
       result
@@ -63,7 +65,7 @@ class BaseTemplate
   # returns the text with blank lines removed and HTML line feed entities
   # converted to an endline character.
   def compact(str)
-    str.gsub(BLANK_LINES_PATTERN, '').gsub(LINE_FEED_ENTITY, "\n")
+    str.gsub(BLANK_LINE_PATTERN, '').gsub(LINE_FEED_ENTITY, "\n")
   end
 
   # Public: Preserve endlines by replacing them with the HTML line feed entity.
@@ -94,10 +96,15 @@ class BaseTemplate
   end
 
   # create template matter to insert a style class if the variable has a value
-  def attrvalue(key, sibling = true)
+  def attrvalue(key, sibling = true, inherit = true)
     delimiter = sibling ? ' ' : ''
-    # example: <% if attr? 'foo' %><%= attr 'foo' %><% end %>
-    %(<% if attr? '#{key}' %>#{delimiter}<%= attr '#{key}' %><% end %>)
+    if inherit
+      # example: <% if attr? 'foo' %><%= attr 'foo' %><% end %>
+      %(<% if attr? '#{key}' %>#{delimiter}<%= attr '#{key}' %><% end %>)
+    else
+      # example: <% if attr? 'foo', nil, false %><%= attr 'foo', nil, false %><% end %>
+      %(<% if attr? '#{key}', nil, false %>#{delimiter}<%= attr '#{key}', nil, false %><% end %>)
+    end
   end
 
   # create template matter to insert an id if one is specified for the block
@@ -105,4 +112,14 @@ class BaseTemplate
     attribute('id', '@id')
   end
 end
+
+module EmptyTemplate
+  def result(node)
+    ''
+  end
+
+  def template
+    :invoke_result
+  end
+end
 end
diff --git a/lib/asciidoctor/backends/docbook45.rb b/lib/asciidoctor/backends/docbook45.rb
index 91ad0a6..a3547ac 100644
--- a/lib/asciidoctor/backends/docbook45.rb
+++ b/lib/asciidoctor/backends/docbook45.rb
@@ -1,11 +1,12 @@
 module Asciidoctor
   class BaseTemplate
-    def tag(name, key)
+    def tag(name, key, dynamic = false)
       type = key.is_a?(Symbol) ? :attr : :var
       key = key.to_s
       if type == :attr
+        key_str = dynamic ? %("#{key}") : "'#{key}'"
         # example: <% if attr? 'foo' %><bar><%= attr 'foo' %></bar><% end %>
-        %(<% if attr? '#{key}' %><#{name}><%= attr '#{key}' %></#{name}><% end %>)
+        %(<% if attr? #{key_str} %><#{name}><%= attr #{key_str} %></#{name}><% end %>)
       else
         # example: <% unless foo.to_s.empty? %><bar><%= foo %></bar><% end %>
         %(<% unless #{key}.to_s.empty? %><#{name}><%= #{key} %></#{name}><% end %>)
@@ -14,9 +15,9 @@ module Asciidoctor
 
     def title_tag(optional = true)
       if optional
-        %q{<%= title? ? "<title>#{title}</title>" : '' %>}
+        %(<%= title? ? "\n<title>\#{title}</title>" : nil %>)
       else
-        %q{<title><%= title %></title>}
+        %(\n<title><%= title %></title>)
       end
     end
 
@@ -25,16 +26,34 @@ module Asciidoctor
     end
 
     def common_attrs_erb
-      %q{<%= template.common_attrs(@id, (attr 'role'), (attr 'reftext')) %>}
+      %q(<%= template.common_attrs(@id, (attr 'role'), (attr 'reftext')) %>)
+    end
+
+    def content(node)
+      node.blocks? ? node.content.chomp : "<simpara>#{node.content.chomp}</simpara>"
+    end
+
+    def content_erb
+      %q(<%= blocks? ? content.chomp : "<simpara>#{content.chomp}</simpara>" %>)
     end
   end
 
 module DocBook45
 class DocumentTemplate < BaseTemplate
+  def title_tags(str)
+    if str.include?(': ')
+      title, _, subtitle = str.rpartition(': ')
+      %(<title>#{title}</title>
+    <subtitle>#{subtitle}</subtitle>)
+    else
+      %(<title>#{str}</title>)
+    end
+  end
+
   def docinfo
     <<-EOF
     <% if has_header? && !notitle %>
-    #{tag 'title', '@header.title'}
+    <%= template.title_tags(@header.title) %>
     <% end %>
     <% if attr? :revdate %>
     <date><%= attr :revdate %></date>
@@ -43,6 +62,7 @@ class DocumentTemplate < BaseTemplate
     <% end %>
     <% if has_header? %>
     <% if attr? :author %>
+    <% if (attr :authorcount).to_i < 2 %>
     <author>
       #{tag 'firstname', :firstname}
       #{tag 'othername', :middlename}
@@ -50,15 +70,31 @@ class DocumentTemplate < BaseTemplate
       #{tag 'email', :email}
     </author>
     #{tag 'authorinitials', :authorinitials}
+    <% else %>
+    <authorgroup>
+    <% (1..((attr :authorcount).to_i)).each do |idx| %>
+      <author> 
+        #{tag 'firstname', :"firstname_\#{idx}", true}
+        #{tag 'othername', :"middlename_\#{idx}", true}
+        #{tag 'surname', :"lastname_\#{idx}", true}
+        #{tag 'email', :"email_\#{idx}", true}
+      </author> 
+    <% end %>
+    </authorgroup>
+    <% end %>
     <% end %>
     <% if (attr? :revnumber) || (attr? :revremark) %>
     <revhistory>
-      #{tag 'revision', :revnumber}
-      #{tag 'date', :revdate}
-      #{tag 'authorinitials', :authorinitials}
-      #{tag 'revremark', :revremark}
+      <revision>
+        #{tag 'revnumber', :revnumber}
+        #{tag 'date', :revdate}
+        #{tag 'authorinitials', :authorinitials}
+        #{tag 'revremark', :revremark}
+      </revision>
     </revhistory>
     <% end %>
+<%= docinfo %>
+    #{tag 'orgname', :orgname}
     <% end %>
     EOF
   end
@@ -74,14 +110,14 @@ class DocumentTemplate < BaseTemplate
   <bookinfo>
 #{docinfo}
   </bookinfo>
-<%= content %>
+<%= content.chomp %>
 </book>
 <% else %>
 <article<% unless attr? :nolang %> lang="<%= attr :lang, 'en' %>"<% end %>>
   <articleinfo>
 #{docinfo}
   </articleinfo>
-<%= content %>
+<%= content.chomp %>
 </article>
 <% end %>
     EOF
@@ -94,39 +130,45 @@ class EmbeddedTemplate < BaseTemplate
   end
 end
 
+class BlockTocTemplate < BaseTemplate
+  def result(node)
+    ''
+  end
+
+  def template
+    :invoke_result
+  end
+end
+
 class BlockPreambleTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><% if @document.doctype == 'book' %>
-<preface#{common_attrs_erb}>
-  <title><%= title %></title>
-<%= content %>
-</preface>
-<% else %>
-<%= content %>
-<% end %>
+<%#encoding:UTF-8%><%
+if @document.doctype == 'book' %><preface#{common_attrs_erb}>#{title_tag false}
+<%= content.chomp %>
+</preface><%
+else %>
+<%= content.chomp %><%
+end %>
     EOF
   end
 end
 
 class SectionTemplate < BaseTemplate
-  def section(sec)
+  def result(sec)
     if sec.special
       tag = sec.level <= 1 ? sec.sectname : 'section'
     else
-      tag = sec.document.doctype == 'book' && sec.level <= 1 ? 'chapter' : 'section'
+      tag = sec.document.doctype == 'book' && sec.level <= 1 ? (sec.level == 0 ? 'part' : 'chapter') : 'section'
     end
     %(<#{tag}#{common_attrs(sec.id, (sec.attr 'role'), (sec.attr 'reftext'))}>
-  #{sec.title? ? "<title>#{sec.title}</title>" : nil}
-  #{sec.content}
-</#{tag}>)
+#{sec.title? ? "<title>#{sec.title}</title>" : nil}
+#{sec.content.chomp}
+</#{tag}>\n)
   end
 
   def template
-    # hot piece of code, optimized for speed
-    @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><%= template.section(self) %>
-    EOF
+    :invoke_result
   end
 end
 
@@ -138,38 +180,32 @@ class BlockFloatingTitleTemplate < BaseTemplate
   end
 end
 
-
 class BlockParagraphTemplate < BaseTemplate
-
-  def paragraph(id, role, reftext, title, content)
-    if title
+  def paragraph(id, style, role, reftext, title, content)
+    if !title.nil?
       %(<formalpara#{common_attrs(id, role, reftext)}>
-  <title>#{title}</title>
-  <para>#{content}</para>
-</formalpara>)
+<title>#{title}</title>
+<para>#{content}</para>
+</formalpara>\n)
     else
-      %(<simpara#{common_attrs(id, role, reftext)}>#{content}</simpara>)
+      %(<simpara#{common_attrs(id, role, reftext)}>#{content}</simpara>\n)
     end
   end
 
+  def result(node)
+    paragraph(node.id, (node.attr 'style', nil, false), (node.attr 'role'), (node.attr 'reftext'), (node.title? ? node.title : nil), node.content)
+  end
+
   def template
-    # very hot piece of code, optimized for speed
-    @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><%= template.paragraph(@id, (attr 'role'), (attr 'reftext'), title? ? title : nil, content) %>
-    EOF
+    :invoke_result
   end
 end
 
 class BlockAdmonitionTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><<%= attr :name %>#{common_attrs_erb}>
-  #{title_tag}
-  <% if blocks? %>
-<%= content %>
-  <% else %>
-  <simpara><%= content.chomp %></simpara>
-  <% end %>
+<%#encoding:UTF-8%><<%= attr :name %>#{common_attrs_erb}>#{title_tag}
+#{content_erb}
 </<%= attr :name %>>
     EOF
   end
@@ -179,8 +215,7 @@ class BlockUlistTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
 <%#encoding:UTF-8%><% if attr? :style, 'bibliography' %>
-<bibliodiv#{common_attrs_erb}>
-  #{title_tag}
+<bibliodiv#{common_attrs_erb}>#{title_tag}
   <% content.each do |li| %>
     <bibliomixed>
       <bibliomisc><%= li.text %></bibliomisc>
@@ -191,8 +226,7 @@ class BlockUlistTemplate < BaseTemplate
   <% end %>
 </bibliodiv>
 <% else %>
-<itemizedlist#{common_attrs_erb}>
-  #{title_tag}
+<itemizedlist#{common_attrs_erb}>#{title_tag}
   <% content.each do |li| %>
     <listitem>
       <simpara><%= li.text %></simpara>
@@ -210,8 +244,7 @@ end
 class BlockOlistTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><orderedlist#{common_attrs_erb}#{attribute('numeration', :style)}>
-  #{title_tag}
+<%#encoding:UTF-8%><orderedlist#{common_attrs_erb}#{attribute('numeration', :style)}>#{title_tag}
   <% content.each do |li| %>
     <listitem>
       <simpara><%= li.text %></simpara>
@@ -228,8 +261,7 @@ end
 class BlockColistTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><calloutlist#{common_attrs_erb}>
-  #{title_tag}
+<%#encoding:UTF-8%><calloutlist#{common_attrs_erb}>#{title_tag}
   <% content.each do |li| %>
   <callout arearefs="<%= li.attr :coids %>">
     <para><%= li.text %></para>
@@ -254,7 +286,8 @@ class BlockDlistTemplate < BaseTemplate
     'qanda' => {
       :list => 'qandaset',
       :entry => 'qandaentry',
-      :term => 'question',
+      :label => 'question',
+      :term => 'simpara',
       :item => 'answer'
     },
     'glossary' => {
@@ -266,59 +299,154 @@ class BlockDlistTemplate < BaseTemplate
   }
 
   def template
+    # TODO may want to refactor ListItem content to hold multiple terms
+    # that change would drastically simplify this template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><% tags = (template.class::LIST_TAGS[attr :style] || template.class::LIST_TAGS['labeled']) %>
-<% if tags[:list] %><<%= tags[:list] %>#{common_attrs_erb}><% end %>
-  #{title_tag}
-  <% content.each do |dt, dd| %>
-  <<%= tags[:entry] %>>
-    <<%= tags[:term] %>>
-      <%= dt.text %>
-    </<%= tags[:term] %>>
-    <% unless dd.nil? %>
-    <<%= tags[:item] %>>
-      <% if dd.text? %>
-      <simpara><%= dd.text %></simpara>
-      <% end %>
-      <% if dd.blocks? %>
-<%= dd.content %>
-      <% end %>
-    </<%= tags[:item] %>>
-    <% end %>
-  </<%= tags[:entry] %>>
-  <% end %>
-<% if tags[:list] %></<%= tags[:list] %>><% end %>
+<%#encoding:UTF-8%><%
+continuing = false;
+entries = content
+last_index = entries.length - 1
+if attr? :style, 'horizontal'
+%><<%= (tag = title? ? 'table' : 'informaltable') %>#{common_attrs_erb} tabstyle="horizontal" frame="none" colsep="0" rowsep="0">#{title_tag}
+<tgroup cols="2">
+<colspec colwidth="<%= attr :labelwidth, 15 %>*"/>
+<colspec colwidth="<%= attr :labelwidth, 85 %>*"/>
+<tbody valign="top"><%
+  entries.each_with_index do |(dt, dd), index|
+    last = (index == last_index)
+    unless continuing %>
+<row>
+<entry><%
+    end %>
+<simpara><%= dt.text %></simpara><%
+    if !last && dd.nil?
+      continuing = true
+      next
+    else
+      continuing = false
+    end %>
+</entry>
+<entry><%
+    unless dd.nil?
+      if dd.text? %>
+<simpara><%= dd.text %></simpara><%
+      end
+      if dd.blocks? %>
+<%= dd.content.chomp %><%
+      end
+    end %>
+</entry><%
+    if last || !dd.nil? %>
+</row><%
+    end %><%
+  end %>
+</tbody>
+</tgroup>
+</<%= tag %>><%
+else
+  tags = (template.class::LIST_TAGS[attr :style] || template.class::LIST_TAGS['labeled'])
+  if tags[:list]
+%><<%= tags[:list] %>#{common_attrs_erb}>#{title_tag}<%
+  end
+  entries.each_with_index do |(dt, dd), index|
+    last = (index == last_index)
+    unless continuing %>
+<<%= tags[:entry] %>><%
+    end
+    if tags.has_key?(:label)
+      unless continuing %>
+<<%= tags[:label] %>><%
+      end %>
+<<%= tags[:term] %>><%= dt.text %></<%= tags[:term] %>><%
+      if last || !dd.nil? %>
+</<%= tags[:label] %>><%
+      end
+    else %>
+<<%= tags[:term] %>><%= dt.text %></<%= tags[:term] %>><%
+    end
+    if !last && dd.nil?
+      continuing = true
+      next
+    else
+      continuing = false
+    end %>
+<<%= tags[:item] %>><%
+    unless dd.nil?
+      if dd.text? %>
+<simpara><%= dd.text %></simpara><%
+      end
+      if dd.blocks? %>
+<%= dd.content %><%
+      end
+    end %>
+</<%= tags[:item] %>>
+</<%= tags[:entry] %>><%
+  end
+  if tags[:list] %>
+</<%= tags[:list] %>><%
+  end
+end %>
     EOF
   end
 end
 
 class BlockOpenTemplate < BaseTemplate
+  def result(node)
+    open_block(node, node.id, (node.attr 'style', nil, false),
+        (node.attr 'role'), (node.attr 'reftext'), node.title? ? node.title : nil)
+  end
+
+  def open_block(node, id, style, role, reftext, title)
+    case style
+    when 'abstract'
+      if node.parent == node.document && node.document.attr?('doctype', 'book')
+        puts 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
+        ''
+      else
+        %(<abstract>#{title && "\n<title>#{title}</title>"}
+#{content node}
+</abstract>\n)
+      end
+    when 'partintro'
+      unless node.document.attr?('doctype', 'book') && node.parent.is_a?(Asciidoctor::Section) && node.level == 0
+        puts 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a part section. Excluding block content.'
+        ''
+      else
+        %(<partintro#{common_attrs id, role, reftext}>#{title && "\n<title>#{title}</title>"}
+#{content node}
+</partintro>\n)
+      end
+    else
+      node.content
+    end
+  end
+
   def template
-    :content
+    :invoke_result
   end
 end
 
 class BlockListingTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><% if !title? %>
-<% if attr? :style, 'source' %>
-<programlisting#{common_attrs_erb}#{attribute('language', :language)} linenumbering="<%= (attr? :linenums) ? 'numbered' : 'unnumbered' %>"><%= template.preserve_endlines(content, self) %></programlisting>
-<% else %>
-<screen#{common_attrs_erb}><%= template.preserve_endlines(content, self) %></screen>
-<% end %>
-<% else %>
-<formalpara#{common_attrs_erb}>
-  #{title_tag false}
-  <para>
-    <% if attr :style, 'source' %>
-    <programlisting language="<%= attr :language %>" linenumbering="<%= (attr? :linenums) ? 'numbered' : 'unnumbered' %>"><%= template.preserve_endlines(content, self) %></programlisting>
-    <% else %>
-    <screen><%= template.preserve_endlines(content, self) %></screen>
-    <% end %>
-  </para>
-</formalpara>
-<% end %>
+<%#encoding:UTF-8%><%
+if !title?
+  if (attr? 'style', 'source') && (attr? 'language')
+%><programlisting#{common_attrs_erb}#{attribute('language', :language)} linenumbering="<%= (attr? :linenums) ? 'numbered' : 'unnumbered' %>"><%= template.preserve_endlines(content, self) %></programlisting><%
+  else
+%><screen#{common_attrs_erb}><%= template.preserve_endlines(content, self) %></screen><%
+  end
+else
+%><formalpara#{common_attrs_erb}>#{title_tag false}
+<para><%
+  if (attr? 'style', 'source') && (attr? 'language') %>
+<programlisting language="<%= attr 'language' %>" linenumbering="<%= (attr? :linenums) ? 'numbered' : 'unnumbered' %>"><%= template.preserve_endlines(content, self) %></programlisting><%
+  else %>
+<screen><%= template.preserve_endlines(content, self) %></screen><%
+  end %>
+</para>
+</formalpara><%
+end %>
     EOF
   end
 end
@@ -329,8 +457,7 @@ class BlockLiteralTemplate < BaseTemplate
 <%#encoding:UTF-8%><% if !title? %>
 <literallayout#{common_attrs_erb} class="monospaced"><%= template.preserve_endlines(content, self) %></literallayout>
 <% else %>
-<formalpara#{common_attrs_erb}>
-  #{title_tag false}
+<formalpara#{common_attrs_erb}>#{title_tag false}
   <para>
     <literallayout class="monospaced"><%= template.preserve_endlines(content, self) %></literallayout>
   </para>
@@ -343,10 +470,9 @@ end
 class BlockExampleTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><example#{common_attrs_erb}>
-  #{title_tag}
-<%= content %>
-</example>
+<%#encoding:UTF-8%><<%= (tag_name = title? ? 'example' : 'informalexample') %>#{common_attrs_erb}>#{title_tag}
+#{content_erb}
+</<%= tag_name %>>
     EOF
   end
 end
@@ -354,9 +480,8 @@ end
 class BlockSidebarTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><sidebar#{common_attrs_erb}>
-  #{title_tag}
-<%= content %>
+<%#encoding:UTF-8%><sidebar#{common_attrs_erb}>#{title_tag}
+#{content_erb}
 </sidebar>
     EOF
   end
@@ -365,21 +490,16 @@ end
 class BlockQuoteTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><blockquote#{common_attrs_erb}>
-  #{title_tag}
+<%#encoding:UTF-8%><blockquote#{common_attrs_erb}>#{title_tag}
   <% if (attr? :attribution) || (attr? :citetitle) %>
   <attribution>
     <% if attr? :attribution %>
-    <%= attr(:attribution) %>
+    <%= (attr :attribution) %>
     <% end %>
     #{tag 'citetitle', :citetitle}
   </attribution>
   <% end %>
-<% if !@buffer.nil? %>
-<simpara><%= content %></simpara>
-<% else %>
-<%= content %>
-<% end %>
+#{content_erb}
 </blockquote>
     EOF
   end
@@ -388,12 +508,11 @@ end
 class BlockVerseTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><blockquote#{common_attrs_erb}>
-  #{title_tag}
+<%#encoding:UTF-8%><blockquote#{common_attrs_erb}>#{title_tag}
   <% if (attr? :attribution) || (attr? :citetitle) %>
   <attribution>
     <% if attr? :attribution %>
-    <%= attr(:attribution) %>
+    <%= (attr :attribution) %>
     <% end %>
     #{tag 'citetitle', :citetitle}
   </attribution>
@@ -413,9 +532,8 @@ end
 class BlockTableTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><<%= title? ? 'table' : 'informaltable'%>#{common_attrs_erb} frame="<%= attr :frame, 'all'%>"
-    rowsep="<%= ['none', 'cols'].include?(attr :grid) ? 0 : 1 %>" colsep="<%= ['none', 'rows'].include?(attr :grid) ? 0 : 1 %>">
-  #{title_tag}
+<%#encoding:UTF-8%><<%= (tag_name = title? ? 'table' : 'informaltable') %>#{common_attrs_erb} frame="<%= attr :frame, 'all'%>"
+    rowsep="<%= ['none', 'cols'].include?(attr :grid) ? 0 : 1 %>" colsep="<%= ['none', 'rows'].include?(attr :grid) ? 0 : 1 %>">#{title_tag}
   <% if attr? :width %>
   <?dbhtml table-width="<%= attr :width %>"?>
   <?dbfo table-width="<%= attr :width %>"?>
@@ -423,7 +541,7 @@ class BlockTableTemplate < BaseTemplate
   <% end %>
   <tgroup cols="<%= attr :colcount %>">
     <% @columns.each do |col| %>
-    <colspec colname="col_<%= col.attr :colnumber %>" colwidth="<%= col.attr((attr? :width) ? :colabswidth : :colpcwidth) %>*"/>
+    <colspec colname="col_<%= col.attr :colnumber %>" colwidth="<%= (col.attr (attr? :width) ? :colabswidth : :colpcwidth) %>*"/>
     <% end %>
     <% [:head, :foot, :body].select {|tblsec| !rows[tblsec].empty? }.each do |tblsec| %>
     <t<%= tblsec %>>
@@ -433,22 +551,23 @@ class BlockTableTemplate < BaseTemplate
         <entry#{attribute('align', 'cell.attr :halign')}#{attribute('valign', 'cell.attr :valign')}<%
         if cell.colspan %> namest="col_<%= cell.column.attr :colnumber %>" nameend="col_<%= (cell.column.attr :colnumber) + cell.colspan - 1 %>"<%
         end %><% if cell.rowspan %> morerows="<%= cell.rowspan - 1 %>"<% end %>><%
-        if tblsec == :head %><%= cell.text %><%
+        cell_content = ''
+        if tblsec == :head %><% cell_content = cell.text %><%
         else %><%
-        case cell.attr(:style)
-          when :asciidoc %><%= cell.content %><%
-          when :verse %><literallayout><%= template.preserve_endlines(cell.text, self) %></literallayout><%
-          when :literal %><literallayout class="monospaced"><%= template.preserve_endlines(cell.text, self) %></literallayout><%
-          when :header %><% cell.content.each do |text| %><simpara><emphasis role="strong"><%= text %></emphasis></simpara><% end %><%
-          else %><% cell.content.each do |text| %><simpara><%= text %></simpara><% end %><%
-        %><% end %><% end %></entry>
+        case (cell.attr :style)
+          when :asciidoc %><% cell_content = cell.content %><%
+          when :verse %><% cell_content = %(<literallayout>\#{template.preserve_endlines(cell.text, self)}</literallayout>) %><%
+          when :literal %><% cell_content = %(<literallayout class="monospaced">\#{template.preserve_endlines(cell.text, self)}</literallayout>) %><%
+          when :header %><% cell.content.each do |text| %><% cell_content = %(\#{cell_content\}<simpara><emphasis role="strong">\#{text}</emphasis></simpara>) %><% end %><%
+          else %><% cell.content.each do |text| %><% cell_content = %(\#{cell_content}<simpara>\#{text}</simpara>) %><% end %><%
+        %><% end %><% end %><%= (@document.attr? 'cellbgcolor') ? %(<?dbfo bgcolor="\#{@document.attr 'cellbgcolor'}"?>) : nil %><%= cell_content %></entry>
         <% end %>
       </row>
       <% end %>
     </t<%= tblsec %>>
     <% end %>
   </tgroup>
-</<%= title? ? 'table' : 'informaltable'%>>
+</<%= tag_name %>>
     EOS
   end
 end
@@ -456,8 +575,7 @@ end
 class BlockImageTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><%#encoding:UTF-8%><figure#{common_attrs_erb}>
-  #{title_tag}
+<%#encoding:UTF-8%><%#encoding:UTF-8%><figure#{common_attrs_erb}>#{title_tag}
   <mediaobject>
     <imageobject>
       <imagedata fileref="<%= image_uri(attr :target) %>"#{attribute('contentwidth', :width)}#{attribute('contentdepth', :height)}/>
@@ -469,6 +587,14 @@ class BlockImageTemplate < BaseTemplate
   end
 end
 
+class BlockAudioTemplate < BaseTemplate
+  include EmptyTemplate
+end
+
+class BlockVideoTemplate < BaseTemplate
+  include EmptyTemplate
+end
+
 class BlockRulerTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOF
@@ -494,6 +620,8 @@ class InlineBreakTemplate < BaseTemplate
 end
 
 class InlineQuotedTemplate < BaseTemplate
+  NO_TAGS = ['', '']
+
   QUOTED_TAGS = {
     :emphasis => ['<emphasis>', '</emphasis>'],
     :strong => ['<emphasis role="strong">', '</emphasis>'],
@@ -502,11 +630,10 @@ class InlineQuotedTemplate < BaseTemplate
     :subscript => ['<subscript>', '</subscript>'],
     :double => ['“', '”'],
     :single => ['‘', '’']
-    #:none => ['', '']
   }
 
-  def quote(text, type, role)
-    start_tag, end_tag = QUOTED_TAGS[type] || ['', '']
+  def quote_text(text, type, role)
+    start_tag, end_tag = QUOTED_TAGS[type] || NO_TAGS
     if role
       "#{start_tag}<phrase role=\"#{role}\">#{text}</phrase>#{end_tag}"
     else
@@ -514,11 +641,59 @@ class InlineQuotedTemplate < BaseTemplate
     end
   end
 
+  def result(node)
+    quote_text(node.text, node.type, (node.attr 'role'))
+  end
+
   def template
-    # very hot piece of code, optimized for speed
-    @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><%= template.quote(@text, @type, attr('role')) %>
-    EOF
+    :invoke_result
+  end
+end
+
+class InlineButtonTemplate < BaseTemplate
+  def result(node)
+    %(<guibutton>#{node.text}</guibutton>)
+  end
+
+  def template
+    :invoke_result
+  end
+end
+
+class InlineKbdTemplate < BaseTemplate
+  def result(node)
+    keys = node.attr 'keys'
+    if keys.size == 1
+      %(<keycap>#{keys.first}</keycap>)
+    else
+      key_combo = keys.map{|key| %(<keycap>#{key}</keycap>) }.join
+      %(<keycombo>#{key_combo}</keycombo>)
+    end
+  end
+
+  def template
+    :invoke_result
+  end
+end
+
+class InlineMenuTemplate < BaseTemplate
+  def menu(menu, submenus, menuitem)
+    if !submenus.empty?
+      submenu_path = submenus.map{|submenu| %(<guisubmenu>#{submenu}</guisubmenu> ) }.join.chop
+      %(<menuchoice><guimenu>#{menu}</guimenu> #{submenu_path} <guimenuitem>#{menuitem}</guimenuitem></menuchoice>)
+    elsif !menuitem.nil?
+      %(<menuchoice><guimenu>#{menu}</guimenu> <guimenuitem>#{menuitem}</guimenuitem></menuchoice>)
+    else
+      %(<guimenu>#{menu}</guimenu>)
+    end
+  end
+
+  def result(node)
+    menu(node.attr('menu'), node.attr('submenus'), node.attr('menuitem'))
+  end
+
+  def template
+    :invoke_result
   end
 end
 
@@ -536,11 +711,12 @@ class InlineAnchorTemplate < BaseTemplate
     end
   end
 
+  def result(node)
+    anchor(node.target, node.text, node.type)
+  end
+
   def template
-    # hot piece of code, optimized for speed
-    @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><%= template.anchor(@target, @text, @type) %>
-    EOS
+    :invoke_result
   end
 end
 
@@ -571,10 +747,12 @@ end %>
 end
 
 class InlineCalloutTemplate < BaseTemplate
+  def result(node)
+    %(<co id="#{node.id}"/>)
+  end
+
   def template
-    @template ||= @eruby.new <<-EOF
-<%#encoding:UTF-8%><co#{id}/>
-    EOF
+    :invoke_result
   end
 end
 
@@ -588,10 +766,10 @@ if numterms > 2 %><indexterm>
 </indexterm>
 <% end %><%
 if numterms > 1 %><indexterm>
-  <primary><%= terms[numterms - 2] %></primary><secondary><%= terms[numterms - 1] %></secondary>
+  <primary><%= terms[-2] %></primary><secondary><%= terms[-1] %></secondary>
 </indexterm>
 <% end %><indexterm>
-  <primary><%= terms[numterms - 1] %></primary>
+  <primary><%= terms[-1] %></primary>
 </indexterm><% end %>
     EOS
   end
diff --git a/lib/asciidoctor/backends/html5.rb b/lib/asciidoctor/backends/html5.rb
index 05df08f..235e220 100644
--- a/lib/asciidoctor/backends/html5.rb
+++ b/lib/asciidoctor/backends/html5.rb
@@ -1,141 +1,171 @@
+require 'asciidoctor/backends/_stylesheets'
+
 module Asciidoctor
 class BaseTemplate
 
   # create template matter to insert a style class from the role attribute if specified
   def role_class
-    attrvalue(:role)
+    attrvalue('role')
   end
 
   # create template matter to insert a style class from the style attribute if specified
   def style_class(sibling = true)
-    attrvalue(:style, sibling)
+    attrvalue('style', sibling, false)
   end
 
   def title_div(opts = {})
-    %(<% if title? %><div class="title">#{opts.has_key?(:caption) ? '<%= @caption %>' : ''}<%= title %></div><% end %>)
+    if opts.has_key? :caption
+      %q(<% if title? %><div class="title"><%= @caption %><%= title %></div><% end %>)
+    else
+      %q(<% if title? %><div class="title"><%= title %></div><% end %>)
+    end
   end
 end
 
 module HTML5
+
 class DocumentTemplate < BaseTemplate
   def self.outline(node, to_depth = 2)
     toc_level = nil
     sections = node.sections
     unless sections.empty?
-      toc_level, indent = ''
-      nested = true
-      unless node.is_a?(Document)
-        if node.document.doctype == 'book'
-          indent = '    ' * node.level unless node.level == 0
-          nested = node.level > 0
-        else
-          indent = '    ' * node.level
-        end
+      # FIXME the level for special sections should be set correctly in the model
+      # sec_level will only be 0 if we have a book doctype with parts
+      sec_level = sections.first.level
+      if sec_level == 0 && sections.first.special
+        sec_level = 1
       end
-      toc_level << "#{indent}<ol>\n" if nested
+      toc_level = %(<ol type="none" class="sectlevel#{sec_level}">\n)
+      numbered = node.document.attr? 'numbered'
       sections.each do |section|
-        toc_level << "#{indent}  <li><a href=\"##{section.id}\">#{!section.special && section.level > 0 ? "#{section.sectnum} " : ''}#{section.attr('caption')}#{section.title}</a></li>\n"
-        if section.level < to_depth && (child_toc_level = outline(section, to_depth))
-          if section.document.doctype != 'book' || section.level > 0
-            toc_level << "#{indent}  <li>\n#{child_toc_level}\n#{indent}  </li>\n"
-          else
-            toc_level << "#{indent}#{child_toc_level}\n"
+        # need to check playback attributes for change in numbered setting
+        # FIXME encapsulate me
+        if section.attributes.has_key? :attribute_entries
+          if (numbered_override = section.attributes[:attribute_entries].find {|entry| entry.name == 'numbered'})
+            numbered = numbered_override.negate ? false : true
           end
         end
+        section_num = numbered && !section.special && section.level > 0 && section.level < 4 ? %(#{section.sectnum} ) : nil
+        toc_level = %(#{toc_level}<li><a href=\"##{section.id}\">#{section_num}#{section.caption}#{section.title}</a></li>\n)
+        if section.level < to_depth && (child_toc_level = outline(section, to_depth))
+          toc_level = %(#{toc_level}<li>\n#{child_toc_level}\n</li>\n)
+        end
       end
-      toc_level << "#{indent}</ol>" if nested
+      toc_level = %(#{toc_level}</ol>)
     end
     toc_level
   end
 
-  # Internal: Generate the default stylesheet for CodeRay
-  #
-  # returns the default CodeRay stylesheet as a String
-  def self.default_coderay_stylesheet
-    Helpers.require_library 'coderay'
-    ::CodeRay::Encoders[:html]::CSS.new(:default).stylesheet
-  end
-
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><!DOCTYPE html>
-<html lang="<%= attr :lang, 'en' %>">
-  <head>
-    <meta http-equiv="Content-Type" content="text/html; charset=<%= attr :encoding %>">
-    <meta name="generator" content="Asciidoctor <%= attr 'asciidoctor-version' %>">
-    <% if attr? :description %><meta name="description" content="<%= attr :description %>"><% end %>
-    <% if attr? :keywords %><meta name="keywords" content="<%= attr :keywords %>"><% end %>
-    <title><%= doctitle %></title>
-    <% if attr? :toc %>
-    <style>
-#toc > ol { padding-left: 0; }
-#toc ol { list-style-type: none; }
-    </style>
-    <% end %>
-    <% unless attr(:stylesheet, '').empty? %>
-    <link rel="stylesheet" href="<%= (attr? :stylesdir) ? File.join((attr :stylesdir), (attr :stylesheet)) : (attr :stylesheet) %>">
-    <% end %>
-    <%
-    case attr 'source-highlighter' %><%
-    when 'coderay' %>
-    <style>
-pre.highlight { border: none; background-color: #F8F8F8; }
-pre.highlight code, pre.highlight pre { color: #333; }
-pre.highlight span.line-numbers { display: inline-block; margin-right: 4px; padding: 1px 4px; }
-pre.highlight .line-numbers { background-color: #D5F6F6; color: gray; }
-pre.highlight .line-numbers pre { color: gray; }
-<% if (attr 'coderay-css', 'class') == 'class' %><%= template.class.default_coderay_stylesheet %><% end %>
-    </style><%
-    when 'highlightjs' %>
-    <link rel="stylesheet" href="<%= (attr :highlightjsdir, 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/7.3') %>/styles/<%= (attr 'highlightjs-theme', 'default') %>.min.css">
-    <style>
-pre code { background-color: #F8F8F8; padding: 0; }
-    </style>
-    <script src="<%= (attr :highlightjsdir, 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/7.3') %>/highlight.min.js"></script>
-    <script>hljs.initHighlightingOnLoad()</script>
-    <% end %>
-  </head>
-  <body#{id} class="<%= doctype %>"<% if attr? 'max-width' %> style="max-width: <%= attr 'max-width' %>;"<% end %>>
-    <% unless noheader %>
-    <div id="header">
-      <% if has_header? %>
-      <% unless notitle %>
-      <h1><%= @header.title %></h1>
-      <% end %>
-      <% if attr? :author %><span id="author"><%= attr :author %></span><br><% end %>
-      <% if attr? :email %><span id="email" class="monospaced"><<%= attr :email %>></span><br><% end %>
-      <% if attr? :revnumber %><span id="revnumber">version <%= attr :revnumber %><%= attr?(:revdate) ? ',' : '' %></span><% end %>
-      <% if attr? :revdate %><span id="revdate"><%= attr :revdate %></span><% end %>
-      <% if attr? :revremark %><br><span id="revremark"><%= attr :revremark %></span><% end %>
-      <% end %>
-      <% if attr? :toc %>
-      <div id="toc">
-        <div id="toctitle"><%= attr 'toc-title' %></div>
+<html<%= !(attr? 'nolang') ? %( lang="\#{attr 'lang', 'en'}") : nil %>>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=<%= attr :encoding %>">
+<meta name="generator" content="Asciidoctor <%= attr 'asciidoctor-version' %>">
+<meta name="viewport" content="width=device-width, initial-scale=1.0"><%
+if attr? :description %>
+<meta name="description" content="<%= attr :description %>"><%
+end
+if attr? :keywords %>
+<meta name="keywords" content="<%= attr :keywords %>"><%
+end %>
+<title><%= doctitle %></title><%
+if DEFAULT_STYLESHEET_KEYS.include?(attr 'stylesheet')
+  if @safe >= SafeMode::SECURE || (attr? 'linkcss') %>
+<link rel="stylesheet" href="<%= normalize_web_path(DEFAULT_STYLESHEET_NAME, (attr :stylesdir, '')) %>"><%
+  else %>
+<style>
+<%= ::Asciidoctor::HTML5.default_asciidoctor_stylesheet %>
+</style><%
+  end
+elsif attr? :stylesheet
+  if attr? 'linkcss' %>
+<link rel="stylesheet" href="<%= normalize_web_path((attr :stylesheet), (attr :stylesdir, '')) %>"><%
+  else %>
+<style>
+<%= read_asset normalize_system_path((attr :stylesheet), (attr :stylesdir, '')), true %>
+</style><%
+  end
+end
+if attr? 'icons', 'font'
+  if !(attr 'iconfont-remote', '').nil? %>
+<link rel="stylesheet" href="<%= attr 'iconfont-cdn', 'http://cdnjs.cloudflare.com/ajax/libs/font-awesome/3.1.0/css' %>/<%= attr 'iconfont-name', 'font-awesome' %>.min.css"><%
+  else %>
+<link rel="stylesheet" href="<%= normalize_web_path(%(\#{attr 'iconfont-name', 'font-awesome'}.css), (attr 'stylesdir', '')) %>"><%
+  end
+end
+case attr 'source-highlighter'
+when 'coderay'
+  if (attr 'coderay-css', 'class') == 'class' %>
+<style>
+<%= ::Asciidoctor::HTML5.default_coderay_stylesheet %>
+</style><%
+  end
+when 'highlightjs', 'highlight.js' %>
+<link rel="stylesheet" href="<%= attr :highlightjsdir, 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/7.3' %>/styles/<%= attr 'highlightjs-theme', 'default' %>.min.css">
+<script src="<%= attr :highlightjsdir, 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/7.3' %>/highlight.min.js"></script>
+<script>hljs.initHighlightingOnLoad()</script><%
+when 'prettify' %>
+<link rel="stylesheet" href="<%= attr 'prettifydir', 'http://cdnjs.cloudflare.com/ajax/libs/prettify/r298' %>/<%= attr 'prettify-theme', 'prettify' %>.min.css">
+<script src="<%= attr 'prettifydir', 'http://cdnjs.cloudflare.com/ajax/libs/prettify/r298' %>/prettify.min.js"></script>
+<script>document.addEventListener('DOMContentLoaded', prettyPrint)</script><%
+end %><%= (docinfo_content = docinfo).empty? ? nil : %(
+\#{docinfo_content}) %>
+</head>
+<body#{id} class="<%= doctype %><%= (attr? 'toc-class') && (attr? 'toc') && (attr? 'toc-placement', 'auto') ? %( \#{attr 'toc-class'}) : nil %>"<%= (attr? 'max-width') ? %( style="max-width: \#{attr 'max-width'};") : nil %>><%
+unless noheader %>
+<div id="header"><%
+  if has_header?
+    unless notitle %>
+<h1><%= @header.title %></h1><%
+    end %><%
+    if attr? :author %>
+<span id="author"><%= attr :author %></span><br><%
+      if attr? :email %>
+<span id="email"><%= sub_macros(attr :email) %></span><br><%
+      end
+    end
+    if attr? :revnumber %>
+<span id="revnumber">version <%= attr :revnumber %><%= (attr? :revdate) ? ',' : '' %></span><%
+    end
+    if attr? :revdate %>
+<span id="revdate"><%= attr :revdate %></span><%
+    end
+    if attr? :revremark %>
+<br><span id="revremark"><%= attr :revremark %></span><%
+    end
+  end
+  if (attr? :toc) && (attr? 'toc-placement', 'auto') %>
+<div id="toc" class="<%= attr 'toc-class', 'toc' %>">
+<div id="toctitle"><%= attr 'toc-title' %></div>
 <%= template.class.outline(self, (attr :toclevels, 2).to_i) %>
-      </div>
-      <% end %>
-    </div>
-    <% end %>
-    <div id="content">
+</div><%
+  end %>
+</div><%
+end %>
+<div id="content">
 <%= content %>
-    </div>
-    <% if footnotes? %>
-    <div id="footnotes">
-      <hr>
-      <% footnotes.each do |fn| %>
-      <div class="footnote" id="_footnote_<%= fn.index %>">
-        <a href="#_footnoteref_<%= fn.index %>"><%= fn.index %></a>. <%= fn.text %>
-      </div>
-      <% end %>
-    </div>
-    <% end %>
-    <div id="footer">
-      <div id="footer-text">
-        <% if attr? :revnumber %>Version <%= attr :revnumber %><br><% end %>
-        Last updated <%= attr :docdatetime %>
-      </div>
-    </div>
-  </body>
+</div><%
+unless !footnotes? || (attr? :nofootnotes) %>
+<div id="footnotes">
+<hr><%
+  footnotes.each do |fn| %>
+<div class="footnote" id="_footnote_<%= fn.index %>">
+<a href="#_footnoteref_<%= fn.index %>"><%= fn.index %></a>. <%= fn.text %>
+</div><%
+  end %>
+</div><%
+end %>
+<div id="footer">
+<div id="footer-text"><%
+if attr? :revnumber %>
+Version <%= attr :revnumber %><br><%
+end %>
+Last updated <%= attr :docdatetime %>
+</div>
+</div>
+</body>
 </html>
     EOS
   end
@@ -146,42 +176,122 @@ class EmbeddedTemplate < BaseTemplate
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><% unless notitle || !has_header? %><h1#{id}><%= header.title %></h1>
 <% end %><%= content %>
+<% unless !footnotes? || (attr? :nofootnotes) %><div id="footnotes">
+  <hr>
+  <% footnotes.each do |fn| %>
+  <div class="footnote" id="_footnote_<%= fn.index %>">
+    <a href="#_footnoteref_<%= fn.index %>"><%= fn.index %></a>. <%= fn.text %>
+  </div>
+  <% end %>
+</div><% end %>
     EOS
   end
 end
 
+class BlockTocTemplate < BaseTemplate
+  def result(node)
+    doc = node.document
+
+    return '' unless (doc.attr? 'toc')
+
+    if node.id
+      id_attr = %( id="#{node.id}")
+      title_id_attr = ''
+    elsif doc.embedded? || !(doc.attr? 'toc-placement')
+      id_attr = ' id="toc"'
+      title_id_attr = ' id="toctitle"'
+    else
+      id_attr = ''
+      title_id_attr = ''
+    end
+    title = node.title? ? node.title : (doc.attr 'toc-title')
+    levels = (node.attr? 'levels') ? (node.attr 'levels').to_i : (doc.attr 'toclevels', 2).to_i
+    role = (node.attr? 'role') ? (node.attr 'role') : (doc.attr 'toc-class', 'toc')
+
+    %(<div#{id_attr} class="#{role}">
+<div#{title_id_attr} class="title">#{title}</div>
+#{DocumentTemplate.outline(doc, levels)}
+</div>\n)
+  end
+
+  def template
+    :invoke_result
+  end
+end
+
 class BlockPreambleTemplate < BaseTemplate
+  def toc(node)
+    if (node.attr? 'toc') && (node.attr? 'toc-placement', 'preamble')
+      %(\n<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
+<div id="toctitle">#{node.attr 'toc-title'}</div>
+#{DocumentTemplate.outline(node.document, (node.attr 'toclevels', 2).to_i)}
+</div>)
+    else
+      ''
+    end
+  end
+
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><div id="preamble">
-  <div class="sectionbody">
+<div class="sectionbody">
 <%= content %>
-  </div>
+</div><%= template.toc(self) %>
 </div>
     EOS
   end
 end
 
 class SectionTemplate < BaseTemplate
+  def result(sec)
+    slevel = sec.level
+    # QUESTION should this check be done in section?
+    if slevel == 0 && sec.special
+      slevel = 1
+    end
+    htag = "h#{slevel + 1}"
+    id = anchor = link_start = link_end = nil
+    if sec.id
+      id = %( id="#{sec.id}")
+      if sec.document.attr? 'sectanchors'
+        #if sec.document.attr? 'icons', 'font'
+        #  anchor = %(<a class="anchor" href="##{sec.id}"><i class="icon-anchor"></i></a>)
+        #else
+          anchor = %(<a class="anchor" href="##{sec.id}"></a>)
+        #end
+      elsif sec.document.attr? 'sectlinks'
+        link_start = %(<a class="link" href="##{sec.id}">)
+        link_end = '</a>'
+      end
+    end
+
+    if slevel == 0
+      %(<h1#{id} class="sect0">#{anchor}#{link_start}#{sec.title}#{link_end}</h1>
+#{sec.content}\n)
+    else
+      role = (sec.attr? 'role') ? " #{sec.attr 'role'}" : nil
+      if !sec.special && (sec.document.attr? 'numbered') && slevel < 4
+        sectnum = "#{sec.sectnum} "
+      else
+        sectnum = nil
+      end
+
+      if slevel == 1
+        content = %(<div class="sectionbody">
+#{sec.content}
+</div>)
+      else
+        content = sec.content
+      end
+      %(<div class="sect#{slevel}#{role}">
+<#{htag}#{id}>#{anchor}#{link_start}#{sectnum}#{sec.caption}#{sec.title}#{link_end}</#{htag}>
+#{content}
+</div>\n)
+    end
+  end
+
   def template
-    @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><%
-if @level == 0 %>
-<h1#{id}><%= title %></h1>
-<%= content %>
-<% else %>
-<div class="sect<%= @level %>#{role_class}">
-  <h<%= @level + 1 %>#{id}><% if !@special && (attr? :numbered) && @level < 4 %><%= sectnum %> <% end %><%= attr :caption %><%= title %></h<%= @level + 1 %>>
-  <% if @level == 1 %>
-  <div class="sectionbody">
-<%= content %>
-  </div>
-  <% else %>
-<%= content %>
-  <% end %>
-</div>
-<% end %>
-    EOS
+    :invoke_result
   end
 end
 
@@ -197,69 +307,103 @@ class BlockDlistTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><%
-if attr? :style, 'qanda' %>
-<div#{id} class="qlist#{style_class}#{role_class}">
-  #{title_div}
-  <ol>
-  <% content.each do |dt, dd| %>
-    <li>
-      <p><em><%= dt.text %></em></p>
-      <% unless dd.nil? %>
-      <% if dd.text? %>
-      <p><%= dd.text %></p>
-      <% end %>
-      <% if dd.blocks? %>
-<%= dd.content %>
-      <% end %>
-      <% end %>
-    </li>
-  <% end %>
-  </ol>
-</div>
-<% elsif attr? :style, 'horizontal' %>
-<div#{id} class="hdlist#{role_class}">
-  #{title_div}
-  <table>
-    <colgroup>
-      <col<% if attr? :labelwidth %> style="width: <%= attr :labelwidth %>%;"<% end %>>
-      <col<% if attr? :itemwidth %> style="width: <%= attr :itemwidth %>%;"<% end %>>
-    </colgroup>
-    <% content.each do |dt, dd| %>
-    <tr>
-      <td class="hdlist1<% if attr? 'strong-option' %> strong<% end %>">
-        <%= dt.text %>
-        <br>
-      </td>
-      <td class="hdlist2"><% unless dd.nil? %><% if dd.text? %>
-        <p style="margin-top: 0"><%= dd.text %></p><% end %><% if dd.blocks? %>
-<%= dd.content %><% end %><% end %>
-      </td>
-    </tr>
-    <% end %>
-  </table>
-</div>
-<% else %>
-<div#{id} class="dlist#{style_class}#{role_class}">
-  #{title_div}
-  <dl>
-    <% content.each do |dt, dd| %>
-    <dt<% if !(attr? :style) %> class="hdlist1"<% end %>>
-      <%= dt.text %>
-    </dt>
-    <% unless dd.nil? %>
-    <dd>
-      <% if dd.text? %>
-      <p><%= dd.text %></p>
-      <% end %>
-      <% if dd.blocks? %>
-<%= dd.content %>
-      <% end %>
-    </dd>
-    <% end %>
-    <% end %>
-  </dl>
-</div>
-<% end %>
+continuing = false
+entries = content
+last_index = entries.length - 1
+if attr? 'style', 'qanda', false
+%><div#{id} class="qlist#{style_class}#{role_class}"><%
+if title? %>
+<div class="title"><%= title %></div><%
+end %>
+<ol><%
+  entries.each_with_index do |(dt, dd), index|
+    last = (index == last_index)
+    unless continuing %>
+<li><%
+    end %>
+<p><em><%= dt.text %></em></p><%
+    if !last && dd.nil?
+      continuing = true
+      next
+    else
+      continuing = false
+    end
+    unless dd.nil?
+      if dd.text? %>
+<p><%= dd.text %></p><%
+      end
+      if dd.blocks? %>
+<%= dd.content %><%
+      end
+    end %>
+</li><%
+  end %>
+</ol>
+</div><%
+elsif attr? 'style', 'horizontal', false
+%><div#{id} class="hdlist#{role_class}"><%
+if title? %>
+<div class="title"><%= title %></div><%
+end %>
+<table><%
+if (attr? :labelwidth) || (attr? :itemwidth) %>
+<colgroup>
+<col<% if attr? :labelwidth %> style="width:<%= (attr :labelwidth).chomp('%') %>%;"<% end %>>
+<col<% if attr? :itemwidth %> style="width:<%= (attr :itemwidth).chomp('%') %>%;"<% end %>>
+</colgroup><%
+end %><%
+  entries.each_with_index do |(dt, dd), index|
+    last = (index == last_index)
+    unless continuing %>
+<tr>
+<td class="hdlist1<%= (attr? 'strong-option') ? 'strong' : nil %>"><%
+    end %>
+<%= dt.text %>
+<br><%
+    if !last && dd.nil?
+      continuing = true
+      next
+    else
+      continuing = false
+    end %>
+</td>
+<td class="hdlist2"><%
+    unless dd.nil?
+      if dd.text? %>
+<p><%= dd.text %></p><%
+      end
+      if dd.blocks? %>
+<%= dd.content %><%
+      end
+    end %>
+</td>
+</tr><%
+  end %>
+</table>
+</div><%
+else
+%><div#{id} class="dlist#{style_class}#{role_class}"><%
+if title? %>
+<div class="title"><%= title %></div><%
+end %>
+<dl><%
+  entries.each_with_index do |(dt, dd), index|
+    last = (index == last_index) %>
+<dt<%= !(attr? 'style', nil, false) ? %( class="hdlist1") : nil %>><%= dt.text %></dt><%
+    unless dd.nil? %>
+<dd><%
+      if dd.text? %>
+<p><%= dd.text %></p><%
+      end %><%
+      if dd.blocks? %>
+<%= dd.content %><%
+      end %>
+</dd><%
+    end
+  end %>
+</dl>
+</div><%
+end %>
     EOS
   end
 end
@@ -268,14 +412,30 @@ class BlockListingTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><div#{id} class="listingblock#{role_class}">
-  #{title_div :caption => true}
-  <div class="content monospaced">
-    <% if attr? :style, 'source' %>
-    <pre class="highlight<% if attr? 'source-highlighter', 'coderay' %> CodeRay<% end %>"><code#{attribute('class', :language)}><%= template.preserve_endlines(content, self) %></code></pre>
-    <% else %>
-    <pre><%= template.preserve_endlines(content, self) %></pre>
-    <% end %>
-  </div>
+#{title_div :caption => true}
+<div class="content monospaced"><%
+if attr? 'style', 'source', false
+  language = (language = (attr 'language')) ? %(\#{language} language-\#{language}) : nil
+  case attr 'source-highlighter'
+  when 'coderay'
+    pre_class = ' class="CodeRay"'
+    code_class = language ? %( class="\#{language}") : nil
+  when 'highlightjs', 'highlight.js'
+    pre_class = ' class="highlight"'
+    code_class = language ? %( class="\#{language}") : nil
+  when 'prettify'
+    pre_class = %( class="prettyprint\#{(attr? 'linenums') ? ' linenums' : nil})
+    pre_class = language ? %(\#{pre_class} \#{language}") : %(\#{pre_class}")
+    code_class = nil
+  else
+    pre_class = ' class="highlight"'
+    code_class = language ? %( class="\#{language}") : nil
+  end %>
+<pre<%= pre_class %>><code<%= code_class %>><%= template.preserve_endlines(content, self) %></code></pre><%
+else %>
+<pre><%= template.preserve_endlines(content, self) %></pre><%
+end %>
+</div>
 </div>
     EOS
   end
@@ -285,10 +445,10 @@ class BlockLiteralTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><div#{id} class="literalblock#{role_class}">
-  #{title_div}
-  <div class="content monospaced">
-    <pre><%= template.preserve_endlines(content, self) %></pre>
-  </div>
+#{title_div}
+<div class="content monospaced">
+<pre><%= template.preserve_endlines(content, self) %></pre>
+</div>
 </div>
     EOS
   end
@@ -297,22 +457,24 @@ end
 class BlockAdmonitionTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><div#{id} class="admonitionblock#{role_class}">
-  <table>
-    <tr>
-      <td class="icon">
-        <% if attr? :icons %>
-        <img src="<%= icon_uri(attr :name) %>" alt="<%= attr :caption %>">
-        <% else %>
-        <div class="title"><%= attr :caption %></div>
-        <% end %>
-      </td>
-      <td class="content">
-        #{title_div}
-        <%= content %>
-      </td>
-    </tr>
-  </table>
+<%#encoding:UTF-8%><div#{id} class="admonitionblock <%= attr :name %>#{role_class}">
+<table>
+<tr>
+<td class="icon"><%
+if attr? 'icons', 'font' %>
+<i class="icon-<%= attr :name %>" title="<%= @caption %>"></i><%
+elsif attr? 'icons' %>
+<img src="<%= icon_uri(attr :name) %>" alt="<%= @caption %>"><%
+else %>
+<div class="title"><%= @caption %></div><%
+end %>
+</td>
+<td class="content">
+#{title_div}
+<%= content %>
+</td>
+</tr>
+</table>
 </div>
     EOS
   end
@@ -320,17 +482,18 @@ end
 
 class BlockParagraphTemplate < BaseTemplate
   def paragraph(id, role, title, content)
-    %(<div#{id && " id=\"#{id}\""} class=\"paragraph#{role && " #{role}"}\">
-  #{title && "<div class=\"title\">#{title}</div>"}  
-  <p>#{content}</p>
-</div>)
+    %(<div#{id && " id=\"#{id}\""} class="paragraph#{role && " #{role}"}">#{title && "
+<div class=\"title\">#{title}</div>"}
+<p>#{content}</p>
+</div>\n)
+  end
+
+  def result(node)
+    paragraph(node.id, (node.attr 'role'), (node.title? ? node.title : nil), node.content)
   end
 
   def template
-    # very hot piece of code, optimized for speed
-    @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><%= template.paragraph(@id, (attr 'role'), title? ? title : nil, content) %>
-    EOS
+    :invoke_result
   end
 end
 
@@ -338,10 +501,10 @@ class BlockSidebarTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><div#{id} class="sidebarblock#{role_class}">
-  <div class="content">
-    #{title_div}
+<div class="content">
+#{title_div}
 <%= content %>
-  </div>
+</div>
 </div>
     EOS
   end
@@ -351,25 +514,48 @@ class BlockExampleTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><div#{id} class="exampleblock#{role_class}">
-  #{title_div :caption => true}
-  <div class="content">
+#{title_div :caption => true}
+<div class="content">
 <%= content %>
-  </div>
+</div>
 </div>
     EOS
   end
 end
 
 class BlockOpenTemplate < BaseTemplate
-  def template
-    @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><div#{id} class="openblock#{role_class}">
-  #{title_div}
-  <div class="content">
-<%= content %>
-  </div>
+  def result(node)
+    open_block(node, node.id, (node.attr 'style', nil, false), (node.attr 'role'), node.title? ? node.title : nil, node.content)
+  end
+
+  def open_block(node, id, style, role, title, content)
+    if style == 'abstract'
+      if node.parent == node.document && node.document.attr?('doctype', 'book')
+        puts 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
+        ''
+      else
+        %(<div#{id && " id=\"#{id}\""} class="quoteblock abstract#{role && " #{role}"}">#{title &&
+"<div class=\"title\">#{title}</div>"}
+<blockquote>
+#{content}
+</blockquote>
+</div>\n)
+      end
+    elsif style == 'partintro' && (!node.document.attr?('doctype', 'book') || !node.parent.is_a?(Asciidoctor::Section) || node.level != 0)
+      puts 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a book part. Excluding block content.'
+      ''
+    else
+      %(<div#{id && " id=\"#{id}\""} class="openblock#{style != 'open' ? " #{style}" : ''}#{role && " #{role}"}">#{title &&
+"<div class=\"title\">#{title}</div>"}
+<div class="content">
+#{content}
 </div>
-    EOS
+</div>\n)
+    end
+  end
+
+  def template
+    :invoke_result
   end
 end
 
@@ -383,21 +569,23 @@ class BlockQuoteTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><div#{id} class="quoteblock#{role_class}">
-  #{title_div}
-  <blockquote>
+#{title_div}
+<blockquote>
 <%= content %>
-  </blockquote>
-  <div class="attribution">
-    <% if attr? :citetitle %>
-    <cite><%= attr :citetitle %></cite>
-    <% end %>
-    <% if attr? :attribution %>
-    <% if attr? :citetitle %>
-    <br>
-    <% end %>
-    <%= "— \#{attr :attribution}" %>
-    <% end %>
-  </div>
+</blockquote><%
+if (attr? :attribution) || (attr? :citetitle) %>
+<div class="attribution"><%
+  if attr? :citetitle %>
+<cite><%= attr :citetitle %></cite><%
+  end
+  if attr? :attribution
+    if attr? :citetitle %>
+<br><%
+    end %>
+<%= "— \#{attr :attribution}" %><%
+  end %>
+</div><%
+end %>
 </div>
     EOS
   end
@@ -407,19 +595,21 @@ class BlockVerseTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><div#{id} class="verseblock#{role_class}">
-  #{title_div}
-  <pre class="content"><%= template.preserve_endlines(content, self) %></pre>
-  <div class="attribution">
-    <% if attr? :citetitle %>
-    <cite><%= attr :citetitle %></cite>
-    <% end %>
-    <% if attr? :attribution %>
-    <% if attr? :citetitle %>
-    <br>
-    <% end %>
-    <%= "— \#{attr :attribution}" %>
-    <% end %>
-  </div>
+#{title_div}
+<pre class="content"><%= template.preserve_endlines(content, self) %></pre><%
+if (attr? :attribution) || (attr? :citetitle) %>
+<div class="attribution"><%
+  if attr? :citetitle %>
+<cite><%= attr :citetitle %></cite><%
+  end
+  if attr? :attribution
+    if attr? :citetitle %>
+<br><%
+    end %>
+<%= "— \#{attr :attribution}" %><%
+  end %>
+  </div><%
+end %>
 </div>
     EOS
   end
@@ -429,37 +619,38 @@ class BlockUlistTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><div#{id} class="ulist#{style_class}#{role_class}">
-  #{title_div}
-  <ul>
-  <% content.each do |item| %>
-    <li>
-      <p><%= item.text %></p>
-      <% if item.blocks? %>
-<%= item.content %>
-      <% end %>
-    </li>
-  <% end %>
-  </ul>
+#{title_div}
+<ul><%
+content.each do |item| %>
+<li>
+<p><%= item.text %></p><%
+  if item.blocks? %>
+<%= item.content %><%
+  end %>
+</li><%
+end %>
+</ul>
 </div>
     EOS
   end
 end
 
 class BlockOlistTemplate < BaseTemplate
+
   def template
     @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><div#{id} class="olist#{style_class}#{role_class}">
-  #{title_div}
-  <ol class="<%= attr :style %>"#{attribute('start', :start)}>
-  <% content.each do |item| %>
-    <li>
-      <p><%= item.text %></p>
-      <% if item.blocks? %>
-<%= item.content %>
-      <% end %>
-    </li>
-  <% end %>
-  </ol>
+<%#encoding:UTF-8%><% style = attr 'style', nil, false %><div#{id} class="olist#{style_class}#{role_class}">
+#{title_div}
+<ol class="<%= style %>"<%= (type = ::Asciidoctor::ORDERED_LIST_KEYWORDS[style]) ? %( type="\#{type}") : nil %>#{attribute('start', :start)}><%
+content.each do |item| %>
+<li>
+<p><%= item.text %></p><%
+  if item.blocks? %>
+<%= item.content %><%
+  end %>
+</li><%
+end %>
+</ol>
 </div>
     EOS
   end
@@ -469,25 +660,28 @@ class BlockColistTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><div#{id} class="colist#{style_class}#{role_class}">
-  #{title_div}
-  <% if attr? :icons %>
-  <table>
-    <% content.each_with_index do |item, i| %>
-    <tr>
-      <td><img src="<%= icon_uri("callouts/\#{i + 1}") %>" alt="<%= i + 1 %>"></td>
-      <td><%= item.text %></td>
-    </tr>
-    <% end %>
-  </table>
-  <% else %>
-  <ol>
-  <% content.each do |item| %>
-    <li>
-      <p><%= item.text %></p>
-    </li>
-  <% end %>
-  </ol>
-  <% end %>
+#{title_div}<%
+if attr? :icons %>
+<table><%
+  content.each_with_index do |item, i| %>
+<tr>
+<td><%
+    if attr? :icons, 'font' %><i class="conum"><%= i + 1 %></i><%
+    else %><img src="<%= icon_uri("callouts/\#{i + 1}") %>" alt="<%= i + 1 %>"><%
+    end %></td>
+<td><%= item.text %></td>
+</tr><%
+  end %>
+</table><%
+else %>
+<ol><%
+  content.each do |item| %>
+<li>
+<p><%= item.text %></p>
+</li><%
+  end %>
+</ol><%
+end %>
 </div>
     EOS
   end
@@ -497,43 +691,57 @@ class BlockTableTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
 <%#encoding:UTF-8%><table#{id} class="tableblock frame-<%= attr :frame, 'all' %> grid-<%= attr :grid, 'all'%>#{role_class}" style="<%
-if !(attr? 'autowidth-option') %>width: <%= attr :tablepcwidth %>%; <% end %><%
-if attr? :float %>float: <%= attr :float %>; <% end %>">
-  <% if title? %>
-  <caption class="title"><% unless @caption.nil? %><%= @caption %><% end %><%= title %></caption>
-  <% end %>
-  <% if (attr :rowcount) >= 0 %>
-  <colgroup>
-    <% if attr? 'autowidth-option' %>
-    <% @columns.each do %>
-    <col>
-    <% end %>
-    <% else %>
-    <% @columns.each do |col| %>
-    <col style="width: <%= col.attr :colpcwidth %>%;">
-    <% end %>
-    <% end %>
-  </colgroup>
-  <% [:head, :foot, :body].select {|tsec| !@rows[tsec].empty? }.each do |tsec| %>
-  <t<%= tsec %>>
-    <% @rows[tsec].each do |row| %>
-    <tr>
-      <% row.each do |cell| %>
-      <<%= tsec == :head ? 'th' : 'td' %> class="tableblock halign-<%= cell.attr :halign %> valign-<%= cell.attr :valign %>"#{attribute('colspan', 'cell.colspan')}#{attribute('rowspan', 'cell.rowspan')}><%
-      if tsec == :head %><%= cell.text %><% else %><%
-      case cell.attr(:style)
-        when :asciidoc %><div><%= cell.content %></div><%
-        when :verse %><div class="verse"><%= template.preserve_endlines(cell.text, self) %></div><%
-        when :literal %><div class="literal monospaced"><pre><%= template.preserve_endlines(cell.text, self) %></pre></div><%
-        when :header %><% cell.content.each do |text| %><p class="tableblock header"><%= text %></p><% end %><%
-        else %><% cell.content.each do |text| %><p class="tableblock"><%= text %></p><% end %><%
-      end %><% end %></<%= tsec == :head ? 'th' : 'td' %>>
-      <% end %>
-    </tr>
-    <% end %>
-  </t<%= tsec %>>
-  <% end %>
-  <% end %>
+if !(attr? 'autowidth-option') %>width:<%= attr :tablepcwidth %>%; <% end %><%
+if attr? :float %>float: <%= attr :float %>; <% end %>"><%
+if title? %>
+<caption class="title"><% unless @caption.nil? %><%= @caption %><% end %><%= title %></caption><%
+end
+if (attr :rowcount) >= 0 %>
+<colgroup><%
+  if attr? 'autowidth-option'
+    @columns.each do %>
+<col><%
+    end
+  else
+    @columns.each do |col| %>
+<col style="width:<%= col.attr :colpcwidth %>%;"><%
+    end
+  end %> 
+</colgroup><%
+  [:head, :foot, :body].select {|tsec| !@rows[tsec].empty? }.each do |tsec| %>
+<t<%= tsec %>><%
+    @rows[tsec].each do |row| %>
+<tr><%
+      row.each do |cell| %>
+<<%= tsec == :head ? 'th' : 'td' %> class="tableblock halign-<%= cell.attr :halign %> valign-<%= cell.attr :valign %>"#{attribute('colspan', 'cell.colspan')}#{attribute('rowspan', 'cell.rowspan')}<%
+        cell_content = ''
+        if tsec == :head
+          cell_content = cell.text
+        else
+          case (cell.attr 'style', nil, false)
+          when :asciidoc
+            cell_content = %(<div>\#{cell.content}</div>)
+          when :verse
+            cell_content = %(<div class="verse">\#{template.preserve_endlines(cell.text, self)}</div>)
+          when :literal
+            cell_content = %(<div class="literal monospaced"><pre>\#{template.preserve_endlines(cell.text, self)}</pre></div>)
+          when :header
+            cell.content.each do |text|
+              cell_content = %(\#{cell_content}<p class="tableblock header">\#{text}</p>)
+            end
+          else
+            cell.content.each do |text|
+              cell_content = %(\#{cell_content}<p class="tableblock">\#{text}</p>)
+            end
+          end
+        end %><%= (@document.attr? 'cellbgcolor') ? %( style="background-color:\#{@document.attr 'cellbgcolor'};") : nil
+        %>><%= cell_content %></<%= tsec == :head ? 'th' : 'td' %>><%
+      end %>
+</tr><%
+    end %>
+</t<%= tsec %>><%
+  end
+end %>
 </table>
     EOS
   end
@@ -542,103 +750,210 @@ end
 class BlockImageTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><div#{id} class="imageblock#{style_class}#{role_class}"<% if (attr? :align) || (attr? :float)
-%> style="<% if attr? :align %>text-align: <%= attr :align %><% if attr? :float %>; <% end %><% end %><% if attr? :float %>float: <%= attr :float %><% end %>"<% end
-%>>
-  <div class="content">
-    <% if attr? :link %>
-    <a class="image" href="<%= attr :link %>"><img src="<%= image_uri(attr :target) %>" alt="<%= attr :alt %>"#{attribute('width', :width)}#{attribute('height', :height)}></a>
-    <% else %>
-    <img src="<%= image_uri(attr :target) %>" alt="<%= attr :alt %>"#{attribute('width', :width)}#{attribute('height', :height)}>
-    <% end %>
-  </div>
-  #{title_div :caption => true}
+<%#encoding:UTF-8%><div#{id} class="imageblock#{style_class}#{role_class}"<%
+if (attr? :align) || (attr? :float) %> style="<%
+  if attr? :align %>text-align: <%= attr :align %><% if attr? :float %>; <% end %><% end %><% if attr? :float %>float: <%= attr :float %><% end %>"<%
+end %>>
+<div class="content"><%
+if attr? :link %>
+<a class="image" href="<%= attr :link %>"><img src="<%= image_uri(attr :target) %>" alt="<%= attr :alt %>"#{attribute('width', :width)}#{attribute('height', :height)}></a><%
+else %>
+<img src="<%= image_uri(attr :target) %>" alt="<%= attr :alt %>"#{attribute('width', :width)}#{attribute('height', :height)}><%
+end %>
+</div>
+#{title_div :caption => true}
 </div>
     EOS
   end
 end
 
-class BlockRulerTemplate < BaseTemplate
+class BlockAudioTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><hr>
+<%#encoding:UTF-8%><div#{id} class="audioblock#{style_class}#{role_class}">
+#{title_div :caption => true}
+<div class="content">
+<audio src="<%= media_uri(attr :target) %>"<%
+if attr? 'autoplay-option' %> autoplay<% end %><%
+unless attr? 'nocontrols-option' %> controls<% end %><%
+if attr? 'loop-option' %> loop<% end %>>
+Your browser does not support the audio tag.
+</audio>
+</div>
+</div>
     EOS
   end
 end
 
-class BlockPageBreakTemplate < BaseTemplate
+class BlockVideoTemplate < BaseTemplate
   def template
     @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><div style="page-break-after: always"></div>
+<%#encoding:UTF-8%><div#{id} class="videoblock#{style_class}#{role_class}">
+#{title_div :caption => true}
+<div class="content">
+<video src="<%= media_uri(attr :target) %>"#{attribute('width', :width)}#{attribute('height', :height)}<%
+if attr? 'poster' %> poster="<%= media_uri(attr :poster) %>"<% end %><%
+if attr? 'autoplay-option' %> autoplay<% end %><%
+unless attr? 'nocontrols-option' %> controls<% end %><%
+if attr? 'loop-option' %> loop<% end %>>
+Your browser does not support the video tag.
+</video>
+</div>
+</div>
     EOS
   end
 end
 
+class BlockRulerTemplate < BaseTemplate
+  def result(node)
+    '<hr>'
+  end
+
+  def template
+    :invoke_result
+  end
+end
+
+class BlockPageBreakTemplate < BaseTemplate
+  def result(node)
+    %(<div style="page-break-after: always;"></div>\n)
+  end
+
+  def template
+    :invoke_result
+  end
+end
+
 class InlineBreakTemplate < BaseTemplate
+  def result(node)
+    %(#{node.text}<br>\n)
+  end
+
   def template
-    @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><%= "\#@text<br>" %>
-    EOS
+    :invoke_result
   end
 end
 
 class InlineCalloutTemplate < BaseTemplate
+  def result(node)
+    if node.attr? 'icons', 'font'
+      %(<i class="conum">#{node.text}</i>)
+    elsif node.attr? 'icons'
+      src = node.icon_uri("callouts/#{node.text}")
+      %(<img src="#{src}" alt="#{node.text}">)
+    else
+      "<b><#{node.text}></b>"
+    end
+  end
+
   def template
-    @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><% if attr? :icons %><img src="<%= icon_uri("callouts/\#@text") %>" alt="<%= @text %>"><% else %><b><<%= @text %>></b><% end %>
-    EOS
+    :invoke_result
   end
 end
 
 class InlineQuotedTemplate < BaseTemplate
-  QUOTED_TAGS = {
+  NO_TAGS = ['', '']
+
+  QUOTE_TAGS = {
     :emphasis => ['<em>', '</em>'],
     :strong => ['<strong>', '</strong>'],
-    :monospaced => ['<tt>', '</tt>'],
+    :monospaced => ['<code>', '</code>'],
     :superscript => ['<sup>', '</sup>'],
     :subscript => ['<sub>', '</sub>'],
     :double => ['“', '”'],
     :single => ['‘', '’']
-    #:none => ['', '']
   }
 
-  def quote(text, type, role)
-    start_tag, end_tag = QUOTED_TAGS[type] || ['', '']
+  def quote_text(text, type, role)
+    start_tag, end_tag = QUOTE_TAGS[type] || NO_TAGS
     if role
-      "#{start_tag}<span class=\"#{role}\">#{text}</span>#{end_tag}"
+      if start_tag.start_with? '<'
+        %(#{start_tag.chop} class="#{role}">#{text}#{end_tag})
+      else
+        %(#{start_tag}<span class="#{role}">#{text}</span>#{end_tag})
+      end
     else
       "#{start_tag}#{text}#{end_tag}"
     end
   end
 
+  def result(node)
+    quote_text(node.text, node.type, (node.attr 'role'))
+  end
+
   def template
-    # very hot piece of code, optimized for speed
-    @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><%= template.quote(@text, @type, attr('role')) %>
-    EOS
+    :invoke_result
+  end
+end
+
+class InlineButtonTemplate < BaseTemplate
+  def result(node)
+    %(<b class="button">#{node.text}</b>)
+  end
+
+  def template
+    :invoke_result
+  end
+end
+
+class InlineKbdTemplate < BaseTemplate
+  def result(node)
+    keys = node.attr 'keys'
+    if keys.size == 1
+      %(<kbd>#{keys.first}</kbd>)
+    else
+      key_combo = keys.map{|key| %(<kbd>#{key}</kbd>+) }.join.chop
+      %(<kbd class="keyseq">#{key_combo}</kbd>)
+    end
+  end
+
+  def template
+    :invoke_result
+  end
+end
+
+class InlineMenuTemplate < BaseTemplate
+  def menu(menu, submenus, menuitem)
+    if !submenus.empty?
+      submenu_path = submenus.map{|submenu| %(<span class="submenu">#{submenu}</span> ▸ ) }.join.chop
+      %(<span class="menuseq"><span class="menu">#{menu}</span> ▸ #{submenu_path} <span class="menuitem">#{menuitem}</span></span>)
+    elsif !menuitem.nil?
+      %(<span class="menuseq"><span class="menu">#{menu}</span> ▸ <span class="menuitem">#{menuitem}</span></span>)
+    else
+      %(<span class="menu">#{menu}</span>)
+    end
+  end
+
+  def result(node)
+    menu(node.attr('menu'), node.attr('submenus'), node.attr('menuitem'))
+  end
+
+  def template
+    :invoke_result
   end
 end
 
 class InlineAnchorTemplate < BaseTemplate
-  def anchor(target, text, type, document, window = nil)
+  def anchor(target, text, type, document, node)
     case type
     when :xref
       text = document.references[:ids].fetch(target, "[#{target}]") if text.nil?
       %(<a href="##{target}">#{text}</a>)
     when :ref
       %(<a id="#{target}"></a>)
+    when :link
+      %(<a href="#{target}"#{(node.attr? 'role') ? " class=\"#{node.attr 'role'}\"" : nil}#{(node.attr? 'window') ? " target=\"#{node.attr 'window'}\"" : nil}>#{text}</a>)
     when :bibref
       %(<a id="#{target}"></a>[#{target}])
-    when :link
-      %(<a href="#{target}"#{window && " target=\"#{window}\""}>#{text}</a>)
     end
   end
 
+  def result(node)
+    anchor(node.target, node.text, node.type, node.document, node)
+  end
+
   def template
-    # hot piece of code, optimized for speed
-    @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><%= template.anchor(@target, @text, @type, @document, @type == :link ? attr('window') : nil) %>
-    EOS
+    :invoke_result
   end
 end
 
@@ -646,13 +961,11 @@ class InlineImageTemplate < BaseTemplate
   def template
     # care is taken here to avoid a space inside the optional <a> tag
     @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><span class="image#{role_class}">
-  <%
-  if attr? :link %><a class="image" href="<%= attr :link %>"><%
-  end %><img src="<%= image_uri(@target) %>" alt="<%= attr :alt %>"#{attribute('width', :width)}#{attribute('height', :height)}#{attribute('title', :title)}><%
-  if attr? :link%></a><% end
-  %>
-</span>
+<%#encoding:UTF-8%><span class="image#{role_class}"><%
+if attr? :link %><a class="image" href="<%= attr :link %>"><%
+end %><img src="<%= image_uri(@target) %>" alt="<%= attr :alt %>"#{attribute('width', :width)}#{attribute('height', :height)}#{attribute('title', :title)}><%
+if attr? :link%></a><% end
+%></span>
     EOS
   end
 end
@@ -671,10 +984,12 @@ end %>
 end
 
 class InlineIndextermTemplate < BaseTemplate
+  def result(node)
+    node.type == :visible ? node.text : ''
+  end
+
   def template
-    @template ||= @eruby.new <<-EOS
-<%#encoding:UTF-8%><%= "\#{@type == :visible ? @text : ''}" %>
-    EOS
+    :invoke_result
   end
 end
 
diff --git a/lib/asciidoctor/block.rb b/lib/asciidoctor/block.rb
index 0c0f342..1072521 100644
--- a/lib/asciidoctor/block.rb
+++ b/lib/asciidoctor/block.rb
@@ -14,9 +14,6 @@ class Block < AbstractBlock
   # Public: Get/Set the original Array content for this section block.
   attr_accessor :buffer
 
-  # Public: Get/Set the caption for this block
-  attr_accessor :caption
-
   # Public: Initialize an Asciidoctor::Block object.
   #
   # parent  - The parent Asciidoc Object.
@@ -33,56 +30,12 @@ class Block < AbstractBlock
   # rendered and returned as content that can be included in the
   # parent block's template.
   def render
-    Debug.debug { "Now rendering #{@context} block for #{self}" }
     @document.playback_attributes @attributes
     out = renderer.render("block_#{@context}", self)
     @document.callouts.next_list if @context == :colist
     out
   end
 
-  def splain(parent_level = 0)
-    parent_level += 1
-    Debug.puts_indented(parent_level, "Block id: #{id}") unless self.id.nil?
-    Debug.puts_indented(parent_level, "Block title: #{title}") unless self.title.nil?
-    Debug.puts_indented(parent_level, "Block caption: #{caption}") unless self.caption.nil?
-    Debug.puts_indented(parent_level, "Block level: #{level}") unless self.level.nil?
-    Debug.puts_indented(parent_level, "Block context: #{context}") unless self.context.nil?
-
-    Debug.puts_indented(parent_level, "Blocks: #{@blocks.count}")
-
-    if buffer.is_a? Enumerable
-      buffer.each_with_index do |buf, i|
-        Debug.puts_indented(parent_level, "v" * (60 - parent_level*2))
-        Debug.puts_indented(parent_level, "Buffer ##{i} is a #{buf.class}")
-        Debug.puts_indented(parent_level, "Name is #{buf.title rescue 'n/a'}")
-
-        if buf.respond_to? :splain
-          buf.splain(parent_level)
-        else
-          Debug.puts_indented(parent_level, "Buffer: #{buf}")
-        end
-        Debug.puts_indented(parent_level, "^" * (60 - parent_level*2))
-      end
-    else
-      if buffer.respond_to? :splain
-        buffer.splain(parent_level)
-      else
-        Debug.puts_indented(parent_level, "Buffer: #{@buffer}")
-      end
-    end
-
-    @blocks.each_with_index do |block, i|
-      Debug.puts_indented(parent_level, "v" * (60 - parent_level*2))
-      Debug.puts_indented(parent_level, "Block ##{i} is a #{block.class}")
-      Debug.puts_indented(parent_level, "Name is #{block.title rescue 'n/a'}")
-
-      block.splain(parent_level) if block.respond_to? :splain
-      Debug.puts_indented(parent_level, "^" * (60 - parent_level*2))
-    end
-    
-    nil
-  end
-
   # Public: Get an HTML-ified version of the source buffer, with special
   # Asciidoc characters and entities converted to their HTML equivalents.
   #
@@ -94,9 +47,8 @@ class Block < AbstractBlock
   #   block.content
   #   => ["<em>This</em> is what happens when you <meet> a stranger in the <alps>!"]
   def content
-
     case @context
-    when :preamble, :open, :example, :sidebar
+    when :preamble
       @blocks.map {|b| b.render }.join
     # lists get iterated in the template (for now)
     # list items recurse into this block when their text and content methods are called
@@ -106,14 +58,14 @@ class Block < AbstractBlock
       apply_literal_subs(@buffer)
     when :pass
       apply_passthrough_subs(@buffer)
-    when :quote, :verse, :admonition
+    when :admonition, :example, :sidebar, :quote, :verse, :open
       if !@buffer.nil?
-        apply_normal_subs(@buffer)
+        apply_para_subs(@buffer)
       else
         @blocks.map {|b| b.render }.join
       end
     else
-      apply_normal_subs(@buffer)
+      apply_para_subs(@buffer)
     end
   end
 
diff --git a/lib/asciidoctor/cli/invoker.rb b/lib/asciidoctor/cli/invoker.rb
index 3b1b3e3..7ef5ec7 100644
--- a/lib/asciidoctor/cli/invoker.rb
+++ b/lib/asciidoctor/cli/invoker.rb
@@ -5,14 +5,12 @@ module Asciidoctor
       attr_reader :options
       attr_reader :document
       attr_reader :code
-      attr_reader :timings
 
       def initialize(*options)
         @document = nil
         @out = nil
         @err = nil
         @code = 0
-        @timings = {}
         options = options.flatten
         if !options.empty? && options.first.is_a?(Cli::Options)
           @options = options.first
@@ -32,46 +30,52 @@ module Asciidoctor
         return if @options.nil?
 
         begin
-          @timings = {}
-          infile = @options[:input_file]
-          outfile = @options[:output_file]
+          opts = {}
+          monitor = {}
+          infile = nil
+          outfile = nil
+          @options.map {|k, v|
+            case k
+            when :input_file
+              infile = v
+            when :output_file
+              outfile = v
+            when :destination_dir
+              #opts[:to_dir] = File.expand_path(v) unless v.nil?
+              opts[:to_dir] = v unless v.nil?
+            when :attributes
+              opts[:attributes] = v.dup
+            when :verbose
+              opts[:monitor] = monitor if v
+            when :trace
+              # currently, nothing
+            else
+              opts[k] = v unless v.nil?
+            end
+          }
+
           if infile == '-'
             # allow use of block to supply stdin, particularly useful for tests
             input = block_given? ? yield : STDIN
           else
             input = File.new(infile)
           end
-          start = Time.now
-          @document = Asciidoctor.load(input, @options)
-          timings[:parse] = Time.now - start
-          start = Time.now
-          output = @document.render
-          timings[:render] = Time.now - start
-          if @options[:verbose]
-            puts "Time to read and parse source: #{timings[:parse]}"
-            puts "Time to render document: #{timings[:render]}"
-            puts "Total time to read, parse and render: #{timings.reduce(0) {|sum, (_, v)| sum += v}}"
-          end
-          if outfile == '/dev/null'
-            # output nothing
-          elsif outfile == '-' || (infile == '-' && (outfile.nil? || outfile.empty?))
-            (@out || $stdout).puts output
+
+          if outfile == '-' || (infile == '-' && (outfile.to_s.empty? || outfile != '/dev/null'))
+            opts[:to_file] = (@out || $stdout)
+          elsif !outfile.nil?
+            opts[:to_file] = outfile
           else
-            if outfile.nil? || outfile.empty?
-              if @options[:destination_dir]
-                destination_dir = File.expand_path(@options[:destination_dir])
-              else
-                destination_dir = @document.base_dir
-              end
-              outfile = File.join(destination_dir, "#{@document.attributes['docname']}#{@document.attributes['outfilesuffix']}")
-            else
-              outfile = @document.normalize_asset_path outfile
-            end
+            opts[:in_place] = true unless opts.has_key? :to_dir
+          end
 
-            # this assignment is primarily for testing or other post analysis
-            @document.attributes['outfile'] = outfile
-            @document.attributes['outdir'] = File.dirname(outfile)
-            File.open(outfile, 'w') {|file| file.write output }
+          @document = Asciidoctor.render(input, opts)
+
+          # FIXME this should be :monitor, :profile or :timings rather than :verbose
+          if @options[:verbose]
+            puts "Time to read and parse source: #{'%05.5f' % monitor[:parse]}"
+            puts "Time to render document: #{'%05.5f' % monitor[:render]}"
+            puts "Total time to read, parse and render: #{'%05.5f' % monitor[:load_render]}"
           end
         rescue Exception => e
           raise e if @options[:trace] || SystemExit === e
diff --git a/lib/asciidoctor/cli/options.rb b/lib/asciidoctor/cli/options.rb
index eee4e41..2f0a2e7 100644
--- a/lib/asciidoctor/cli/options.rb
+++ b/lib/asciidoctor/cli/options.rb
@@ -22,7 +22,7 @@ module Asciidoctor
         self[:eruby] = options[:eruby] || nil
         self[:compact] = options[:compact] || false
         self[:verbose] = options[:verbose] || false
-        self[:base_dir] = options[:base_dir] || nil
+        self[:base_dir] = options[:base_dir]
         self[:destination_dir] = options[:destination_dir] || nil
         self[:trace] = false
       end
@@ -44,11 +44,11 @@ Example: asciidoctor -b html5 source.asciidoc
           opts.on('-v', '--verbose', 'enable verbose mode (default: false)') do |verbose|
             self[:verbose] = true
           end
-          opts.on('-b', '--backend BACKEND', ['html5', 'docbook45'], 'set output format (i.e., backend): [html5, docbook45] (default: html5)') do |backend|
+          opts.on('-b', '--backend BACKEND', 'set output format backend (default: html5)') do |backend|
             self[:attributes]['backend'] = backend
           end
-          opts.on('-d', '--doctype DOCTYPE', ['article', 'book'],
-                  'document type to use when rendering output: [article, book] (default: article)') do |doc_type|
+          opts.on('-d', '--doctype DOCTYPE', ['article', 'book', 'inline'],
+                  'document type to use when rendering output: [article, book, inline] (default: article)') do |doc_type|
             self[:attributes]['doctype'] = doc_type
           end
           opts.on('-o', '--out-file FILE', 'output file (default: based on input file path); use - to output to STDOUT') do |output_file|
@@ -78,12 +78,13 @@ Example: asciidoctor -b html5 source.asciidoc
           opts.on('-C', '--compact', 'compact the output by removing blank lines (default: false)') do
             self[:compact] = true
           end
-          opts.on('-a', '--attribute key1=value,key2=value2,...', Array,
-                  'a list of attributes, in the form key or key=value pair, to set on the document',
-                  'these attributes take precedence over attributes defined in the source file') do |attribs|
+          opts.on('-a', '--attribute key[=value],key2[=value2],...', Array,
+                  'a list of document attributes to set in the form of key, key! or key=value pair',
+                  'unless @ is appended to the value, these attributes take precedence over attributes',
+                  'defined in the source document') do |attribs|
             attribs.each do |attrib|
-              tokens = attrib.split('=')
-              self[:attributes][tokens[0]] = tokens[1] || ''
+              key, val = attrib.split '=', 2
+              self[:attributes][key] = val || ''
             end
           end
           opts.on('-T', '--template-dir DIR', 'directory containing custom render templates the override the built-in set') do |template_dir|
@@ -125,8 +126,8 @@ Example: asciidoctor -b html5 source.asciidoc
           if self[:input_file].nil? || self[:input_file].empty?
             $stderr.puts opts_parser
             return 1
-          elsif self[:input_file] != '-' && !File.exist?(self[:input_file])
-            $stderr.puts "asciidoctor: FAILED: input file #{self[:input_file]} missing"
+          elsif self[:input_file] != '-' && !File.readable?(self[:input_file])
+            $stderr.puts "asciidoctor: FAILED: input file #{self[:input_file]} missing or cannot be read"
             return 1
           end
         rescue OptionParser::MissingArgument
diff --git a/lib/asciidoctor/document.rb b/lib/asciidoctor/document.rb
index a02093e..cdf923c 100644
--- a/lib/asciidoctor/document.rb
+++ b/lib/asciidoctor/document.rb
@@ -104,13 +104,14 @@ class Document < AbstractBlock
 
     if options[:parent]
       @parent_document = options.delete(:parent)
-      # should we dup here?
+      # should we dup attributes here?
       options[:attributes] = @parent_document.attributes
-      options[:safe] ||= @parent_document.safe
       options[:base_dir] ||= @parent_document.base_dir
+      @safe = @parent_document.safe
       @renderer = @parent_document.renderer
     else
       @parent_document = nil
+      @safe = nil
     end
 
     @header = nil
@@ -124,14 +125,27 @@ class Document < AbstractBlock
     @counters = {}
     @callouts = Callouts.new
     @options = options
-    @safe = @options.fetch(:safe, SafeMode::SECURE).to_i
+    # safely resolve the safe mode from const, int or string
+    if @safe.nil? && !(safe_mode = @options[:safe])
+      @safe = SafeMode::SECURE
+    elsif safe_mode.is_a?(Fixnum)
+      # be permissive in case API user wants to define new levels
+      @safe = safe_mode
+    else
+      begin
+        @safe = SafeMode.const_get(safe_mode.to_s.upcase).to_i
+      rescue
+        @safe = SafeMode::SECURE.to_i
+      end
+    end
     @options[:header_footer] = @options.fetch(:header_footer, false)
 
-    @attributes['asciidoctor'] = ''
-    @attributes['asciidoctor-version'] = VERSION
-    @attributes['sectids'] = ''
     @attributes['encoding'] = 'UTF-8'
-    @attributes['notitle'] = '' if !@options[:header_footer]
+    @attributes['sectids'] = ''
+    @attributes['notitle'] = '' unless @options[:header_footer]
+    @attributes['toc-placement'] = 'auto'
+    @attributes['stylesheet'] = ''
+    @attributes['linkcss'] = ''
 
     # language strings
     # TODO load these based on language settings
@@ -143,11 +157,26 @@ class Document < AbstractBlock
     @attributes['appendix-caption'] = 'Appendix'
     @attributes['example-caption'] = 'Example'
     @attributes['figure-caption'] = 'Figure'
+    #@attributes['listing-caption'] = 'Listing'
     @attributes['table-caption'] = 'Table'
     @attributes['toc-title'] = 'Table of Contents'
 
+    # attribute overrides are attributes that can only be set from the commandline
+    # a direct assignment effectively makes the attribute a constant
+    # assigning a nil value will result in the attribute being unset
     @attribute_overrides = options[:attributes] || {}
 
+    @attribute_overrides['asciidoctor'] = ''
+    @attribute_overrides['asciidoctor-version'] = VERSION
+
+    safe_mode_name = SafeMode.constants.detect {|l| SafeMode.const_get(l) == @safe}.to_s.downcase
+    @attribute_overrides['safe-mode-name'] = safe_mode_name
+    @attribute_overrides["safe-mode-#{safe_mode_name}"] = ''
+    @attribute_overrides['safe-mode-level'] = @safe
+
+    # sync the embedded attribute w/ the value of options...do not allow override
+    @attribute_overrides['embedded'] = @options[:header_footer] ? nil : ''
+
     # the only way to set the include-depth attribute is via the document options
     # 10 is the AsciiDoc default, though currently Asciidoctor only supports 1 level
     @attribute_overrides['include-depth'] ||= 10
@@ -159,8 +188,8 @@ class Document < AbstractBlock
       if @attribute_overrides['docdir']
         @base_dir = @attribute_overrides['docdir'] = File.expand_path(@attribute_overrides['docdir'])
       else
-        # perhaps issue a warning here?
-        @base_dir = @attribute_overrides['docdir'] = Dir.pwd
+        #puts 'asciidoctor: WARNING: setting base_dir is recommended when working with string documents' unless nested?
+        @base_dir = @attribute_overrides['docdir'] = File.expand_path(Dir.pwd)
       end
     else
       @base_dir = @attribute_overrides['docdir'] = File.expand_path(options[:base_dir])
@@ -168,15 +197,16 @@ class Document < AbstractBlock
 
     # allow common attributes backend and doctype to be set using options hash
     unless @options[:backend].nil?
-      @attribute_overrides['backend'] = @options[:backend]
+      @attribute_overrides['backend'] = @options[:backend].to_s
     end
 
     unless @options[:doctype].nil?
-      @attribute_overrides['doctype'] = @options[:doctype]
+      @attribute_overrides['doctype'] = @options[:doctype].to_s
     end
 
     if @safe >= SafeMode::SERVER
-      # restrict document from setting source-highlighter and backend
+      # restrict document from setting linkcss, copycss, source-highlighter and backend
+      @attribute_overrides['copycss'] ||= nil
       @attribute_overrides['source-highlighter'] ||= nil
       @attribute_overrides['backend'] ||= DEFAULT_BACKEND
       # restrict document from seeing the docdir and trim docfile to relative path
@@ -184,17 +214,24 @@ class Document < AbstractBlock
         @attribute_overrides['docfile'] = @attribute_overrides['docfile'][(@attribute_overrides['docdir'].length + 1)..-1]
       end
       @attribute_overrides['docdir'] = ''
-      # restrict document from enabling icons
       if @safe >= SafeMode::SECURE
+        # assign linkcss (preventing css embedding) unless disabled from the commandline
+        unless @attribute_overrides.fetch('linkcss', '').nil? || @attribute_overrides.has_key?('linkcss!')
+          @attribute_overrides['linkcss'] = ''
+        end
+        # restrict document from enabling icons
         @attribute_overrides['icons'] ||= nil
       end
     end
     
     @attribute_overrides.delete_if {|key, val|
       verdict = false
-      # a nil or negative key undefines the attribute 
-      if val.nil? || key[-1..-1] == '!'
-        @attributes.delete(key.chomp '!')
+      # a nil value undefines the attribute 
+      if val.nil?
+        @attributes.delete(key)
+      # a negative key undefines the attribute
+      elsif key.end_with? '!'
+        @attributes.delete(key[0..-2])
       # otherwise it's an attribute assignment
       else
         # a value ending in @ indicates this attribute does not override
@@ -211,6 +248,12 @@ class Document < AbstractBlock
     @attributes['backend'] ||= DEFAULT_BACKEND
     @attributes['doctype'] ||= DEFAULT_DOCTYPE
     update_backend_attributes
+    # make toc and numbered the default for the docbook backend
+    # FIXME this doesn't take into account the backend being set in the document
+    #if @attributes.has_key?('basebackend-docbook')
+    #  @attributes['toc'] = '' unless @attribute_overrides.has_key?('toc!')
+    #  @attributes['numbered'] = '' unless @attribute_overrides.has_key?('numbered!')
+    #end
 
     if !@parent_document.nil?
       # don't need to do the extra processing within our own document
@@ -230,22 +273,15 @@ class Document < AbstractBlock
     @attributes['docdate'] ||= @attributes['localdate']
     @attributes['doctime'] ||= @attributes['localtime']
     @attributes['docdatetime'] ||= @attributes['localdatetime']
-    
-    @attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', 'images'), 'icons')
+
+    # fallback directories
+    @attributes['stylesdir'] ||= '.'
+    @attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', './images'), 'icons')
 
     # Now parse the lines in the reader into blocks
     Lexer.parse(@reader, self, :header_only => @options.fetch(:parse_header_only, false)) 
 
     @callouts.rewind
-
-    Debug.debug {
-      msg = []
-      msg << "Found #{@blocks.size} blocks in this document:"
-      @blocks.each {|b|
-        msg << b
-      }
-      msg * "\n"
-    }
   end
 
   # Public: Get the named counter and take the next number in the sequence.
@@ -269,6 +305,18 @@ class Document < AbstractBlock
     (@attributes[name] = @counters[name])
   end
 
+  # Public: Increment the specified counter and store it in the block's attributes
+  #
+  # counter_name - the String name of the counter attribute
+  # block        - the Block on which to save the counter
+  #
+  # returns the next number in the sequence for the specified counter
+  def counter_increment(counter_name, block)
+    val = counter(counter_name)
+    AttributeEntry.new(counter_name, val).save_to(block.attributes)
+    val
+  end
+
   # Internal: Get the next value in the sequence.
   #
   # Handles both integer and character sequences.
@@ -318,6 +366,11 @@ class Document < AbstractBlock
     !@parent_document.nil?
   end
 
+  def embedded?
+    # QUESTION should this be !@options[:header_footer] ?
+    @attributes.has_key? 'embedded'
+  end
+
   # Make the raw source for the Document available.
   def source
     @reader.source.join if @reader
@@ -392,10 +445,20 @@ class Document < AbstractBlock
   # Internal: Branch the attributes so that the original state can be restored
   # at a future time.
   def save_attributes
+    unless @attributes.has_key?('doctitle') || (val = doctitle).nil?
+      @attributes['doctitle'] = val
+    end
+
     # css-signature cannot be updated after header attributes are processed
     if @id.nil? && @attributes.has_key?('css-signature')
       @id = @attributes['css-signature']
     end
+
+    if @attributes.has_key? 'toc2'
+      @attributes['toc'] = ''
+      @attributes['toc-class'] ||= 'toc2'
+    end
+
     @original_attributes = @attributes.dup
   end
 
@@ -523,27 +586,6 @@ class Document < AbstractBlock
     @attributes["filetype-#{file_type}"] = ''
   end
 
-  def splain
-    Debug.debug {
-      msg = ''
-      if @header
-        msg = "Header is #{@header}"
-      else
-        msg = "No header"
-      end
-
-      msg += "I have #{@blocks.count} blocks"
-      @blocks.each_with_index do |block, i|
-        msg += "v" * 60
-        msg += "Block ##{i} is a #{block.class}"
-        msg += "Name is #{block.title rescue 'n/a'}"
-        block.splain(0) if block.respond_to? :splain
-        msg += "^" * 60
-      end
-    }
-    nil
-  end
-
   def renderer(opts = {})
     return @renderer if @renderer
     
@@ -572,15 +614,65 @@ class Document < AbstractBlock
   def render(opts = {})
     restore_attributes
     r = renderer(opts)
-    @options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self)
+    if doctype == 'inline'
+      # QUESTION should we warn if @blocks.size > 0 and the first block is not a paragraph?
+      if @blocks.size > 0 && (block = @blocks.first).context == :paragraph
+        block.content
+      else
+        ''
+      end
+    else
+      @options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self)
+    end
   end
 
   def content
-    # per AsciiDoc-spec, remove the title after rendering the header
+    # per AsciiDoc-spec, remove the title before rendering the body,
+    # regardless of whether the header is rendered)
     @attributes.delete('title')
     @blocks.map {|b| b.render }.join
   end
 
+  # Public: Read the docinfo file(s) for inclusion in the
+  # document template
+  #
+  # If the docinfo1 attribute is set, read the docinfo.ext file. If the docinfo
+  # attribute is set, read the doc-name.docinfo.ext file. If the docinfo2
+  # attribute is set, read both files in that order.
+  #
+  # ext - The extension of the docinfo file(s). If not set, the extension
+  #       will be determined based on the basebackend. (default: nil)
+  #
+  # returns The contents of the docinfo file(s)
+  def docinfo(ext = nil)
+    if safe >= SafeMode::SECURE
+      ''
+    else
+      ext = @attributes['outfilesuffix'] if ext.nil?
+
+      content = nil
+
+      docinfo = @attributes.has_key?('docinfo')
+      docinfo1 = @attributes.has_key?('docinfo1')
+      docinfo2 = @attributes.has_key?('docinfo2')
+      docinfo_filename = "docinfo#{ext}"
+      if docinfo1 || docinfo2
+        docinfo_path = normalize_system_path(docinfo_filename)
+        content = read_asset(docinfo_path)
+      end
+
+      if (docinfo || docinfo2) && @attributes.has_key?('docname')
+        docinfo_path = normalize_system_path("#{@attributes['docname']}-#{docinfo_filename}")
+        content2 = read_asset(docinfo_path)
+        unless content2.nil?
+          content = content.nil? ? content2 : "#{content}\n#{content2}"
+        end
+      end
+
+      content.nil? ? '' : content
+    end
+  end
+
   def to_s
     %[#{super.to_s} - #{doctitle}]  
   end
diff --git a/lib/asciidoctor/errors.rb b/lib/asciidoctor/errors.rb
deleted file mode 100644
index acb577b..0000000
--- a/lib/asciidoctor/errors.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Base project exception
-module Asciidoctor
-class ProjectError < StandardError; end
-end
-
diff --git a/lib/asciidoctor/helpers.rb b/lib/asciidoctor/helpers.rb
index 71c4d83..7fbc4fc 100644
--- a/lib/asciidoctor/helpers.rb
+++ b/lib/asciidoctor/helpers.rb
@@ -21,6 +21,32 @@ module Helpers
     require name
   end
 
+  # Public: Encode a string for inclusion in a URI
+  #
+  # str - the string to encode
+  #
+  # returns an encoded version of the str
+  def self.encode_uri(str)
+    str.gsub(REGEXP[:uri_encode_chars]) do
+      match = $&
+      buf = ''
+      match.each_byte do |c|
+        buf << sprintf('%%%02X', c)
+      end
+      buf
+    end
+  end
+
+  def self.mkdir_p(dir)
+    unless File.directory? dir
+      parent_dir = File.dirname(dir)
+      if !File.directory?(parent_dir = File.dirname(dir)) && parent_dir != '.'
+        mkdir_p(parent_dir)
+      end
+      Dir.mkdir(dir)
+    end
+  end
+
   # Public: A generic capture output routine to be used in templates
   #def self.capture_output(*args, &block)
   #  Proc.new { block.call(*args) }
diff --git a/lib/asciidoctor/lexer.rb b/lib/asciidoctor/lexer.rb
index 8f541a4..8602e9b 100644
--- a/lib/asciidoctor/lexer.rb
+++ b/lib/asciidoctor/lexer.rb
@@ -23,7 +23,7 @@ module Asciidoctor
 #   # => Asciidoctor::Block
 class Lexer
 
-  BlockMatchData = Struct.new(:name, :tip, :terminator)
+  BlockMatchData = Struct.new(:context, :masq, :tip, :terminator)
 
   # Public: Make sure the Lexer object doesn't get initialized.
   #
@@ -74,10 +74,33 @@ class Lexer
     # that precede first block
     block_attributes = parse_block_metadata_lines(reader, document)
 
+    # special case, block title is not allowed above document title,
+    # carry attributes over to the document body
+    if block_attributes.has_key?('title')
+      document.clear_playback_attributes block_attributes
+      document.save_attributes
+      block_attributes['invalid-header'] = true
+      return block_attributes
+    end
+
+    # yep, document title logic in AsciiDoc is just insanity
+    # definitely an area for spec refinement
+    assigned_doctitle = nil
+    unless (val = document.attributes.fetch('doctitle', '')).empty?
+      document.title = val
+      assigned_doctitle = val
+    end
+
+    section_title = nil
     # check if the first line is the document title
     # if so, add a header to the document and parse the header metadata
     if is_next_line_document_title?(reader, block_attributes)
-      document.id, document.title, _, _ = parse_section_title(reader)
+      document.id, doctitle, _, _ = parse_section_title(reader, document)
+      unless assigned_doctitle
+        document.title = doctitle
+        assigned_doctitle = doctitle
+      end
+      document.attributes['doctitle'] = section_title = doctitle
       # QUESTION: should this be encapsulated in document?
       if document.id.nil? && block_attributes.has_key?('id')
         document.id = block_attributes.delete('id')
@@ -85,8 +108,15 @@ class Lexer
       parse_header_metadata(reader, document)
     end
 
-    if document.attributes.has_key? 'doctitle'
-      document.title = document.attributes['doctitle']
+    if !(val = document.attributes.fetch('doctitle', '')).empty? &&
+        val != section_title
+      document.title = val
+      assigned_doctitle = val
+    end
+
+    # restore doctitle attribute to original assignment
+    if assigned_doctitle
+      document.attributes['doctitle'] = assigned_doctitle
     end
  
     document.clear_playback_attributes block_attributes
@@ -137,11 +167,13 @@ class Lexer
   def self.next_section(reader, parent, attributes = {})
     preamble = false
 
+    # FIXME if attributes[1] is a verbatim style, then don't check for section
+
     # check if we are at the start of processing the document
     # NOTE we could drop a hint in the attributes to indicate
     # that we are at a section title (so we don't have to check)
     if parent.is_a?(Document) && parent.blocks.empty? &&
-        (parent.has_header? || !is_next_line_section?(reader, attributes))
+        (parent.has_header? || attributes.delete('invalid-header') || !is_next_line_section?(reader, attributes))
 
       if parent.has_header?
         preamble = Block.new(parent, :preamble)
@@ -164,7 +196,13 @@ class Lexer
       # section title to next block of content
       attributes = attributes.delete_if {|k, v| k != 'title'}
       current_level = section.level
-      expected_next_levels = [current_level + 1]
+      # subsections in preface & appendix in multipart books start at level 2
+      if current_level == 0 && section.special &&
+          section.document.doctype == 'book' && ['preface', 'appendix'].include?(section.sectname)
+        expected_next_levels = [current_level + 2]
+      else
+        expected_next_levels = [current_level + 1]
+      end
     end
 
     reader.skip_blank_lines
@@ -183,12 +221,12 @@ class Lexer
 
       next_level = is_next_line_section? reader, attributes
       if next_level
+        next_level += section.document.attr('leveloffset', 0).to_i
         doctype = parent.document.doctype
-        if next_level == 0 && doctype != 'book'
-          puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
-        end
         if next_level > current_level || (section.is_a?(Document) && next_level == 0)
-          unless expected_next_levels.nil? || expected_next_levels.include?(next_level)
+          if next_level == 0 && doctype != 'book'
+            puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
+          elsif !expected_next_levels.nil? && !expected_next_levels.include?(next_level)
             puts "asciidoctor: WARNING: line #{reader.lineno + 1}: section title out of sequence: " +
                 "expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, " +
                 "got level #{next_level}"
@@ -197,12 +235,15 @@ class Lexer
           new_section, attributes = next_section(reader, section, attributes)
           section << new_section
         else
+          if next_level == 0 && doctype != 'book'
+            puts "asciidoctor: ERROR: line #{reader.lineno + 1}: only book doctypes can contain level 0 sections"
+          end
           # close this section (and break out of the nesting) to begin a new one
           break
         end
       else
         # just take one block or else we run the risk of overrunning section boundaries
-        new_block = next_block(reader, section, attributes, :parse_metadata => false)
+        new_block = next_block(reader, (preamble || section), attributes, :parse_metadata => false)
         if !new_block.nil?
           (preamble || section) << new_block
           attributes = {}
@@ -215,8 +256,8 @@ class Lexer
       reader.skip_blank_lines
     end
 
-    # prune the preamble if it has no content
-    if preamble && preamble.blocks.empty?
+    if preamble && !preamble.blocks?
+      # drop the preamble if it has no content
       section.delete_at(0)
     end
 
@@ -241,357 +282,406 @@ class Lexer
   # parent - The Document, Section or Block to which the next block belongs
   # 
   # Returns a Section or Block object holding the parsed content of the processed lines
+  #--
+  # QUESTION should next_block have an option for whether it should keep looking until
+  # a block is found? right now it bails when it encounters a line to be skipped
   def self.next_block(reader, parent, attributes = {}, options = {})
     # Skip ahead to the block content
     skipped = reader.skip_blank_lines
 
-    # bail if we've reached the end of the section content
+    # bail if we've reached the end of the parent block or document
     return nil unless reader.has_more_lines?
 
-    if options[:text] && skipped > 0
+    text_only = options[:text]
+    # check for option to find list item text only
+    # if skipped a line, assume a list continuation was
+    # used and block content is acceptable
+    if text_only && skipped > 0
       options.delete(:text)
+      text_only = false
     end
-
-    Debug.debug {
-      msg = []
-      msg << '/' * 64
-      msg << 'next_block() - First two lines are:'
-      msg.concat reader.peek_lines(2)
-      msg << '/' * 64
-      msg * "\n"
-    }
     
-    parse_metadata = options[:parse_metadata] || true
-    parse_sections = options[:parse_sections] || false
+    parse_metadata = options.fetch(:parse_metadata, true)
+    #parse_sections = options.fetch(:parse_sections, false)
 
     document = parent.document
-    context = parent.is_a?(Block) ? parent.context : nil
+    parent_context = parent.is_a?(Block) ? parent.context : nil
     block = nil
+    style = nil
+    explicit_style = nil
 
     while reader.has_more_lines? && block.nil?
+      # if parsing metadata, read until there is no more to read
       if parse_metadata && parse_block_metadata_line(reader, document, attributes, options)
         reader.advance
         next
-      elsif parse_sections && context.nil? && is_next_line_section?(reader, attributes)
-        block, attributes = next_section(reader, parent, attributes)
-        break
+      #elsif parse_sections && parent_context.nil? && is_next_line_section?(reader, attributes)
+      #  block, attributes = next_section(reader, parent, attributes)
+      #  break
       end
 
+      # QUESTION introduce parsing context object?
       this_line = reader.get_line
-
+      delimited_block = false
       block_context = nil
       terminator = nil
+      # QUESTION put this inside call to rekey attributes?
+      if attributes[1]
+        style, explicit_style = parse_style_attribute(attributes)
+      end
+
       if delimited_blk_match = is_delimited_block?(this_line, true)
-        block_context = delimited_blk_match.name
+        delimited_block = true
+        block_context = delimited_blk_match.context
         terminator = delimited_blk_match.terminator
+        if !style
+          style = attributes['style'] = block_context.to_s
+        elsif style != block_context.to_s
+          if delimited_blk_match.masq.include? style
+            block_context = style.to_sym
+          elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
+            block_context = :admonition
+          else
+            puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for #{block_context} block: #{style}"
+            style = block_context.to_s
+          end
+        end
       end
 
-      # NOTE we're letting break lines (ruler, page_break, etc) have attributes
-      if !options[:text] && block_context.nil? && (match = this_line.match(REGEXP[:break_line]))
-        block = Block.new(parent, BREAK_LINES[match[0][0..2]])
-        reader.skip_blank_lines
-
-      elsif !options[:text] && block_context.nil? && (match = this_line.match(REGEXP[:image_blk]))
-        block = Block.new(parent, :image)
-        AttributeList.new(document.sub_attributes(match[2])).parse_into(attributes, ['alt', 'width', 'height'])
-        target = block.sub_attributes(match[1])
-        if !target.to_s.empty?
-          attributes['target'] = target
-          document.register(:images, target)
-          attributes['alt'] ||= File.basename(target, File.extname(target))
-          block.title = attributes['title']
-          if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
-            number = document.counter('figure-number')
-            attributes['caption'] = "#{document.attributes['figure-caption']} #{number}. "
-            Document::AttributeEntry.new('figure-number', number).save_to(attributes)
+      if !delimited_block
+
+        # this loop only executes once; used for flow control
+        # break once a block is found or at end of loop
+        # returns nil if the line must be dropped
+        # Implementation note - while(true) is twice as fast as loop
+        while true
+
+          # process lines verbatim
+          if !style.nil? && COMPLIANCE[:strict_verbatim_paragraphs] && VERBATIM_STYLES.include?(style)
+            block_context = style.to_sym
+            reader.unshift_line this_line
+            # advance to block parsing =>
+            break
           end
-        else
-          # drop the line if target resolves to nothing
-          block = nil
-        end
-        reader.skip_blank_lines
 
-      elsif block_context == :open
-        # an open block is surrounded by '--' lines and has zero or more blocks inside
-        buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
+          # process lines normally
+          if !text_only
+            # NOTE we're letting break lines (ruler, page_break, etc) have attributes
+            if (match = this_line.match(REGEXP[:break_line]))
+              block = Block.new(parent, BREAK_LINES[match[0][0..2]])
+              break
+
+            # TODO make this a media_blk and handle image, video & audio
+            elsif (match = this_line.match(REGEXP[:media_blk_macro]))
+              blk_ctx = match[1].to_sym
+              block = Block.new(parent, blk_ctx)
+              if blk_ctx == :image
+                posattrs = ['alt', 'width', 'height']
+              elsif blk_ctx == :video
+                posattrs = ['poster', 'width', 'height']
+              else
+                posattrs = []
+              end
 
-        # Strip lines off end of block - not implemented yet
-        # while buffer.has_more_lines? && buffer.last.strip.empty?
-        #   buffer.pop
-        # end
+              unless style.nil? || explicit_style
+                attributes['alt'] = style if blk_ctx == :image
+                attributes.delete('style')
+                style = nil
+              end
 
-        block = Block.new(parent, block_context)
-        while buffer.has_more_lines?
-          new_block = next_block(buffer, block)
-          block.blocks << new_block unless new_block.nil?
-        end
+              block.parse_attributes(match[3], posattrs,
+                  :unescape_input => (blk_ctx == :image),
+                  :sub_input => true,
+                  :sub_result => false,
+                  :into => attributes)
+              target = block.sub_attributes(match[2])
+              if target.empty?
+                # drop the line if target resolves to nothing
+                return nil
+              end
 
-      # needs to come before list detection
-      elsif block_context == :sidebar
-        # sidebar is surrounded by '****' (4 or more '*' chars) lines
-        # FIXME violates DRY because it's a duplication of quote parsing
-        block = Block.new(parent, block_context)
-        buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
+              attributes['target'] = target
+              block.title = attributes.delete('title') if attributes.has_key?('title')
+              if blk_ctx == :image
+                document.register(:images, target)
+                attributes['alt'] ||= File.basename(target, File.extname(target))
+                # QUESTION should video or audio have an auto-numbered caption?
+                block.assign_caption attributes.delete('caption'), 'figure'
+              end
+              break
 
-        while buffer.has_more_lines?
-          new_block = next_block(buffer, block)
-          block.blocks << new_block unless new_block.nil?
-        end
+            # NOTE we're letting the toc macro have attributes
+            elsif (match = this_line.match(REGEXP[:toc]))
+              block = Block.new(parent, :toc)
+              block.parse_attributes(match[1], [], :sub_result => false, :into => attributes)
+              break
 
-      elsif block_context.nil? && (match = this_line.match(REGEXP[:colist]))
-        block = Block.new(parent, :colist)
-        attributes['style'] = 'arabic'
-        items = []
-        block.buffer = items
-        reader.unshift_line this_line
-        expected_index = 1
-        begin
-          # might want to move this check to a validate method
-          if match[1].to_i != expected_index
-            puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}"
-          end
-          list_item = next_list_item(reader, block, match)
-          expected_index += 1
-          if !list_item.nil?
-            items << list_item
-            coids = document.callouts.callout_ids(items.size)
-            if !coids.empty?
-              list_item.attributes['coids'] = coids
-            else
-              puts "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{items.size}"
             end
           end
-        end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist])
-
-        document.callouts.next_list
-
-      elsif block_context.nil? && (match = this_line.match(REGEXP[:ulist]))
-        AttributeList.rekey(attributes, ['style'])
-        reader.unshift_line this_line
-        block = next_outline_list(reader, :ulist, parent)
-
-      elsif block_context.nil? && (match = this_line.match(REGEXP[:olist]))
-        AttributeList.rekey(attributes, ['style'])
-        reader.unshift_line this_line
-        block = next_outline_list(reader, :olist, parent)
-        # QUESTION move this logic to next_outline_list?
-        if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style')
-          marker = block.buffer.first.marker
-          if marker.start_with? '.'
-            # first one makes more sense, but second on is AsciiDoc-compliant
-            #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s
-            attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s
-          else
-            style = ORDERED_LIST_STYLES.detect{|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) }
-            attributes['style'] = (style || ORDERED_LIST_STYLES.first).to_s
-          end
-        end
 
-      elsif block_context.nil? && (match = this_line.match(REGEXP[:dlist]))
-        reader.unshift_line this_line
-        block = next_labeled_list(reader, match, parent)
-        AttributeList.rekey(attributes, ['style'])
-
-      elsif block_context == :table
-        # table is surrounded by lines starting with a | followed by 3 or more '=' chars
-        AttributeList.rekey(attributes, ['style'])
-        table_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
-        block = next_table(table_reader, parent, attributes)
-        block.title = attributes['title']
-        if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
-          number = document.counter('table-number')
-          attributes['caption'] = "#{document.attributes['table-caption']} #{number}. "
-          Document::AttributeEntry.new('table-number', number).save_to(attributes)
-        end
-    
-      # FIXME violates DRY because it's a duplication of other block parsing
-      elsif block_context == :example
-        # example is surrounded by lines with 4 or more '=' chars
-        AttributeList.rekey(attributes, ['style'])
-        if admonition_style = ADMONITION_STYLES.detect {|s| attributes['style'] == s}
-          block = Block.new(parent, :admonition)
-          attributes['name'] = admonition_name = admonition_style.downcase
-          attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
-        else
-          block = Block.new(parent, block_context)
-          block.title = attributes['title']
-          if block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
-            number = document.counter('example-number')
-            attributes['caption'] = "#{document.attributes['example-caption']} #{number}. "
-            Document::AttributeEntry.new('example-number', number).save_to(attributes)
-          end
-        end
-        buffer = Reader.new reader.grab_lines_until(:terminator => terminator)
+          # haven't found anything yet, continue
+          if (match = this_line.match(REGEXP[:colist]))
+            block = Block.new(parent, :colist)
+            attributes['style'] = 'arabic'
+            items = []
+            block.buffer = items
+            reader.unshift_line this_line
+            expected_index = 1
+            begin
+              # might want to move this check to a validate method
+              if match[1].to_i != expected_index
+                puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}"
+              end
+              list_item = next_list_item(reader, block, match)
+              expected_index += 1
+              if !list_item.nil?
+                items << list_item
+                coids = document.callouts.callout_ids(items.size)
+                if !coids.empty?
+                  list_item.attributes['coids'] = coids
+                else
+                  puts "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{items.size}"
+                end
+              end
+            end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist])
 
-        while buffer.has_more_lines?
-          new_block = next_block(buffer, block)
-          block.blocks << new_block unless new_block.nil?
-        end
+            document.callouts.next_list
+            break
 
-      # FIXME violates DRY w/ non-delimited block listing
-      elsif block_context == :listing || block_context == :fenced_code
-        if block_context == :fenced_code
-          attributes['style'] = 'source'
-          lang = this_line[3..-1].strip
-          attributes['language'] = lang unless lang.empty?
-          terminator = terminator[0..2] if terminator.length > 3
-        else
-          AttributeList.rekey(attributes, ['style', 'language', 'linenums'])
-        end
-        buffer = reader.grab_lines_until(:terminator => terminator)
-        buffer.last.chomp! unless buffer.empty?
-        block = Block.new(parent, :listing, buffer)
-        block.title = attributes['title']
-        if document.attributes.has_key?('listing-caption') &&
-            block.title? && !attributes.has_key?('caption') && !block.attr?('caption')
-          number = document.counter('listing-number')
-          attributes['caption'] = "#{document.attributes['listing-caption']} #{number}. "
-          Document::AttributeEntry.new('listing-number', number).save_to(attributes)
-        end
+          elsif (match = this_line.match(REGEXP[:ulist]))
+            reader.unshift_line this_line
+            block = next_outline_list(reader, :ulist, parent)
+            break
 
-      elsif block_context == :quote
-        # multi-line verse or quote is surrounded by a block delimiter
-        AttributeList.rekey(attributes, ['style', 'attribution', 'citetitle'])
-        quote_context = (attributes['style'] == 'verse' ? :verse : :quote)
-        block_reader = Reader.new reader.grab_lines_until(:terminator => terminator)
+          elsif (match = this_line.match(REGEXP[:olist]))
+            reader.unshift_line this_line
+            block = next_outline_list(reader, :olist, parent)
+            # QUESTION move this logic to next_outline_list?
+            if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style')
+              marker = block.buffer.first.marker
+              if marker.start_with? '.'
+                # first one makes more sense, but second on is AsciiDoc-compliant
+                #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s
+                attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s
+              else
+                style = ORDERED_LIST_STYLES.detect{|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) }
+                attributes['style'] = (style || ORDERED_LIST_STYLES.first).to_s
+              end
+            end
+            break
 
-        # only quote can have other section elements (as section block)
-        section_body = (quote_context == :quote)
+          elsif (match = this_line.match(REGEXP[:dlist]))
+            reader.unshift_line this_line
+            block = next_labeled_list(reader, match, parent)
+            break
 
-        if section_body
-          block = Block.new(parent, quote_context)
-          while block_reader.has_more_lines?
-            new_block = next_block(block_reader, block)
-            block.blocks << new_block unless new_block.nil?
+          elsif (style == 'float' || style == 'discrete') && is_section_title?(this_line, reader.peek_line)
+            reader.unshift_line this_line
+            float_id, float_title, float_level, _ = parse_section_title(reader, document)
+            float_id ||= attributes['id'] if attributes.has_key?('id')
+            block = Block.new(parent, :floating_title)
+            if float_id.nil? || float_id.empty?
+              # FIXME remove hack of creating throwaway Section to get at the generate_id method
+              tmp_sect = Section.new(parent)
+              tmp_sect.title = float_title
+              block.id = tmp_sect.generate_id
+            else
+              block.id = float_id
+            end
+            document.register(:ids, [block.id, float_title]) if block.id
+            block.level = float_level
+            block.title = float_title
+            break
+
+          # FIXME create another set for "passthrough" styles
+          elsif !style.nil? && style != 'normal'
+            if PARAGRAPH_STYLES.include?(style)
+              block_context = style.to_sym
+              reader.unshift_line this_line
+              # advance to block parsing =>
+              break
+            elsif ADMONITION_STYLES.include?(style)
+              block_context = :admonition
+              reader.unshift_line this_line
+              # advance to block parsing =>
+              break
+            else
+              puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for paragraph: #{style}"
+              style = nil
+              # continue to process paragraph
+            end
           end
-        else
-          block_reader.chomp_last!
-          block = Block.new(parent, quote_context, block_reader.lines)
-        end
 
-      elsif block_context == :literal || block_context == :pass
-        # literal is surrounded by '....' (4 or more '.' chars) lines
-        # pass is surrounded by '++++' (4 or more '+' chars) lines
-        buffer = reader.grab_lines_until(:terminator => terminator)
-        buffer.last.chomp! unless buffer.empty?
-        # a literal can masquerade as a listing
-        if attributes[1] == 'listing'
-          block_context = :listing
-        end
-        block = Block.new(parent, block_context, buffer)
-
-      elsif this_line.match(REGEXP[:lit_par])
-        # literal paragraph is contiguous lines starting with
-        # one or more space or tab characters
-
-        # So we need to actually include this one in the grab_lines group
-        reader.unshift_line this_line
-        buffer = reader.grab_lines_until(:preserve_last_line => true, :break_on_blank_lines => true) {|line|
-          # labeled list terms can be indented, but a preceding blank indicates
-          # we are in a list continuation and therefore literals should be strictly literal
-          (context == :dlist && skipped == 0 && line.match(REGEXP[:dlist])) ||
-          is_delimited_block?(line)
-        }
+          break_at_list = (skipped == 0 && parent_context.to_s.end_with?('list'))
 
-        # trim off the indentation equivalent to the size of the least indented line
-        if !buffer.empty?
-          offset = buffer.map {|line| line.match(REGEXP[:leading_blanks])[1].length }.min
-          if offset > 0
-            buffer = buffer.map {|l| l.sub(/^\s{1,#{offset}}/, '') }
-          end
-          buffer.last.chomp!
-        end
+          # a literal paragraph is contiguous lines starting at least one space
+          if style != 'normal' && this_line.match(REGEXP[:lit_par])
+            # So we need to actually include this one in the grab_lines group
+            reader.unshift_line this_line
+            buffer = reader.grab_lines_until(
+                :break_on_blank_lines => true,
+                :break_on_list_continuation => true,
+                :preserve_last_line => true) {|line|
+              # a preceding blank line (skipped > 0) indicates we are in a list continuation
+              # and therefore we should not break at a list item
+              # (this won't stop breaking on item of same level since we've already parsed them out)
+              # QUESTION can we turn this block into a lambda or function call?
+              (break_at_list && line.match(REGEXP[:any_list])) ||
+              (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
+            }
 
-        block = Block.new(parent, :literal, buffer)
-        # a literal gets special meaning inside of a definition list
-        if LIST_CONTEXTS.include?(context)
-          attributes['options'] ||= []
-          # TODO this feels hacky, better way to distinguish from explicit literal block?
-          attributes['options'] << 'listparagraph'
-        end
+            reset_block_indent! buffer
 
-      ## these switches based on style need to come immediately before the else ##
+            block = Block.new(parent, :literal, buffer)
+            # a literal gets special meaning inside of a definition list
+            if LIST_CONTEXTS.include?(parent_context)
+              attributes['options'] ||= []
+              # TODO this feels hacky, better way to distinguish from explicit literal block?
+              attributes['options'] << 'listparagraph'
+            end
 
-      elsif attributes[1] == 'source' || attributes[1] == 'listing'
-        if attributes[1] == 'source'
-          AttributeList.rekey(attributes, ['style', 'language', 'linenums'])
-        end
-        reader.unshift_line this_line
-        buffer = reader.grab_lines_until(:break_on_blank_lines => true)
-        buffer.last.chomp! unless buffer.empty?
-        block = Block.new(parent, :listing, buffer)
-
-      elsif attributes[1] == 'literal'
-        reader.unshift_line this_line
-        buffer = reader.grab_lines_until(:break_on_blank_lines => true)
-        buffer.last.chomp! unless buffer.empty?
-        block = Block.new(parent, :literal, buffer)
-
-      elsif admonition_style = ADMONITION_STYLES.detect{|s| attributes[1] == s}
-        # an admonition preceded by [<TYPE>] and lasts until a blank line
-        reader.unshift_line this_line
-        buffer = reader.grab_lines_until(:break_on_blank_lines => true)
-        buffer.last.chomp! unless buffer.empty?
-        block = Block.new(parent, :admonition, buffer)
-        attributes['style'] = admonition_style
-        attributes['name'] = admonition_name = admonition_style.downcase
-        attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
-
-      elsif quote_context = [:quote, :verse].detect{|s| attributes[1] == s.to_s}
-        # single-paragraph verse or quote is preceded by [verse] or [quote], respectively, and lasts until a blank line
-        AttributeList.rekey(attributes, ['style', 'attribution', 'citetitle'])
-        reader.unshift_line this_line
-        buffer = reader.grab_lines_until(:break_on_blank_lines => true)
-        buffer.last.chomp! unless buffer.empty?
-        block = Block.new(parent, quote_context, buffer)
-
-      # a floating (i.e., discrete) title
-      elsif ['float', 'discrete'].include?(attributes[1]) && is_section_title?(this_line, reader.peek_line)
-        attributes['style'] = attributes[1]
-        reader.unshift_line this_line
-        float_id, float_title, float_level, _ = parse_section_title reader
-        block = Block.new(parent, :floating_title)
-        if float_id.nil? || float_id.empty?
-          # FIXME remove hack of creating throwaway Section to get at the generate_id method
-          tmp_sect = Section.new(parent)
-          tmp_sect.title = float_title
-          block.id = tmp_sect.generate_id
-        else
-          block.id = float_id
-          @document.register(:ids, [float_id, float_title])
-        end
-        block.level = float_level
-        block.title = float_title
+          # a paragraph is contiguous nonblank/noncontinuation lines
+          else
+            reader.unshift_line this_line
+            buffer = reader.grab_lines_until(
+                :break_on_blank_lines => true,
+                :break_on_list_continuation => true,
+                :preserve_last_line => true,
+                :skip_line_comments => true) {|line|
+              # a preceding blank line (skipped > 0) indicates we are in a list continuation
+              # and therefore we should not break at a list item
+              # (this won't stop breaking on item of same level since we've already parsed them out)
+              # QUESTION can we turn this block into a lambda or function call?
+              (break_at_list && line.match(REGEXP[:any_list])) ||
+              (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
+            }
 
-      # a paragraph - contiguous nonblank/noncontinuation lines
-      else
-        reader.unshift_line this_line
-        buffer = reader.grab_lines_until(:break_on_blank_lines => true, :preserve_last_line => true, :skip_line_comments => true) {|line|
-          is_delimited_block?(line) || line.match(REGEXP[:attr_line]) ||
-          # next list item can be directly adjacent to paragraph of previous list item
-          context == :dlist && line.match(REGEXP[:dlist])
-          # not sure if there are any cases when we need this check for other list types
-          #LIST_CONTEXTS.include?(context) && line.match(REGEXP[context])
-        }
+            # NOTE we need this logic because we've asked the reader to skip
+            # line comments, which may leave us w/ an empty buffer if those
+            # were the only lines found
+            if buffer.empty?
+              # call get_line since the reader preserved the last line
+              reader.get_line
+              return nil
+            end
+
+            catalog_inline_anchors(buffer.join, document)
+
+            first_line = buffer.first
+            if !text_only && (admonition_match = first_line.match(REGEXP[:admonition_inline]))
+              buffer[0] = admonition_match.post_match.lstrip
+              block = Block.new(parent, :admonition, buffer)
+              attributes['style'] = admonition_match[1]
+              attributes['name'] = admonition_name = admonition_match[1].downcase
+              attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
+            elsif !text_only && COMPLIANCE[:markdown_syntax] && first_line.start_with?('> ')
+              buffer.map! {|line|
+                if line.start_with?('> ')
+                  line[2..-1]
+                elsif line.chomp == '>'
+                  line[1..-1]
+                else
+                  line
+                end
+              }
+
+              if buffer.last.start_with?('-- ')
+                attribution, citetitle = buffer.pop[3..-1].split(', ')
+                buffer.pop while buffer.last.chomp.empty?
+                buffer[-1] = buffer.last.chomp
+              else
+                attribution, citetitle = nil
+              end
+              attributes['style'] = 'quote'
+              attributes['attribution'] = attribution unless attribution.nil?
+              attributes['citetitle'] = citetitle unless citetitle.nil?
+              # NOTE will only detect headings that are floating titles (not section titles)
+              # TODO could assume a floating title when inside a block context
+              block = build_block(:quote, :complex, false, parent, Reader.new(buffer), attributes)
+            elsif !text_only && buffer.size > 1 && first_line.start_with?('"') &&
+                buffer.last.start_with?('-- ') && buffer[-2].chomp.end_with?('"')
+              buffer[0] = first_line[1..-1]
+              attribution, citetitle = buffer.pop[3..-1].split(', ')
+              buffer.pop while buffer.last.chomp.empty?
+              buffer[-1] = buffer.last.chomp.chop
+              attributes['style'] = 'quote'
+              attributes['attribution'] = attribution unless attribution.nil?
+              attributes['citetitle'] = citetitle unless citetitle.nil?
+              block = Block.new(parent, :quote, buffer)
+              #block = Block.new(parent, :quote)
+              #block << Block.new(block, :paragraph, buffer)
+            else
+              # QUESTION is this necessary?
+              #if style == 'normal' && [' ', "\t"].include?(buffer.first[0..0])
+              #  # QUESTION should we only trim leading blanks?
+              #  buffer.map! &:lstrip
+              #end
 
-        # NOTE we need this logic because the reader is processing line
-        # comments and that might leave us w/ an empty buffer
-        if buffer.empty?
-          reader.get_line
+              block = Block.new(parent, :paragraph, buffer)
+            end
+          end
+
+          # forbid loop from executing more than once
           break
         end
+      end
 
-        catalog_inline_anchors(buffer.join, document)
+      # either delimited block or styled paragraph
+      if block.nil? && !block_context.nil?
+        # abstract and partintro should be handled by open block
+        # FIXME kind of hackish...need to sort out how to generalize this
+        block_context = :open if block_context == :abstract || block_context == :partintro
 
-        if !options[:text] && (admonition = buffer.first.match(Regexp.new('^(' + ADMONITION_STYLES.join('|') + '):\s+')))
-          buffer[0] = admonition.post_match
-          block = Block.new(parent, :admonition, buffer)
-          attributes['style'] = admonition[1]
-          attributes['name'] = admonition_name = admonition[1].downcase
+        case block_context
+        when :admonition
+          attributes['name'] = admonition_name = style.downcase
           attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
+          block = build_block(block_context, :complex, terminator, parent, reader, attributes)
+
+        when :comment
+          reader.grab_lines_until(:break_on_blank_lines => true, :chomp_last_line => false)
+          return nil
+
+        when :example
+          block = build_block(block_context, :complex, terminator, parent, reader, attributes, {:supports_caption => true})
+
+        when :listing, :fenced_code, :source
+          if block_context == :fenced_code
+            style = attributes['style'] = 'source'
+            lang = this_line[3..-1].strip
+            attributes['language'] = lang unless lang.empty?
+            terminator = terminator[0..2] if terminator.length > 3
+          elsif block_context == :source
+            AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
+          end
+          block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, {:supports_caption => true})
+
+        when :literal
+          block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
+        
+        when :pass
+          block = build_block(block_context, :simple, terminator, parent, reader, attributes)
+
+        when :open, :sidebar
+          block = build_block(block_context, :complex, terminator, parent, reader, attributes)
+
+        when :table
+          block_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
+          case terminator[0..0]
+            when ','
+              attributes['format'] = 'csv'
+            when ':'
+              attributes['format'] = 'dsv'
+          end
+          block = next_table(block_reader, parent, attributes)
+
+        when :quote, :verse
+          AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
+          block = build_block(block_context, (block_context == :verse ? :verbatim : :complex), terminator, parent, reader, attributes)
+
         else
-          buffer.last.chomp!
-          block = Block.new(parent, :paragraph, buffer)
+          # this should only happen if there is a misconfiguration
+          raise "Unsupported block type #{block_context} at line #{reader.lineno}"
         end
       end
     end
@@ -599,10 +689,12 @@ class Lexer
     # when looking for nested content, one or more line comments, comment
     # blocks or trailing attribute lists could leave us without a block,
     # so handle accordingly
+    # REVIEW we may no longer need this check
     if !block.nil?
-      block.id        = attributes['id'] if attributes.has_key?('id')
+      # REVIEW seems like there is a better way to organize this wrap-up
+      block.id      ||= attributes['id'] if attributes.has_key?('id')
       block.title     = attributes['title'] unless block.title?
-      block.caption ||= attributes['caption'] unless block.is_a?(Section)
+      block.caption ||= attributes.delete('caption')
       # AsciiDoc always use [id] as the reftext in HTML output,
       # but I'd like to do better in Asciidoctor
       if block.id && block.title? && !attributes.has_key?('reftext')
@@ -632,20 +724,34 @@ class Lexer
         tip = line[0..3]
         tl = 4
 
-        # special case for fenced code blocks
-        tip_alt = tip.chop
-        if tip_alt == '```' || tip_alt == '~~~'
-          tip = tip_alt
-          tl = 3
+        if COMPLIANCE[:markdown_syntax]
+          # special case for fenced code blocks
+          tip_alt = tip.chop
+          if tip_alt == '```' || tip_alt == '~~~'
+            tip = tip_alt
+            tl = 3
+          end
         end
       end
 
       if DELIMITED_BLOCKS.has_key? tip
         # if tip is the full line
         if tl == line_len - 1
-          return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, tip) : true
+          #return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, tip) : true
+          if return_match_data
+            context, masq = *DELIMITED_BLOCKS[tip]
+            BlockMatchData.new(context, masq, tip, tip)
+          else
+            true
+          end
         elsif match = line.match(REGEXP[:any_blk])
-          return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, match[0]) : true
+          #return_match_data ? BlockMatchData.new(DELIMITED_BLOCKS[tip], tip, match[0]) : true
+          if return_match_data
+            context, masq = *DELIMITED_BLOCKS[tip]
+            BlockMatchData.new(context, masq, tip, match[0])
+          else
+            true
+          end
         else
           nil
         end
@@ -657,6 +763,56 @@ class Lexer
     end
   end
 
+  # whether a block supports complex content should be a config setting
+  # if terminator is false, that means the all the lines in the reader should be parsed
+  # NOTE could invoke filter in here, before and after parsing
+  def self.build_block(block_context, content_type, terminator, parent, reader, attributes, options = {})
+    if terminator.nil?
+      if content_type == :verbatim
+        buffer = reader.grab_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true)
+      else
+        buffer = reader.grab_lines_until(
+            :break_on_blank_lines => true,
+            :break_on_list_continuation => true,
+            :preserve_last_line => true,
+            :skip_line_comments => true) {|line|
+          COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line]))
+        }
+        # QUESTION check for empty buffer?
+      end
+    elsif content_type != :complex
+      buffer = reader.grab_lines_until(:terminator => terminator, :chomp_last_line => true)
+    elsif terminator == false
+      buffer = nil
+      block_reader = reader
+    else
+      buffer = nil
+      block_reader = Reader.new reader.grab_lines_until(:terminator => terminator)
+    end
+
+    if content_type == :verbatim && attributes.has_key?('indent')
+      reset_block_indent! buffer, attributes['indent'].to_i
+    end
+
+    block = Block.new(parent, block_context, buffer)
+    # should supports_caption be necessary?
+    if options.fetch(:supports_caption, false)
+      block.title = attributes.delete('title') if attributes.has_key?('title')
+      block.assign_caption attributes.delete('caption')
+    end
+
+    if buffer.nil?
+      # we can look for blocks until there are no more lines (and not worry
+      # about sections) since the reader is confined within the boundaries of a
+      # delimited block
+      while block_reader.has_more_lines?
+        parsed_block = next_block(block_reader, block)
+        block.blocks << parsed_block unless parsed_block.nil?
+      end
+    end
+    block
+  end
+
   # Internal: Parse and construct an outline list Block from the current position of the Reader
   #
   # reader    - The Reader from which to retrieve the outline list
@@ -744,9 +900,9 @@ class Lexer
       m = $~
       next if m[0].start_with? '\\'
       id, reftext = m[1].split(',')
-      id.sub!(/^("|)(.*)\1$/, '\2')
+      id.sub!(REGEXP[:dbl_quoted], '\2')
       if !reftext.nil?
-        reftext.sub!(/^("|)(.*)\1$/m, '\2')
+        reftext.sub!(REGEXP[:m_dbl_quoted], '\2')
       end
       document.register(:ids, [id, reftext])
     }
@@ -834,6 +990,9 @@ class Lexer
       # only relevant for :dlist
       options = {:text => !has_text}
 
+      # we can look for blocks until there are no more lines (and not worry
+      # about sections) since the reader is confined within the boundaries of a
+      # list
       while list_item_reader.has_more_lines?
         new_block = next_block(list_item_reader, list_block, {}, options)
         list_item.blocks << new_block unless new_block.nil?
@@ -898,7 +1057,7 @@ class Lexer
         if continuation == :inactive
           continuation = :active
           has_text = true
-          buffer[buffer.size - 1] = "\n" unless within_nested_list
+          buffer[-1] = "\n" unless within_nested_list
         end
 
         # dealing with adjacent list continuations (which is really a syntax error)
@@ -937,12 +1096,12 @@ class Lexer
           if this_line.match(REGEXP[:lit_par])
             reader.unshift_line this_line
             buffer.concat reader.grab_lines_until(
-              :preserve_last_line => true,
-              :break_on_blank_lines => true,
-              :break_on_list_continuation => true) {|line|
-                # we may be in an indented list disguised as a literal paragraph
-                # so we need to make sure we don't slurp up a legitimate sibling
-                list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
+                :preserve_last_line => true,
+                :break_on_blank_lines => true,
+                :break_on_list_continuation => true) {|line|
+              # we may be in an indented list disguised as a literal paragraph
+              # so we need to make sure we don't slurp up a legitimate sibling
+              list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
             }
             continuation = :inactive
           # let block metadata play out until we find the block
@@ -980,13 +1139,13 @@ class Lexer
               if this_line.match(REGEXP[:lit_par])
                 reader.unshift_line this_line
                 buffer.concat reader.grab_lines_until(
-                  :preserve_last_line => true,
-                  :break_on_blank_lines => true,
-                  :break_on_list_continuation => true) {|line|
-                    # we may be in an indented list disguised as a literal paragraph
-                    # so we need to make sure we don't slurp up a legitimate sibling
-                    list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
-                  }
+                    :preserve_last_line => true,
+                    :break_on_blank_lines => true,
+                    :break_on_list_continuation => true) {|line|
+                  # we may be in an indented list disguised as a literal paragraph
+                  # so we need to make sure we don't slurp up a legitimate sibling
+                  list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait)
+                }
               # TODO any way to combine this with the check after skipping blank lines?
               elsif is_sibling_list_item?(this_line, list_type, sibling_trait)
                 break
@@ -1038,7 +1197,7 @@ class Lexer
     end
 
     #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.join}<BUFFER"
-    #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer}<BUFFER"
+    #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.inspect}<BUFFER"
 
     buffer
   end
@@ -1053,29 +1212,40 @@ class Lexer
   # attributes - a Hash of attributes to assign to this section (default: {})
   def self.initialize_section(reader, parent, attributes = {})
     section = Section.new parent
-    section.id, section.title, section.level, _ = parse_section_title(reader)
-    if section.id.nil? && attributes.has_key?('id')
-      section.id = attributes['id']
-    else
-      # generate an id if one was not *embedded* in the heading line
-      # or as an anchor above the section
-      section.id ||= section.generate_id
-    end
-
+    section.id, section.title, section.level, _ = parse_section_title(reader, section.document)
+    # parse style, id and role from first positional attribute
     if attributes[1]
-      section.sectname = attributes[1]
+      section.sectname, _ = parse_style_attribute(attributes)
       section.special = true
       document = parent.document
-      if section.sectname == 'appendix' &&
+      # HACK needs to be refactored so it's driven by config
+      if section.sectname == 'abstract' && document.doctype == 'book'
+        section.sectname = "sect1"
+        section.special = false
+        section.level = 1
+      # FIXME refactor to use assign_caption (also check requirements)
+      elsif section.sectname == 'appendix' &&
           !attributes.has_key?('caption') &&
           !document.attributes.has_key?('caption')
         number = document.counter('appendix-number', 'A')
-        attributes['caption'] = "#{document.attributes['appendix-caption']} #{number}: "
+        section.caption = "#{document.attributes['appendix-caption']} #{number}: "
         Document::AttributeEntry.new('appendix-number', number).save_to(attributes)
       end
     else
       section.sectname = "sect#{section.level}"
     end
+
+    if section.id.nil? && (id = attributes['id'])
+      section.id = id
+    else
+      # generate an id if one was not *embedded* in the heading line
+      # or as an anchor above the section
+      section.id ||= section.generate_id
+    end
+
+    if section.id
+      section.document.register(:ids, [section.id, section.title])
+    end
     section.update_attributes(attributes)
     reader.skip_blank_lines
 
@@ -1087,20 +1257,13 @@ class Lexer
   #
   # line - the String line from under the section title.
   def self.section_level(line)
-    char = line.chomp.chars.to_a.uniq
-    case char
-    when ['=']; 0
-    when ['-']; 1
-    when ['~']; 2
-    when ['^']; 3
-    when ['+']; 4
-    end
+    SECTION_LEVELS[line[0..0]]
   end
 
   #--
   # = is level 0, == is level 1, etc.
-  def self.single_line_section_level(line)
-    [line.length - 1, 0].max
+  def self.single_line_section_level(marker)
+    marker.length - 1
   end
 
   # Internal: Checks if the next line on the Reader is a section title
@@ -1111,7 +1274,7 @@ class Lexer
   # returns the section level if the Reader is positioned at a section title,
   # false otherwise
   def self.is_next_line_section?(reader, attributes)
-    return false if !attributes[1].nil? && ['float', 'discrete'].include?(attributes[1])
+    return false if !(val = attributes[1]).nil? && ['float', 'discrete'].include?(val)
     return false if !reader.has_more_lines?
     is_section_title?(*reader.peek_lines(2))
   end
@@ -1144,7 +1307,8 @@ class Lexer
   end
 
   def self.is_single_line_section_title?(line1)
-    if !line1.nil? && (match = line1.match(REGEXP[:section_title]))
+    if !line1.nil? && (line1.start_with?('=') || (COMPLIANCE[:markdown_syntax] && line1.start_with?('#'))) &&
+        (match = line1.match(REGEXP[:section_title]))
       single_line_section_level match[1]
     else
       false
@@ -1152,8 +1316,8 @@ class Lexer
   end
 
   def self.is_two_line_section_title?(line1, line2)
-    if !line1.nil? && !line2.nil? && line1.match(REGEXP[:section_name]) &&
-        line2.match(REGEXP[:section_underline]) &&
+    if !line1.nil? && !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) &&
+        line2.match(REGEXP[:section_underline]) && line1.match(REGEXP[:section_name]) &&
         # chomp so that a (non-visible) endline does not impact calculation
         (line1.chomp.size - line2.chomp.size).abs <= 1
       section_level line2
@@ -1168,13 +1332,14 @@ class Lexer
   # the Reader will be positioned at the line after the section title.
   #
   # reader  - the source reader, positioned at a section title
+  # document- the current document
   #
   # Examples
   #
   #   reader.lines
   #   # => ["Foo\n", "~~~\n"]
   #
-  #   title, level, id, single = parse_section_title(reader)
+  #   title, level, id, single = parse_section_title(reader, document)
   #
   #   title
   #   # => "Foo"
@@ -1188,7 +1353,7 @@ class Lexer
   #   line1
   #   # => "==== Foo\n"
   #
-  #   title, level, id, single = parse_section_title(reader)
+  #   title, level, id, single = parse_section_title(reader, document)
   #
   #   title
   #   # => "Foo"
@@ -1204,21 +1369,22 @@ class Lexer
   #
   #--
   # NOTE for efficiency, we don't reuse methods that check for a section title
-  def self.parse_section_title(reader)
+  def self.parse_section_title(reader, document)
     line1 = reader.get_line
     sect_id = nil
     sect_title = nil
     sect_level = -1
     single_line = true
 
-    if match = line1.match(REGEXP[:section_title])
+    if (line1.start_with?('=') || (COMPLIANCE[:markdown_syntax] && line1.start_with?('#'))) &&
+        (match = line1.match(REGEXP[:section_title]))
       sect_id = match[3]
       sect_title = match[2]
       sect_level = single_line_section_level match[1]
     else
       line2 = reader.peek_line
-      if !line2.nil? && (name_match = line1.match(REGEXP[:section_name])) &&
-        line2.match(REGEXP[:section_underline]) &&
+      if !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) && line2.match(REGEXP[:section_underline]) &&
+        (name_match = line1.match(REGEXP[:section_name])) &&
         # chomp so that a (non-visible) endline does not impact calculation
         (line1.chomp.size - line2.chomp.size).abs <= 1
         if anchor_match = name_match[1].match(REGEXP[:anchor_embedded]) 
@@ -1232,7 +1398,10 @@ class Lexer
         reader.get_line
       end
     end
-    return [sect_id, sect_title, sect_level, single_line]
+    if sect_level >= 0
+      sect_level += document.attr('leveloffset', 0).to_i
+    end
+    [sect_id, sect_title, sect_level, single_line]
   end
 
   # Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info).
@@ -1253,55 +1422,97 @@ class Lexer
     process_attribute_entries(reader, document)
 
     metadata = {}
+    implicit_author = nil
+    implicit_authors = nil
 
     if reader.has_more_lines? && !reader.peek_line.chomp.empty?
-      author_line = reader.get_line
-      if match = author_line.match(REGEXP[:author_info])
-        metadata['firstname'] = fname = match[1].tr('_', ' ')
-        metadata['author'] = fname
-        metadata['authorinitials'] = fname[0, 1]
-        if !match[2].nil? && !match[3].nil?
-          metadata['middlename'] = mname = match[2].tr('_', ' ')
-          metadata['lastname'] = lname = match[3].tr('_', ' ')
-          metadata['author'] = [fname, mname, lname].join ' '
-          metadata['authorinitials'] = [fname[0, 1], mname[0, 1], lname[0, 1]].join
-        elsif !match[2].nil?
-          metadata['lastname'] = lname = match[2].tr('_', ' ')
-          metadata['author'] = [fname, lname].join ' '
-          metadata['authorinitials'] = [fname[0, 1], lname[0, 1]].join
+      author_metadata = process_authors reader.get_line
+
+      unless author_metadata.empty?
+        # apply header subs and assign to document
+        if !document.nil?
+          author_metadata.map do |key, val|
+            val = val.is_a?(String) ? document.apply_header_subs(val) : val
+            document.attributes[key] = val if !document.attributes.has_key?(key)
+            val
+          end
+
+          implicit_author = document.attributes['author']
+          implicit_authors = document.attributes['authors']
         end
-        metadata['email'] = match[4] unless match[4].nil?
-      else
-        metadata['author'] = metadata['firstname'] = author_line.strip.squeeze(' ')
-        metadata['authorinitials'] = metadata['firstname'][0, 1]
+
+        metadata = author_metadata
       end
 
-      # NOTE this will discard away any comment lines, but not skip blank lines
+      # NOTE this will discard any comment lines, but not skip blank lines
       process_attribute_entries(reader, document)
 
+      rev_metadata = {}
+
       if reader.has_more_lines? && !reader.peek_line.chomp.empty?
         rev_line = reader.get_line 
         if match = rev_line.match(REGEXP[:revision_info])
-          metadata['revdate'] = match[2].strip
-          metadata['revnumber'] = match[1].rstrip unless match[1].nil?
-          metadata['revremark'] = match[3].rstrip unless match[3].nil?
+          rev_metadata['revdate'] = match[2].strip
+          rev_metadata['revnumber'] = match[1].rstrip unless match[1].nil?
+          rev_metadata['revremark'] = match[3].rstrip unless match[3].nil?
         else
           # throw it back
           reader.unshift_line rev_line
         end
       end
 
-      # NOTE this will discard away any comment lines, but not skip blank lines
+      unless rev_metadata.empty?
+        # apply header subs and assign to document
+        if !document.nil?
+          rev_metadata.map do |key, val|
+            val = document.apply_header_subs(val)
+            document.attributes[key] = val if !document.attributes.has_key?(key)
+            val
+          end
+        end
+
+        metadata.update rev_metadata
+      end
+
+      # NOTE this will discard any comment lines, but not skip blank lines
       process_attribute_entries(reader, document)
 
       reader.skip_blank_lines
+    end
+
+    if !document.nil?
+      # process author attribute entries that override (or stand in for) the implicit author line
+      author_metadata = nil
+      if document.attributes.has_key?('author') &&
+          (author_line = document.attributes['author']) != implicit_author
+        # do not allow multiple, process as names only
+        author_metadata = process_authors author_line, true, false
+      elsif document.attributes.has_key?('authors') &&
+          (author_line = document.attributes['authors']) != implicit_authors
+        # allow multiple, process as names only
+        author_metadata = process_authors author_line, true
+      else
+        authors = []
+        author_key = "author_#{authors.size + 1}"
+        while document.attributes.has_key? author_key
+          authors << document.attributes[author_key]
+          author_key = "author_#{authors.size + 1}"
+        end
+        if authors.size == 1
+          # do not allow multiple, process as names only
+          author_metadata = process_authors authors.first, true, false
+        elsif authors.size > 1
+          # allow multiple, process as names only
+          author_metadata = process_authors authors.join('; '), true
+        end
+      end
+
+      unless author_metadata.nil?
+        document.attributes.update author_metadata
 
-      # apply header subs and assign to document
-      if !document.nil?
-        metadata.map do |key, val|
-          val = document.apply_header_subs(val)
-          document.attributes[key] = val if !document.attributes.has_key?(key)
-          val
+        # special case
+        if !document.attributes.has_key?('email') && document.attributes.has_key?('email_1')
+          document.attributes['email'] = document.attributes['email_1']
         end
       end
     end
@@ -1309,6 +1520,79 @@ class Lexer
     metadata
   end
 
+  # Internal: Parse the author line into a Hash of author metadata
+  #
+  # author_line  - the String author line
+  # names_only   - a Boolean flag that indicates whether to process line as
+  #                names only or names with emails (default: false)
+  # multiple     - a Boolean flag that indicates whether to process multiple
+  #                semicolon-separated entries in the author line (default: true)
+  #
+  # returns a Hash of author metadata
+  def self.process_authors(author_line, names_only = false, multiple = true)
+    author_metadata = {}
+    keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
+    author_entries = multiple ? author_line.split(';').map(&:strip) : [author_line]
+    author_entries.each_with_index do |author_entry, idx|
+      author_entry.strip!
+      next if author_entry.empty?
+      key_map = {}
+      if idx.zero?
+        keys.each do |key|
+          key_map[key.to_sym] = key
+        end
+      else
+        keys.each do |key|
+          key_map[key.to_sym] = "#{key}_#{idx + 1}"
+        end
+      end
+
+      segments = nil
+      if names_only
+        # splitting on ' ' will collapse repeating spaces
+        segments = author_entry.split(' ', 3)
+      elsif (match = author_entry.match(REGEXP[:author_info]))
+        segments = match.to_a
+        segments.shift
+      end
+
+      unless segments.nil?
+        author_metadata[key_map[:firstname]] = fname = segments[0].tr('_', ' ')
+        author_metadata[key_map[:author]] = fname
+        author_metadata[key_map[:authorinitials]] = fname[0, 1]
+        if !segments[1].nil? && !segments[2].nil?
+          author_metadata[key_map[:middlename]] = mname = segments[1].tr('_', ' ')
+          author_metadata[key_map[:lastname]] = lname = segments[2].tr('_', ' ')
+          author_metadata[key_map[:author]] = [fname, mname, lname].join ' '
+          author_metadata[key_map[:authorinitials]] = [fname[0, 1], mname[0, 1], lname[0, 1]].join
+        elsif !segments[1].nil?
+          author_metadata[key_map[:lastname]] = lname = segments[1].tr('_', ' ')
+          author_metadata[key_map[:author]] = [fname, lname].join ' '
+          author_metadata[key_map[:authorinitials]] = [fname[0, 1], lname[0, 1]].join
+        end
+        author_metadata[key_map[:email]] = segments[3] unless names_only || segments[3].nil?
+      else
+        author_metadata[key_map[:author]] = author_metadata[key_map[:firstname]] = fname = author_entry.strip.squeeze(' ')
+        author_metadata[key_map[:authorinitials]] = fname[0, 1]
+      end
+
+      author_metadata['authorcount'] = idx + 1
+      # only assign the _1 attributes if there are multiple authors
+      if idx == 1
+        keys.each do |key|
+          author_metadata["#{key}_1"] = author_metadata[key] if author_metadata.has_key? key
+        end
+      end
+      if idx.zero?
+        author_metadata['authors'] = author_metadata[key_map[:author]]
+      else
+        author_metadata['authors'] = "#{author_metadata['authors']}, #{author_metadata[key_map[:author]]}"
+      end
+    end
+
+    author_metadata
+  end
+
   # Internal: Parse lines of metadata until a line of metadata is not found.
   #
   # This method processes sequential lines containing block metadata, ignoring
@@ -1372,7 +1656,7 @@ class Lexer
         parent.document.register(:ids, [id, reftext])
       end
     elsif match = next_line.match(REGEXP[:blk_attr_list])
-      AttributeList.new(parent.document.sub_attributes(match[1]), parent.document).parse_into(attributes)
+      parent.document.parse_attributes(match[1], [], :sub_input => true, :into => attributes)
     # NOTE title doesn't apply to section, but we need to stash it for the first block
     # TODO should issue an error if this is found above the document title
     elsif !options[:text] && (match = next_line.match(REGEXP[:blk_title]))
@@ -1412,29 +1696,41 @@ class Lexer
         end
       end
 
-      if name.end_with?('!')
-        # a nil value signals the attribute should be deleted (undefined)
-        value = nil
-        name = name.chop
-      end
-
-      name = sanitize_attribute_name(name)
-      accessible = true
-      if !parent.nil?
-        accessible = value.nil? ?
-            parent.document.delete_attribute(name) :
-            parent.document.set_attribute(name, value)
-      end
-
-      if !attributes.nil?
-        Document::AttributeEntry.new(name, value).save_to(attributes) if accessible
-      end
+      store_attribute(name, value, parent.nil? ? nil : parent.document, attributes)
       true
     else
       false
     end
   end
 
+  # Public: Store the attribute in the document and register attribute entry if accessible
+  #
+  # name  - the String name of the attribute to store
+  # value - the String value of the attribute to store
+  # doc   - the Document being parsed
+  # attrs - the attributes for the current context
+  #
+  # returns a 2-element array containing the attribute name and value
+  def self.store_attribute(name, value, doc = nil, attrs = nil)
+    if name.end_with?('!')
+      # a nil value signals the attribute should be deleted (undefined)
+      value = nil
+      name = name.chop
+    end
+
+    name = sanitize_attribute_name(name)
+    accessible = true
+    unless doc.nil?
+      accessible = value.nil? ? doc.delete_attribute(name) : doc.set_attribute(name, value)
+    end
+
+    unless !accessible || attrs.nil?
+      Document::AttributeEntry.new(name, value).save_to(attrs)
+    end
+
+    [name, value]
+  end
+
   # Internal: Resolve the 0-index marker for this list item
   #
   # For ordered lists, match the marker used for this list item against the
@@ -1566,6 +1862,8 @@ class Lexer
   # returns an instance of Asciidoctor::Table parsed from the provided reader
   def self.next_table(table_reader, parent, attributes)
     table = Table.new(parent, attributes)
+    table.title = attributes.delete('title') if attributes.has_key?('title')
+    table.assign_caption attributes.delete('caption')
 
     if attributes.has_key? 'cols'
       table.create_columns(parse_col_specs(attributes['cols']))
@@ -1615,9 +1913,9 @@ class Lexer
           if parser_ctx.format == 'psv'
             next_cell_spec, cell_text = parse_cell_spec(m.pre_match, :end)
             parser_ctx.push_cell_spec next_cell_spec
-            parser_ctx.buffer << cell_text
+            parser_ctx.buffer = %(#{parser_ctx.buffer}#{cell_text})
           else
-            parser_ctx.buffer << m.pre_match
+            parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match})
           end
 
           line = m.post_match
@@ -1625,10 +1923,10 @@ class Lexer
         else
           # no other delimiters to see here
           # suck up this line into the buffer and move on
-          parser_ctx.buffer << line
+          parser_ctx.buffer = %(#{parser_ctx.buffer}#{line})
           # QUESTION make this an option? (unwrap-option?)
           if parser_ctx.format == 'csv'
-            parser_ctx.buffer.rstrip!.concat(' ')
+            parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} )
           end
           line = ''
           if parser_ctx.format == 'psv' || (parser_ctx.format == 'csv' &&
@@ -1764,6 +2062,143 @@ class Lexer
     [spec, rest]
   end
 
+  # Public: Parse the first positional attribute and assign named attributes
+  #
+  # Parse the first positional attribute to extract the style, role and id
+  # parts, assign the values to their cooresponding attribute keys and return
+  # both the original style attribute and the parsed value from the first
+  # positional attribute.
+  #
+  # attributes - The Hash of attributes to process
+  #
+  # Examples
+  #
+  #   puts attributes
+  #   => {1 => "abstract#intro.lead", "style" => "preamble"}
+  #
+  #   parse_style_attribute(attributes)
+  #   => ["abstract", "preamble"]
+  #
+  #   puts attributes
+  #   => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro", "role" => "lead"}
+  #
+  # Returns a two-element Array of the parsed style from the
+  # first positional attribute and the original style that was
+  # replaced
+  def self.parse_style_attribute(attributes)
+    original_style = attributes['style']
+    raw_style = attributes[1]
+    if !raw_style || raw_style.include?(' ')
+      attributes['style'] = raw_style
+      [raw_style, original_style]
+    # FIXME this logic could be condensed
+    else
+      hash_index = raw_style.index('#')
+      dot_index = raw_style.index('.') 
+      if !hash_index.nil? && (dot_index.nil? || hash_index < dot_index)
+        parsed_style = attributes['style'] = (hash_index > 0 ? raw_style[0..(hash_index - 1)] : nil)
+        id = raw_style[(hash_index + 1)..-1]
+        if !dot_index.nil?
+          id, roles = id.split('.', 2)
+          attributes['id'] = id
+          attributes['role'] = roles.tr('.', ' ')
+        else
+          attributes['id'] = id
+        end
+      elsif !dot_index.nil? && (hash_index.nil? || dot_index < hash_index)
+        parsed_style = attributes['style'] = (dot_index > 0 ? raw_style[0..(dot_index - 1)] : nil)
+        roles = raw_style[(dot_index + 1)..-1]
+        if !hash_index.nil?
+          roles, id = roles.split('#', 2)
+          attributes['id'] = id
+          attributes['role'] = roles.tr('.', ' ')
+        else
+          attributes['role'] = roles.tr('.', ' ')
+        end
+      else
+        parsed_style = attributes['style'] = raw_style
+      end
+
+      [parsed_style, original_style]
+    end
+  end
+
+  # Remove the indentation (block offset) shared by all the lines, then
+  # indent the lines by the specified amount if specified
+  #
+  # Trim the leading whitespace (indentation) equivalent to the length
+  # of the indent on the least indented line. If the indent argument
+  # is specified, indent the lines by this many spaces (columns).
+  # 
+  # The purpose of this method is to shift a block of text to
+  # align to the left margin, while still preserving the relative
+  # indentation between lines
+  #
+  # lines  - the Array of String lines to process
+  # indent - the integer number of spaces to add to the beginning
+  #          of each line; if this value is nil, the existing
+  #          space is preserved (optional, default: 0)
+  #
+  # Examples
+  #
+  #   source = <<EOS
+  #       def names
+  #         @name.split ' ')
+  #       end
+  #   EOS
+  #
+  #   source.lines.entries
+  #   # => ["    def names\n", "      @names.split ' '\n", "    end\n"]
+  #
+  #   Lexer.reset_block_indent(source.lines.entries)
+  #   # => ["def names\n", "  @names.split ' '\n", "end\n"]
+  #
+  #   puts Lexer.reset_block_indent(source.lines.entries).join
+  #   # => def names
+  #   # =>   @names.split ' '
+  #   # => end
+  #
+  # returns the Array of String lines with block offset removed
+  def self.reset_block_indent!(lines, indent = 0)
+    return if indent.nil? || lines.empty?
+
+    tab_detected = false
+    # TODO make tab size configurable
+    tab_expansion = '    '
+    # strip leading block indent
+    offsets = lines.map do |line|
+      # break if the first char is non-whitespace
+      break [] unless line.chomp[0..0].lstrip.empty?
+      if line.include? "\t"
+        tab_detected = true
+        line = line.gsub("\t", tab_expansion)
+      end
+      if (flush_line = line.lstrip).empty?
+        nil
+      elsif (offset = line.length - flush_line.length) == 0
+        break []
+      else
+        offset
+      end
+    end
+    
+    unless offsets.empty? || (offsets = offsets.compact).empty?
+      if (offset = offsets.min) > 0
+        lines.map! {|line|
+          line = line.gsub("\t", tab_expansion) if tab_detected
+          line[offset..-1] || "\n"
+        }
+      end
+    end
+
+    if indent > 0
+      padding = ' ' * indent
+      lines.map! {|line| %(#{padding}#{line}) }
+    end
+
+    nil
+  end
+
   # Public: Convert a string to a legal attribute name.
   #
   # name  - the String name of the attribute
diff --git a/lib/asciidoctor/list_item.rb b/lib/asciidoctor/list_item.rb
index cc85789..fb2c574 100644
--- a/lib/asciidoctor/list_item.rb
+++ b/lib/asciidoctor/list_item.rb
@@ -55,27 +55,6 @@ class ListItem < AbstractBlock
     nil
   end
 
-  def splain(parent_level = 0)
-    parent_level += 1
-    Debug.puts_indented(parent_level, "List Item anchor: #{@anchor}") unless @anchor.nil?
-    Debug.puts_indented(parent_level, "Text: #{@text}") unless @text.nil?
-
-    Debug.puts_indented(parent_level, "Blocks: #{@blocks.count}")
-
-    if @blocks.any?
-      Debug.puts_indented(parent_level, "Blocks content (#{@blocks.count}):")
-      @blocks.each_with_index do |block, i|
-        Debug.puts_indented(parent_level, "v" * (60 - parent_level*2))
-        Debug.puts_indented(parent_level, "Block ##{i} is a #{block.class}")
-        Debug.puts_indented(parent_level, "Name is #{block.title rescue 'n/a'}")
-        Debug.puts_indented(parent_level, "=" * 40)
-        block.splain(parent_level) if block.respond_to? :splain
-        Debug.puts_indented(parent_level, "^" * (60 - parent_level*2))
-      end
-    end
-    nil
-  end
-
   def to_s
     "#{super.to_s} - #@context [text:#@text, blocks:#{(@blocks || []).size}]"
   end
diff --git a/lib/asciidoctor/path_resolver.rb b/lib/asciidoctor/path_resolver.rb
new file mode 100644
index 0000000..f03fe63
--- /dev/null
+++ b/lib/asciidoctor/path_resolver.rb
@@ -0,0 +1,366 @@
+module Asciidoctor
+# Public: Handles all operations for resolving, cleaning and joining paths.
+# This class includes operations for handling both web paths (request URIs) and
+# system paths.
+#
+# The main emphasis of the class is on creating clean and secure paths. Clean
+# paths are void of duplicate parent and current directory references in the
+# path name. Secure paths are paths which are restricted from accessing
+# directories outside of a jail root, if specified.
+#
+# Since joining two paths can result in an insecure path, this class also
+# handles the task of joining a parent (start) and child (target) path.
+#
+# This class makes no use of path utilities from the Ruby libraries. Instead,
+# it handles all aspects of path manipulation. The main benefit of
+# internalizing these operations is that the class is able to handle both posix
+# and windows paths independent of the operating system on which it runs. This
+# makes the class both deterministic and easier to test.
+#
+# Examples
+#
+#     resolver = PathResolver.new
+#
+#     # Web Paths
+#
+#     resolver.web_path('images')
+#     => 'images'
+#
+#     resolver.web_path('./images')
+#     => './images'
+#
+#     resolver.web_path('/images')
+#     => '/images'
+#
+#     resolver.web_path('./images/../assets/images')
+#     => './assets/images'
+#
+#     resolver.web_path('/../images')
+#     => '/images'
+#
+#     resolver.web_path('images', 'assets')
+#     => 'assets/images'
+#
+#     resolver.web_path('tiger.png', '../assets/images')
+#     => '../assets/images/tiger.png'
+#
+#     # System Paths
+#
+#     resolver.working_dir
+#     => '/path/to/docs'
+#
+#     resolver.system_path('images')
+#     => '/path/to/docs/images'
+#
+#     resolver.system_path('../images')
+#     => '/path/to/images'
+#
+#     resolver.system_path('/etc/images')
+#     => '/etc/images'
+#
+#     resolver.system_path('images', '/etc')
+#     => '/etc/images'
+#
+#     resolver.system_path('', '/etc/images')
+#     => '/etc/images'
+#
+#     resolver.system_path(nil, nil, '/path/to/docs')
+#     => '/path/to/docs'
+#
+#     resolver.system_path('..', nil, '/path/to/docs')
+#     => '/path/to/docs'
+#
+#     resolver.system_path('../../../css', nil, '/path/to/docs')
+#     => '/path/to/docs/css'
+#
+#     resolver.system_path('../../../css', '../../..', '/path/to/docs')
+#     => '/path/to/docs/css'
+#
+#     resolver.system_path('..', 'C:\\data\\docs\\assets', 'C:\\data\\docs')
+#     => 'C:/data/docs'
+#
+#     resolver.system_path('..\\..\\css', 'C:\\data\\docs\\assets', 'C:\\data\\docs')
+#     => 'C:/data/docs/css'
+#
+#     begin
+#       resolver.system_path('../../../css', '../../..', '/path/to/docs', :recover => false)
+#     rescue SecurityError => e
+#       puts e.message
+#     end
+#     => 'path ../../../../../../css refers to location outside jail: /path/to/docs (disallowed in safe mode)'
+#
+#     resolver.system_path('/path/to/docs/images', nil, '/path/to/docs')
+#     => '/path/to/docs/images'
+#
+#     begin
+#       resolver.system_path('images', '/etc', '/path/to/docs')
+#     rescue SecurityError => e
+#       puts e.message 
+#     end
+#     => Start path /etc is outside of jail: /path/to/docs'
+#
+class PathResolver
+  DOT = '.'
+  DOT_DOT = '..'
+  SLASH = '/'
+  BACKSLASH = '\\'
+  WIN_ROOT_RE = /^[[:alpha:]]:(?:\\|\/)/
+
+  attr_accessor :file_separator
+  attr_accessor :working_dir
+
+  # Public: Construct a new instance of PathResolver, optionally specifying the
+  # file separator (to override the system default) and the working directory
+  # (to override the present working directory). The working directory will be
+  # expanded to an absolute path inside the constructor.
+  #
+  # file_separator - the String file separator to use for path operations
+  #                  (optional, default: File::FILE_SEPARATOR)
+  # working_dir    - the String working directory (optional, default: Dir.pwd)
+  #
+  def initialize(file_separator = nil, working_dir = nil)
+    @file_separator = file_separator.nil? ? (File::ALT_SEPARATOR || File::SEPARATOR) : file_separator
+    if working_dir.nil?
+      @working_dir = File.expand_path(Dir.pwd)
+    else
+      @working_dir = is_root?(working_dir) ? working_dir : File.expand_path(working_dir) 
+    end
+  end
+
+  # Public: Check if the specified path is an absolute root path
+  # This operation correctly handles both posix and windows paths.
+  #
+  # path - the String path to check
+  #
+  # returns a Boolean indicating whether the path is an absolute root path
+  def is_root?(path)
+    if @file_separator == BACKSLASH && path.match(WIN_ROOT_RE)
+      true
+    elsif path.start_with? SLASH
+      true
+    else
+      false
+    end
+  end
+
+  # Public: Determine if the path is an absolute (root) web path
+  #
+  # path - the String path to check
+  #
+  # returns a Boolean indicating whether the path is an absolute (root) web path
+  def is_web_root?(path)
+    path.start_with? SLASH
+  end
+  
+  # Public: Normalize path by converting any backslashes to forward slashes
+  #
+  # path - the String path to normalize
+  #
+  # returns a String path with any backslashes replaced with forward slashes
+  def posixfy(path)
+    return '' if path.to_s.empty?
+    path.include?(BACKSLASH) ? path.tr(BACKSLASH, SLASH) : path
+  end
+
+  # Public: Expand the path by resolving any parent references (..)
+  # and cleaning self references (.).
+  #
+  # The result will be relative if the path is relative and
+  # absolute if the path is absolute. The file separator used
+  # in the expanded path is the one specified when the class
+  # was constructed.
+  #
+  # path - the String path to expand
+  #
+  # returns a String path with any parent or self references resolved.
+  def expand_path(path)
+    path_segments, path_root, _ = partition_path(path)
+    join_path path_segments, path_root
+  end
+  
+  # Public: Partition the path into path segments and remove any empty segments
+  # or segments that are self references (.). The path is split on either posix
+  # or windows file separators.
+  #
+  # path     - the String path to partition
+  # web_path - a Boolean indicating whether the path should be handled
+  #            as a web path (optional, default: false)
+  #
+  # returns a 3-item Array containing the Array of String path segments, the
+  # path root, if the path is absolute, and the posix version of the path.
+  def partition_path(path, web_path = false)
+    posix_path = posixfy path
+    is_root = web_path ? is_web_root?(posix_path) : is_root?(posix_path)
+    path_segments = posix_path.tr_s(SLASH, SLASH).split(SLASH)
+    # capture relative root
+    root = path_segments.first == DOT ? DOT : nil
+    path_segments.delete(DOT)
+    # capture absolute root, preserving relative root if set
+    root = is_root ? path_segments.shift : root
+  
+    [path_segments, root, posix_path]
+  end
+  
+  # Public: Join the segments using the posix file separator (since Ruby knows
+  # how to work with paths specified this way, regardless of OS). Use the root,
+  # if specified, to construct an absolute path. Otherwise join the segments as
+  # a relative path.
+  #
+  # segments - a String Array of path segments
+  # root     - a String path root (optional, default: nil)
+  #
+  # returns a String path formed by joining the segments using the posix file
+  # separator and prepending the root, if specified
+  def join_path(segments, root = nil)
+    if root
+      "#{root}#{SLASH}#{segments * SLASH}"
+    else
+      segments * SLASH
+    end
+  end
+  
+  # Public: Resolve a system path from the target and start paths. If a jail
+  # path is specified, enforce that the resolved directory is contained within
+  # the jail path. If a jail path is not provided, the resolved path may be
+  # any location on the system. If the resolved path is absolute, use it as is.
+  # If the resolved path is relative, resolve it relative to the working_dir
+  # specified in the constructor.
+  #
+  # target - the String target path
+  # start  - the String start (i.e., parent) path
+  # jail   - the String jail path to confine the resolved path
+  # opts   - an optional Hash of options to control processing (default: {}):
+  #          * :recover is used to control whether the processor should auto-recover
+  #              when an illegal path is encountered
+  #          * :target_name is used in messages to refer to the path being resolved
+  #
+  # returns a String path that joins the target path with the start path with
+  # any parent references resolved and self references removed and enforces
+  # that the resolved path be contained within the jail, if provided
+  def system_path(target, start, jail = nil, opts = {})
+    recover = opts.fetch(:recover, true)
+    unless jail.nil?
+      unless is_root? jail
+        raise SecurityError, "Jail is not an absolute path: #{jail}"
+      end
+      jail = posixfy jail
+    end
+
+    if target.to_s.empty?
+      target_segments = []
+    else
+      target_segments, target_root, _ = partition_path(target)
+    end
+
+    if target_segments.empty?
+      if start.to_s.empty?
+        return jail.nil? ? @working_dir : jail
+      elsif is_root? start
+        if jail.nil?
+          return expand_path start
+        end
+      else
+        return system_path(start, jail, jail)
+      end
+    end
+  
+    if target_root && target_root != DOT
+      resolved_target = join_path target_segments, target_root
+      # if target is absolute and a sub-directory of jail, or
+      # a jail is not in place, let it slide
+      if jail.nil? || resolved_target.start_with?(jail)
+        return resolved_target
+      end
+    end
+  
+    if start.to_s.empty?
+      start = jail.nil? ? @working_dir : jail
+    elsif is_root? start
+      start = posixfy start
+    else
+      start = system_path(start, jail, jail)
+    end
+  
+    # both jail and start have been posixfied at this point
+    if jail == start
+      jail_segments, jail_root, _ = partition_path(jail)
+      start_segments = jail_segments.dup
+    elsif !jail.nil?
+      if !start.start_with?(jail)
+        raise SecurityError, "#{opts[:target_name] || 'Start path'} #{start} is outside of jail: #{jail} (disallowed in safe mode)"
+      end
+
+      start_segments, start_root, _ = partition_path(start)
+      jail_segments, jail_root, _ = partition_path(jail)
+  
+      # Already checked for this condition
+      #if start_root != jail_root
+      #  raise SecurityError, "Jail root #{jail_root} does not match root of #{opts[:target_name] || 'start path'}: #{start_root}"
+      #end
+    else
+      start_segments, start_root, _ = partition_path(start)
+      jail_root = start_root
+    end
+  
+    resolved_segments = start_segments.dup
+    warned = false
+    target_segments.each do |segment|
+      if segment == DOT_DOT
+        if !jail.nil?
+          if resolved_segments.length > jail_segments.length
+            resolved_segments.pop
+          elsif !recover
+            raise SecurityError, "#{opts[:target_name] || 'path'} #{target} refers to location outside jail: #{jail} (disallowed in safe mode)"
+          elsif !warned
+            puts "asciidoctor: WARNING: #{opts[:target_name] || 'path'} has illegal reference to ancestor of jail, auto-recovering"
+            warned = true
+          end
+        else
+          resolved_segments.pop
+        end
+      else
+        resolved_segments.push segment
+      end
+    end
+  
+    join_path resolved_segments, jail_root
+  end
+
+  # Public: Resolve a web path from the target and start paths.
+  # The main function of this operation is to resolve any parent
+  # references and remove any self references.
+  #
+  # target - the String target path
+  # start  - the String start (i.e., parent) path
+  #
+  # returns a String path that joins the target path with the
+  # start path with any parent references resolved and self
+  # references removed
+  def web_path(target, start = nil)
+    target = posixfy(target)
+    start = posixfy(start)
+
+    unless is_web_root?(target) || start.empty?
+      target = "#{start}#{SLASH}#{target}"
+    end
+
+    target_segments, target_root, _ = partition_path(target, true)
+    resolved_segments = target_segments.inject([]) do |accum, segment|
+      if segment == DOT_DOT
+        if accum.empty?
+          accum.push segment unless target_root && target_root != DOT
+        elsif accum[-1] == DOT_DOT
+          accum.push segment
+        else
+          accum.pop
+        end
+      else
+        accum.push segment
+      end
+      accum
+    end
+
+    join_path resolved_segments, target_root
+  end
+end
+end
diff --git a/lib/asciidoctor/reader.rb b/lib/asciidoctor/reader.rb
index 7a5e49a..e8aa78f 100644
--- a/lib/asciidoctor/reader.rb
+++ b/lib/asciidoctor/reader.rb
@@ -268,7 +268,7 @@ class Reader
   def preprocess_next_line
     # this return could be happening from a recursive call
     return nil if @eof || (next_line = @lines.first).nil?
-    if next_line.include?('if') && (match = next_line.match(REGEXP[:ifdef_macro]))
+    if next_line.include?('::') && (next_line.include?('if') || next_line.include?('endif')) && (match = next_line.match(REGEXP[:ifdef_macro]))
       if next_line.start_with? '\\'
         @next_line_preprocessed = true
         @unescape_next_line = true
@@ -287,7 +287,7 @@ class Reader
         @unescape_next_line = true
         false
       else
-        preprocess_include(match[1])
+        preprocess_include(match[1], match[2].strip)
       end
     else
       @next_line_preprocessed = true
@@ -343,7 +343,7 @@ class Reader
       return preprocess_next_line.nil? ? nil : true
     end
 
-    skip = nil
+    skip = false
     if !@skipping
       case directive
       when 'ifdef'
@@ -384,17 +384,19 @@ class Reader
 
         skip = !lhs.send(op.to_sym, rhs)
       end
-      @skipping = skip
     end
     advance
     # single line conditional inclusion
     if directive != 'ifeval' && !text.nil?
-      if !@skipping
+      if !@skipping && !skip
         unshift_line "#{text.rstrip}\n"
         return true
       end
     # conditional inclusion block
     else
+      if !@skipping && skip
+        @skipping = true
+      end
       @conditionals_stack << {:target => target, :skip => skip, :skipping => @skipping}
     end
     return preprocess_next_line.nil? ? nil : true
@@ -422,9 +424,16 @@ class Reader
   #          target slot of the include::[] macro
   #
   # returns a Boolean indicating whether the line under the cursor has changed.
-  def preprocess_include(target)
+  def preprocess_include(target, raw_attributes)
+    target = @document.sub_attributes target
+    if target.empty?
+      advance
+      @next_line_preprocessed = false
+      false
     # if running in SafeMode::SECURE or greater, don't process this directive
-    if @document.safe >= SafeMode::SECURE
+    # however, be friendly and at least make it a link to the source document
+    elsif @document.safe >= SafeMode::SECURE
+      @lines[0] = "link:#{target}[#{target}]\n"
       @next_line_preprocessed = true
       false
     # assume that if a block is given, the developer wants
@@ -433,12 +442,88 @@ class Reader
     elsif @include_block
       advance
       # FIXME this borks line numbers
-      @lines.unshift(*@include_block.call(target).map {|l| "#{l.rstrip}\n"})
+      @lines.unshift(*normalize_include_data(@include_block.call(target)))
     # FIXME currently we're not checking the upper bound of the include depth
     elsif @document.attributes.fetch('include-depth', 0).to_i > 0
       advance
       # FIXME this borks line numbers
-      @lines.unshift(*File.readlines(@document.normalize_asset_path(target, 'include file')).map {|l| "#{l.rstrip}\n"})
+      include_file = @document.normalize_system_path(target, nil, nil, :target_name => 'include file')
+      if !File.file?(include_file)
+        puts "asciidoctor: WARNING: line #{@lineno}: include file not found: #{include_file}"
+        return true
+      end
+
+      inc_lines = nil
+      tags = nil
+      attributes = {}
+      if !raw_attributes.empty?
+        attributes = AttributeList.new(raw_attributes).parse
+        if attributes.has_key? 'lines'
+          inc_lines = []
+          attributes['lines'].split(REGEXP[:ssv_or_csv_delim]).each do |linedef|
+            if linedef.include?('..')
+              from, to = linedef.split('..').map(&:to_i)
+              if to == -1
+                inc_lines << from
+                inc_lines << 1.0/0.0
+              else
+                inc_lines.concat Range.new(from, to).to_a
+              end
+            else
+              inc_lines << linedef.to_i
+            end
+          end
+          inc_lines = inc_lines.sort.uniq
+        elsif attributes.has_key? 'tags'
+          tags = attributes['tags'].split(REGEXP[:ssv_or_csv_delim]).uniq
+        end
+      end
+      if !inc_lines.nil?
+        if !inc_lines.empty?
+          selected = []
+          f = File.new(include_file)
+          f.each_line do |l|
+            take = inc_lines.first
+            if take.is_a?(Float) && take.infinite?
+              selected.push l
+            else
+              if f.lineno == take
+                selected.push l
+                inc_lines.shift 
+              end
+              break if inc_lines.empty?
+            end
+          end
+          @lines.unshift(*normalize_include_data(selected, attributes['indent'])) unless selected.empty?
+        end
+      elsif !tags.nil?
+        if !tags.empty?
+          selected = []
+          active_tag = nil
+          f = File.new(include_file)
+          f.each_line do |l|
+            l.force_encoding(::Encoding::UTF_8) if ::Asciidoctor::FORCE_ENCODING
+            if !active_tag.nil?
+              if l.include?("end::#{active_tag}[]")
+                active_tag = nil
+              else
+                selected.push "#{l.rstrip}\n"
+              end
+            else
+              tags.each do |tag|
+                if l.include?("tag::#{tag}[]")
+                  active_tag = tag
+                  break
+                end
+              end
+            end
+          end
+          #@lines.unshift(*selected) unless selected.empty?
+          @lines.unshift(*normalize_include_data(selected, attributes['indent'])) unless selected.empty?
+        end
+      else
+        @lines.unshift(*normalize_include_data(File.readlines(include_file), attributes['indent']))
+      end
       true
     else
       @next_line_preprocessed = true
@@ -514,32 +599,54 @@ class Reader
   def grab_lines_until(options = {}, &block)
     buffer = []
 
-    finis = false
     advance if options[:skip_first_line]
-    # save options to locals for minor optimization
-    terminator = options[:terminator]
-    terminator.chomp! if terminator
-    break_on_blank_lines = options[:break_on_blank_lines]
-    break_on_list_continuation = options[:break_on_list_continuation]
+    # very hot code
+    # save options to locals for minor optimizations
+    if options.has_key? :terminator
+      terminator = options[:terminator]
+      break_on_blank_lines = false
+      break_on_list_continuation = false
+      chomp_last_line = options[:chomp_last_line] || false
+    else
+      terminator = nil
+      break_on_blank_lines = options[:break_on_blank_lines]
+      break_on_list_continuation = options[:break_on_list_continuation]
+      chomp_last_line = break_on_blank_lines
+    end
     skip_line_comments = options[:skip_line_comments]
     preprocess = options.fetch(:preprocess, true)
+    buffer_empty = true
     while !(this_line = get_line(preprocess)).nil?
-      Debug.debug { "Reader processing line: '#{this_line}'" }
-      finis = true if terminator && this_line.chomp == terminator
-      finis = true if !finis && break_on_blank_lines && this_line.strip.empty?
-      finis = true if !finis && break_on_list_continuation && this_line.chomp == LIST_CONTINUATION
-      finis = true if !finis && block && yield(this_line)
-      if finis
-        buffer << this_line if options[:grab_last_line]
-        unshift_line(this_line) if options[:preserve_last_line]
+      # effectively a no-args lamba, but much faster
+      finish = while true
+        break true if terminator && this_line.chomp == terminator
+        break true if break_on_blank_lines && this_line.strip.empty?
+        if break_on_list_continuation && !buffer_empty && this_line.chomp == LIST_CONTINUATION
+          options[:preserve_last_line] = true
+          break true
+        end
+        break true if block && yield(this_line)
+        break false
+      end
+
+      if finish
+        if options[:grab_last_line]
+          buffer << this_line
+          buffer_empty = false
+        end
+        # QUESTION should we dup this_line when restoring??
+        unshift_line this_line if options[:preserve_last_line]
         break
       end
 
       unless skip_line_comments && this_line.match(REGEXP[:comment])
         buffer << this_line
+        buffer_empty = false
       end
     end
 
+    # should we dup the line before chopping?
+    buffer.last.chomp! if chomp_last_line && !buffer_empty
     buffer
   end
 
@@ -575,7 +682,7 @@ class Reader
     end
 
     if val.include? '{'
-      val = @document.sub_attributes(val)
+      val = @document.sub_attributes val
     end
 
     if type != :s
@@ -595,6 +702,39 @@ class Reader
     val
   end
 
+  # Private: Normalize raw input read from an include directive
+  #
+  # This method strips whitespace from the end of every line of
+  # the source data and appends a LF (i.e., Unix endline). This
+  # whitespace substitution is very important to how Asciidoctor
+  # works.
+  #
+  # Any leading or trailing blank lines are also removed. (DISABLED)
+  #
+  # data - A String Array of input data to be normalized
+  #
+  # returns the processed lines
+  #-
+  # FIXME this shares too much in common w/ normalize_data; combine
+  # in a shared function
+  def normalize_include_data(data, indent = nil)
+    if ::Asciidoctor::FORCE_ENCODING
+      result = data.map {|line| "#{line.rstrip.force_encoding(::Encoding::UTF_8)}\n" }
+    else
+      result = data.map {|line| "#{line.rstrip}\n" }
+    end
+
+    unless indent.nil?
+      Lexer.reset_block_indent! result, indent.to_i
+    end
+
+    result
+
+    #data.shift while !data.first.nil? && data.first.chomp.empty?
+    #data.pop while !data.last.nil? && data.last.chomp.empty?
+    #data
+  end
+
   # Private: Normalize raw input, used for the outermost Reader.
   #
   # This method strips whitespace from the end of every line of
@@ -612,15 +752,19 @@ class Reader
   def normalize_data(data)
     # normalize line ending to LF (purging occurrences of CRLF)
     # this rstrip is *very* important to how Asciidoctor works
-    @lines = data.map {|line| "#{line.rstrip}\n" }
+
+    if ::Asciidoctor::FORCE_ENCODING
+      @lines = data.map {|line| "#{line.rstrip.force_encoding(::Encoding::UTF_8)}\n" }
+    else
+      @lines = data.map {|line| "#{line.rstrip}\n" }
+    end
 
     @lines.shift && @lineno += 1 while !@lines.first.nil? && @lines.first.chomp.empty?
     @lines.pop while !@lines.last.nil? && @lines.last.chomp.empty?
 
     # Process bibliography references, so they're available when text
     # before the reference is being rendered.
-    # FIXME we don't have support for bibliography lists yet, so disable for now
-    # plus, this should be done while we are walking lines above
+    # FIXME reenable whereever it belongs
     #@lines.each do |line|
     #  if biblio = line.match(REGEXP[:biblio])
     #    @document.register(:ids, biblio[1])
diff --git a/lib/asciidoctor/renderer.rb b/lib/asciidoctor/renderer.rb
index 65c3309..10389f5 100644
--- a/lib/asciidoctor/renderer.rb
+++ b/lib/asciidoctor/renderer.rb
@@ -55,18 +55,11 @@ class Renderer
         :slim => { :disable_escape => true, :sort_attrs => false, :pretty => false }
       }
 
-      if backend == 'html5'
+      # workaround until we have a proper way to configure
+      if {'html5' => true, 'dzslides' => true, 'deckjs' => true, 'revealjs' => true}.has_key? backend
         view_opts[:haml][:format] = view_opts[:slim][:format] = :html5
       end
 
-      Debug.debug {
-        msg = []
-        msg << "Views going in are like so:"
-        msg << @views.map {|k, v| "#{k}: #{v}"}
-        msg << '=' * 60
-        msg * "\n"
-      }
-
       slim_loaded = false
       helpers = nil
       
@@ -91,17 +84,7 @@ class Renderer
       end
 
       require helpers unless helpers.nil?
-
-      Debug.debug {
-        msg = []
-        msg << "Views going in are like so:"
-        msg << @views.map {|k, v| "#{k}: #{v}"}
-        msg << '=' * 60
-        msg * "\n"
-      }
     end
-
-    @render_stack = []
   end
 
   # Public: Render an Asciidoc object with a specified view template.
@@ -110,43 +93,11 @@ class Renderer
   # object - the Object to be used as an evaluation scope.
   # locals - the optional Hash of locals to be passed to Tilt (default {}) (also ignored, really)
   def render(view, object, locals = {})
-    @render_stack.push([view, object])
-
     if !@views.has_key? view
       raise "Couldn't find a view in @views for #{view}"
-    else
-      Debug.debug { "View for #{view} is #{@views[view]}, object is #{object}" }
     end
     
-    ret = @views[view].render(object, locals)
-
-    if @debug
-      prefix = ''
-      STDERR.puts '=' * 80
-      STDERR.puts "Rendering:"
-      @render_stack.each do |stack_view, stack_obj|
-        obj_info = case stack_obj
-                   when Section; "SECTION #{stack_obj.title}"
-                   when Block;
-                     if stack_obj.context == :dlist
-                       dt_list = stack_obj.buffer.map{|dt,dd| dt.content.strip}.join(', ')
-                       "BLOCK :dlist (#{dt_list})"
-                     #else
-                     #  "BLOCK #{stack_obj.context.inspect}"
-                     end
-                   else stack_obj.class
-                   end
-        STDERR.puts "#{prefix}#{stack_view}: #{obj_info}"
-        prefix << '  '
-      end
-      STDERR.puts '-' * 80
-      #STDERR.puts ret.inspect
-      STDERR.puts '=' * 80
-      STDERR.puts
-    end
-
-    @render_stack.pop
-    ret
+    @views[view].render(object, locals)
   end
 
   def views
diff --git a/lib/asciidoctor/section.rb b/lib/asciidoctor/section.rb
index 90bbcee..177ce73 100644
--- a/lib/asciidoctor/section.rb
+++ b/lib/asciidoctor/section.rb
@@ -72,6 +72,7 @@ class Section < AbstractBlock
   def generate_id
     if @document.attr? 'sectids'
       separator = @document.attr('idseparator', '_')
+      # FIXME define constants for these regexps
       base_id = @document.attr('idprefix', '_') + title.downcase.gsub(/&#[0-9]+;/, separator).
           gsub(/\W+/, separator).tr_s(separator, separator).chomp(separator)
       gen_id = base_id
@@ -80,7 +81,6 @@ class Section < AbstractBlock
         gen_id = "#{base_id}#{separator}#{cnt}" 
         cnt += 1
       end 
-      @document.register(:ids, [gen_id, title])
       gen_id
     else
       nil
@@ -90,7 +90,6 @@ class Section < AbstractBlock
   # Public: Get the rendered String content for this Section and all its child
   # Blocks.
   def render
-    Debug.debug { "Now rendering section for #{self}" }
     @document.playback_attributes @attributes
     renderer.render('section', self)
   end
diff --git a/lib/asciidoctor/substituters.rb b/lib/asciidoctor/substituters.rb
index a06d0f7..a4db676 100644
--- a/lib/asciidoctor/substituters.rb
+++ b/lib/asciidoctor/substituters.rb
@@ -40,8 +40,9 @@ module Substituters
     multiline = lines.is_a?(Array)
     text = multiline ? lines.join : lines
 
-    passthroughs = subs.include?(:macros)
-    text = extract_passthroughs(text) if passthroughs
+    if (has_passthroughs = subs.include?(:macros))
+      text = extract_passthroughs(text)
+    end
     
     subs.each {|type|
       case type
@@ -63,7 +64,7 @@ module Substituters
         puts "asciidoctor: WARNING: unknown substitution type #{type}"
       end
     }
-    text = restore_passthroughs(text) if passthroughs
+    text = restore_passthroughs(text) if has_passthroughs
 
     multiline ? text.lines.entries : text
   end
@@ -92,7 +93,9 @@ module Substituters
   #
   # returns - A String with literal (verbatim) substitutions performed
   def apply_literal_subs(lines)
-    if @document.attributes['basebackend'] == 'html' && attr('style') == 'source' &&
+    if attr? 'subs'
+      apply_subs(lines.join, resolve_subs(attr 'subs'))
+    elsif @document.attributes['basebackend'] == 'html' && attr('style') == 'source' &&
       @document.attributes['source-highlighter'] == 'coderay' && attr?('language')
       sub_callouts(highlight_source(lines.join))
     else
@@ -109,11 +112,24 @@ module Substituters
     apply_subs(text, [:specialcharacters, :attributes])
   end
 
+  # Public: Apply explicit substitutions, if specified, otherwise normal substitutions.
+  #
+  # lines  - The lines of text to process. Can be a String or a String Array 
+  #
+  # returns - A String with substitutions applied
+  def apply_para_subs(lines)
+    if attr? 'subs'
+      apply_subs(lines.join, resolve_subs(attr 'subs'))
+    else
+      apply_subs(lines.join)
+    end
+  end
+
   # Public: Apply substitutions for passthrough text
   #
   # lines  - A String Array containing the lines of text process
   #
-  # returns - A String Array with passthrough substitutions performed
+  # returns - A String with passthrough substitutions performed
   def apply_passthrough_subs(lines)
     if attr? 'subs'
       subs = resolve_subs(attr('subs'))
@@ -150,7 +166,7 @@ module Substituters
       # TODO move unescaping closing square bracket to an operation
       @passthroughs << {:text => m[2] || m[4].gsub('\]', ']'), :subs => subs}
       index = @passthroughs.size - 1
-      "\x0#{index}\x0"
+      "\e#{index}\e"
     } unless !(result.include?('+++') || result.include?('$$') || result.include?('pass:'))
 
     result.gsub!(REGEXP[:pass_lit]) {
@@ -164,7 +180,7 @@ module Substituters
       
       @passthroughs << {:text => m[3], :subs => [:specialcharacters], :literal => true}
       index = @passthroughs.size - 1
-      "#{m[1]}\x0#{index}\x0"
+      "#{m[1]}\e#{index}\e"
     } unless !result.include?('`')
 
     result
@@ -176,7 +192,7 @@ module Substituters
   #
   # returns The String text with the passthrough text restored
   def restore_passthroughs(text)
-    return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\x0")
+    return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\e")
     
     text.gsub(REGEXP[:pass_placeholder]) {
       pass = @passthroughs[$1.to_i];
@@ -222,8 +238,24 @@ module Substituters
   def sub_replacements(text)
     result = text.dup
 
-    REPLACEMENTS.each {|pattern, replacement|
-      result.gsub!(pattern, replacement)
+    REPLACEMENTS.each {|pattern, replacement, restore|
+      result.gsub!(pattern) {
+        matched = $&
+        head = $1
+        tail = $2
+        if matched.include?('\\')
+          matched.tr('\\', '')
+        else
+          case restore
+          when :none
+            replacement
+          when :leading
+            "#{head}#{replacement}"
+          when :bounding
+            "#{head}#{replacement}#{tail}" 
+          end
+        end
+      }
     }
     
     result
@@ -244,41 +276,55 @@ module Substituters
   def sub_attributes(data)
     return data if data.nil? || data.empty?
 
+    string_data = data.is_a? String
     # normalizes data type to an array (string becomes single-element array)
-    lines = Array(data)
+    lines = string_data ? [data] : data
 
-    result = lines.map {|line|
+    result = []
+    lines.each {|line|
       reject = false
-      subject = line.dup
-      subject.gsub!(REGEXP[:attr_ref]) {
+      line = line.gsub(REGEXP[:attr_ref]) {
         # alias match for Ruby 1.8.7 compat
         m = $~
-        key = m[2].downcase
-        # escaped attribute
-        if !$1.empty? || !$3.empty?
-          "{#$2}"
-        elsif m[2].start_with?('counter:')
-          args = m[2].split(':')
-          @document.counter(args[1], args[2])
-        elsif m[2].start_with?('counter2:')
-          args = m[2].split(':')
-          @document.counter(args[1], args[2])
-          ''
-        elsif document.attributes.has_key? key
+        # escaped attribute, return unescaped
+        if !m[1].nil? || !m[4].nil?
+          "{#{m[2]}}"
+        elsif (directive = m[3])
+          offset = directive.length + 1
+          expr = m[2][offset..-1]
+          case directive
+          when 'set'
+            args = expr.split(':')
+            _, value = Lexer::store_attribute(args[0], args[1] || '', @document)
+            if value.nil?
+              reject = true
+              break '{undefined}'
+            end
+            ''
+          when 'counter', 'counter2'
+            args = expr.split(':')
+            val = @document.counter(args[0], args[1])
+            directive == 'counter2' ? '' : val
+          else
+            # if we get here, our attr_ref regex is too loose
+            puts "asciidoctor: WARNING: illegal attribute directive: #{m[2]}"
+            ''
+          end
+        elsif (key = m[2].downcase) && @document.attributes.has_key?(key)
           @document.attributes[key]
         elsif INTRINSICS.has_key? key
           INTRINSICS[key]
         else
-          Debug.debug { "Missing attribute: #{m[2]}, line marked for removal" }
+          Debug.debug { "Missing attribute: #{key}, line marked for removal" }
           reject = true
           break '{undefined}'
         end
-      } if subject.include?('{')
+      } if line.include? '{' 
 
-      !reject ? subject : nil
-    }.compact
+      result << line unless reject
+    }
 
-    data.is_a?(String) ? result.join : result
+    string_data ? result.join : result
   end
 
   # Public: Substitute inline macros (e.g., links, images, etc)
@@ -298,10 +344,94 @@ module Substituters
     found[:square_bracket] = result.include?('[')
     found[:round_bracket] = result.include?('(')
     found[:colon] = result.include?(':')
+    found[:at] = result.include?('@')
     found[:macroish] = (found[:square_bracket] && found[:colon])
     found[:macroish_short_form] = (found[:square_bracket] && found[:colon] && result.include?(':['))
     found[:uri] = (found[:colon] && result.include?('://'))
-    link_attrs = @document.attributes.has_key?('linkattrs')
+    use_link_attrs = @document.attributes.has_key?('linkattrs')
+    experimental = @document.attributes.has_key?('experimental')
+
+    if experimental
+      if found[:macroish_short_form] && (result.include?('kbd:') || result.include?('btn:'))
+        result.gsub!(REGEXP[:kbd_btn_macro]) {
+          # alias match for Ruby 1.8.7 compat
+          m = $~
+          # honor the escape
+          if (captured = m[0]).start_with? '\\'
+            next captured[1..-1]
+          end
+
+          if captured.start_with?('kbd')
+            keys = unescape_bracketed_text m[1]
+
+            if keys == '+'
+              keys = ['+']
+            else
+              # need to use closure to work around lack of negative lookbehind
+              keys = keys.split(REGEXP[:kbd_delim]).inject([]) {|c, key|
+                if key.end_with?('++')
+                  c << key[0..-3].strip
+                  c << '+'
+                else
+                  c << key.strip
+                end
+                c
+              }
+            end
+            Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).render 
+          elsif captured.start_with?('btn')
+            label = unescape_bracketed_text m[1]
+            Inline.new(self, :button, label).render
+          end
+        }
+      end
+      
+      if found[:macroish] && result.include?('menu:')
+        result.gsub!(REGEXP[:menu_macro]) {
+          # alias match for Ruby 1.8.7 compat
+          m = $~
+          # honor the escape
+          if (captured = m[0]).start_with? '\\'
+            next captured[1..-1]
+          end
+
+          menu = m[1]
+          items = m[2]
+
+          if items.nil?
+            submenus = []
+            menuitem = nil
+          else
+            if (delim = items.include?('>') ? '>' : (items.include?(',') ? ',' : nil))
+              submenus = items.split(delim).map(&:strip)
+              menuitem = submenus.pop
+            else
+              submenus = []
+              menuitem = items.rstrip
+            end
+          end
+
+          Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render
+        }
+      end
+
+      if result.include?('"') && result.include?('>')
+        result.gsub!(REGEXP[:menu_inline_macro]) {
+          # alias match for Ruby 1.8.7 compat
+          m = $~
+          # honor the escape
+          if (captured = m[0]).start_with? '\\'
+            next captured[1..-1]
+          end
+
+          input = m[1]
+
+          menu, *submenus = input.split('>').map(&:strip)
+          menuitem = submenus.pop 
+          Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render
+        }
+      end
+    end
 
     if found[:macroish] && result.include?('image:')
       # image:filename.png[Alt Text]
@@ -314,8 +444,8 @@ module Substituters
         end
         target = sub_attributes(m[1])
         @document.register(:images, target)
-        attrs = parse_attributes(m[2], ['alt', 'width', 'height'])
-        if !attrs.has_key?('alt') || attrs['alt'].empty?
+        attrs = parse_attributes(unescape_bracketed_text(m[2]), ['alt', 'width', 'height'])
+        if !attrs['alt']
           attrs['alt'] = File.basename(target, File.extname(target))
         end
         Inline.new(self, :image, nil, :target => target, :attributes => attrs).render
@@ -333,7 +463,7 @@ module Substituters
           next m[0][1..-1]
         end
 
-        terms = (m[1] || m[2]).strip.tr("\n", ' ').gsub('\]', ']').split(REGEXP[:csv_delimiter])
+        terms = unescape_bracketed_text(m[1] || m[2]).split(',').map(&:strip)
         document.register(:indexterms, [*terms])
         Inline.new(self, :indexterm, text, :attributes => {'terms' => terms}).render
       }
@@ -348,7 +478,7 @@ module Substituters
           next m[0][1..-1]
         end
 
-        text = (m[1] || m[2]).strip.tr("\n", ' ').gsub('\]', ']')
+        text = unescape_bracketed_text(m[1] || m[2])
         document.register(:indexterms, [text])
         Inline.new(self, :indexterm, text, :type => :visible).render
       }
@@ -369,33 +499,44 @@ module Substituters
         end
         prefix = (m[1] != 'link:' ? m[1] : '')
         target = m[2]
+        suffix = ''
         # strip the <> around the link
-        if prefix.end_with? '<'
-          prefix = prefix[0..-5]
-        end
-        if target.end_with? '>'
+        if prefix.start_with?('<') && target.end_with?('>')
+          prefix = prefix[4..-1]
           target = target[0..-5]
+        elsif prefix.start_with?('(') && target.end_with?(')')
+          target = target[0..-2]
+          suffix = ')'
+        elsif target.end_with?('):')
+          target = target[0..-3]
+          suffix = '):'
         end
         @document.register(:links, target)
 
         attrs = nil
         #text = !m[3].nil? ? sub_attributes(m[3].gsub('\]', ']')) : ''
         if !m[3].to_s.empty?
-          if link_attrs && (m[3].start_with?('"') || m[3].include?(','))
-            attrs = parse_attributes(sub_attributes(m[3].gsub('\]', ']')))
+          if use_link_attrs && (m[3].start_with?('"') || m[3].include?(','))
+            attrs = parse_attributes(sub_attributes(m[3].gsub('\]', ']')), [])
             text = attrs[1]
           else
             text = sub_attributes(m[3].gsub('\]', ']'))
           end
+
+          if text.end_with? '^'
+            text = text.chop
+            attrs ||= {}
+            attrs['window'] = '_blank' unless attrs.has_key?('window')
+          end
         else
           text = ''
         end
 
-        "#{prefix}#{Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target, :attributes => attrs).render}"
+        "#{prefix}#{Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target, :attributes => attrs).render}#{suffix}"
       }
     end
 
-    if found[:macroish] && result.include?('link:')
+    if found[:macroish] && (result.include?('link:') || result.include?('mailto:'))
       # inline link macros, link:target[text]
       result.gsub!(REGEXP[:link_macro]) {
         # alias match for Ruby 1.8.7 compat
@@ -404,19 +545,58 @@ module Substituters
         if m[0].start_with? '\\'
           next m[0][1..-1]
         end
-        target = m[1]
-        @document.register(:links, target)
+        raw_target = m[1]
+        mailto = m[0].start_with?('mailto:')
+        target = mailto ? "mailto:#{raw_target}" : raw_target
 
         attrs = nil
         #text = sub_attributes(m[2].gsub('\]', ']'))
-        if link_attrs && (m[2].start_with?('"') || m[2].include?(','))
-          attrs = parse_attributes(sub_attributes(m[2].gsub('\]', ']')))
+        if use_link_attrs && (m[2].start_with?('"') || m[2].include?(','))
+          attrs = parse_attributes(sub_attributes(m[2].gsub('\]', ']')), [])
           text = attrs[1]
+          if mailto
+            if attrs.has_key? 2
+              target = "#{target}?subject=#{Helpers.encode_uri(attrs[2])}"
+
+              if attrs.has_key? 3
+                target = "#{target}&body=#{Helpers.encode_uri(attrs[3])}"
+              end
+            end
+          end
         else
           text = sub_attributes(m[2].gsub('\]', ']'))
         end
 
-        Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target, :attributes => attrs).render
+        if text.end_with? '^'
+          text = text.chop
+          attrs ||= {}
+          attrs['window'] = '_blank' unless attrs.has_key?('window')
+        end
+
+        # QUESTION should a mailto be registered as an e-mail address?
+        @document.register(:links, target)
+
+        Inline.new(self, :anchor, (!text.empty? ? text : raw_target), :type => :link, :target => target, :attributes => attrs).render
+      }
+    end
+
+    if found[:at]
+      result.gsub!(REGEXP[:email_inline]) {
+        # alias match for Ruby 1.8.7 compat
+        m = $~
+        address = m[0]
+        case address[0..0]
+        when '\\'
+          next address[1..-1]
+        when '>', ':'
+          next address
+        end
+
+        target = "mailto:#{address}"
+        # QUESTION should this be registered as an e-mail address?
+        @document.register(:links, target)
+
+        Inline.new(self, :anchor, address, :type => :link, :target => target).render
       }
     end
 
@@ -437,7 +617,7 @@ module Substituters
           type = nil
           target = nil
         else
-          id, text = m[2].split(REGEXP[:csv_delimiter], 2)
+          id, text = m[2].split(',', 2).map(&:strip)
           if !text.nil?
             # hmmmm
             text = restore_passthroughs(text)
@@ -467,7 +647,7 @@ module Substituters
           next m[0][1..-1]
         end
         if !m[1].nil?
-          id, reftext = m[1].split(REGEXP[:csv_delimiter], 2)
+          id, reftext = m[1].split(',', 2).map(&:strip)
           id.sub!(REGEXP[:dbl_quoted], '\2')
           reftext.sub!(REGEXP[:m_dbl_quoted], '\2') unless reftext.nil?
         else
@@ -499,7 +679,7 @@ module Substituters
         if m[0].start_with? '\\'
           next m[0][1..-1]
         end
-        id, reftext = m[1].split(REGEXP[:csv_delimiter])
+        id, reftext = m[1].split(',').map(&:strip)
         id.sub!(REGEXP[:dbl_quoted], '\2')
         if reftext.nil?
           reftext = "[#{id}]"
@@ -573,11 +753,29 @@ module Substituters
   # posattrs  - The keys for positional attributes
   #
   # returns nil if attrline is nil, an empty Hash if attrline is empty, otherwise a Hash of parsed attributes
-  def parse_attributes(attrline, posattrs = ['role'])
+  def parse_attributes(attrline, posattrs = ['role'], opts = {})
     return nil if attrline.nil?
     return {} if attrline.empty?
+    attrline = @document.sub_attributes(attrline) if opts[:sub_input]
+    attrline = unescape_bracketed_text(attrline) if opts[:unescape_input]
+    block = nil
+    if opts.fetch(:sub_result, true)
+      # substitutions are only performed on attribute values if block is not nil
+      block = self
+    end
     
-    AttributeList.new(attrline, self).parse(posattrs)
+    if opts.has_key?(:into)
+      AttributeList.new(attrline, block).parse_into(opts[:into], posattrs)
+    else
+      AttributeList.new(attrline, block).parse(posattrs)
+    end
+  end
+
+  # Internal: Strip bounding whitespace, fold endlines and unescaped closing
+  # square brackets from text extracted from brackets
+  def unescape_bracketed_text(text)
+    return '' if text.empty?
+    text.strip.tr("\n", ' ').gsub('\]', ']')
   end
 
   # Internal: Resolve the list of comma-delimited subs against the possible options.
diff --git a/lib/asciidoctor/table.rb b/lib/asciidoctor/table.rb
index a6a0aea..b2a5927 100644
--- a/lib/asciidoctor/table.rb
+++ b/lib/asciidoctor/table.rb
@@ -44,12 +44,6 @@ class Table < AbstractBlock
     }
   }
 
-  # Public: A compiled Regexp to match a blank line
-  BLANK_LINE_PATTERN = /\n[[:blank:]]*\n/
-
-  # Public: Get/Set the String caption (unused, necessary for compatibility w/ next_block)
-  attr_accessor :caption
-
   # Public: Get/Set the columns for this table
   attr_accessor :columns
 
@@ -57,23 +51,24 @@ class Table < AbstractBlock
   # and body rows)
   attr_accessor :rows
 
+  # Public: Boolean specifies whether this table has a header row
+  attr_reader :header_option
+
   def initialize(parent, attributes)
     super(parent, :table)
-    # QUESTION since caption is on block, should it go to AbstractBlock?
-    @caption = nil
     @rows = Rows.new([], [], [])
     @columns = []
 
-    unless @attributes.has_key? 'tablepcwidth'
-      # smell like we need a utility method here
-      # to resolve an integer width from potential bogus input
-      pcwidth = attributes['width']
-      pcwidth_intval = pcwidth.to_i.abs
-      if pcwidth_intval == 0 && pcwidth != "0" || pcwidth_intval > 100
-        pcwidth_intval = 100
-      end
-      @attributes['tablepcwidth'] = pcwidth_intval
+    @has_header_option = attributes.has_key? 'header-option'
+
+    # smell like we need a utility method here
+    # to resolve an integer width from potential bogus input
+    pcwidth = attributes['width']
+    pcwidth_intval = pcwidth.to_i.abs
+    if pcwidth_intval == 0 && pcwidth != "0" || pcwidth_intval > 100
+      pcwidth_intval = 100
     end
+    @attributes['tablepcwidth'] = pcwidth_intval
 
     if @document.attributes.has_key? 'pagewidth'
       @attributes['tableabswidth'] ||=
@@ -81,6 +76,12 @@ class Table < AbstractBlock
     end
   end
 
+  # Internal: Returns whether the current row being processed is
+  # the header row
+  def header_row?
+    @has_header_option && @rows.body.size == 0
+  end
+
   # Internal: Creates the Column objects from the column spec
   #
   # returns nothing
@@ -130,9 +131,6 @@ class Table < AbstractBlock
   # rendered and returned as content that can be included in the
   # parent block's template.
   def render
-    Debug.debug { "Now attempting to render for table my own bad #{self}" }
-    Debug.debug { "Parent is #{@parent}" }
-    Debug.debug { "Renderer is #{renderer}" }
     @document.playback_attributes @attributes
     renderer.render('block_table', self) 
   end
@@ -154,6 +152,9 @@ class Table::Column < AbstractNode
     update_attributes(attributes)
   end
 
+  # Public: An alias to the parent block (which is always a Table)
+  alias :table :parent
+
   # Internal: Calculate and assign the widths (percentage and absolute) for this column
   #
   # This method assigns the colpcwidth and colabswidth attributes.
@@ -209,14 +210,19 @@ class Table::Cell < AbstractNode
       end
       update_attributes(attributes)
     end
-    if @attributes['style'] == :asciidoc
+    # only allow AsciiDoc cells in non-header rows
+    if @attributes['style'] == :asciidoc && !column.table.header_row?
+      # FIXME hide doctitle from nested document; temporary workaround to fix
+      # nested document seeing doctitle and assuming it has its own document title
+      parent_doctitle = @document.attributes.delete('doctitle')
       @inner_document = Document.new(@text, :header_footer => false, :parent => @document)
+      @document.attributes['doctitle'] = parent_doctitle unless parent_doctitle.nil?
     end
   end
 
   # Public: Get the text with normal substitutions applied for this cell. Used for cells in the head rows
   def text
-    apply_normal_subs(@text)
+    apply_normal_subs(@text).strip
   end
 
   # Public: Handles the body data (tbody, tfoot), applying styles and partitioning into paragraphs
@@ -225,7 +231,7 @@ class Table::Cell < AbstractNode
     if style == :asciidoc
       @inner_document.render
     else
-      text.split(Table::BLANK_LINE_PATTERN).map {|p|
+      text.split(BLANK_LINE_PATTERN).map {|p|
         !style || style == :header ? p : Inline.new(parent, :quoted, p, :type => attr('style')).render
       }
     end
@@ -314,7 +320,7 @@ class Table::ParserContext
   #
   # returns the String after the match
   def skip_matched_delimiter(match, escaped = false)
-    @buffer << (escaped ? match.pre_match.chop : match.pre_match) << @delimiter
+    @buffer = %(#@buffer#{escaped ? match.pre_match.chop : match.pre_match}#@delimiter)
     match.post_match
   end
 
diff --git a/lib/asciidoctor/version.rb b/lib/asciidoctor/version.rb
index 3a6f4b4..dbfdfef 100644
--- a/lib/asciidoctor/version.rb
+++ b/lib/asciidoctor/version.rb
@@ -1,3 +1,3 @@
 module Asciidoctor
-  VERSION = "0.1.1"
+  VERSION = '0.1.3'
 end
diff --git a/man/asciidoctor.1 b/man/asciidoctor.1
index 99cb903..f1b76b1 100644
--- a/man/asciidoctor.1
+++ b/man/asciidoctor.1
@@ -1,13 +1,13 @@
 '\" t
 .\"     Title: asciidoctor
 .\"    Author: [see the "AUTHORS" section]
-.\" Generator: DocBook XSL Stylesheets v1.76.1 <http://docbook.sf.net/>
-.\"      Date: 02/18/2013
+.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
+.\"      Date: 05/30/2013
 .\"    Manual: \ \&
 .\"    Source: \ \&
 .\"  Language: English
 .\"
-.TH "ASCIIDOCTOR" "1" "02/18/2013" "\ \&" "\ \&"
+.TH "ASCIIDOCTOR" "1" "05/30/2013" "\ \&" "\ \&"
 .\" -----------------------------------------------------------------
 .\" * Define some portability stuff
 .\" -----------------------------------------------------------------
@@ -71,20 +71,22 @@ when Asciidoctor is invoked using this script\&.
 Define, override or delete a document attribute\&. Command\-line attributes take precedence over attributes defined in the source file\&.
 .sp
 \fIATTRIBUTE\fR
-is formatted as a key\-value pair, in the form
-\fINAME=VALUE\fR\&. Values containing spaces should be enclosed in double\-quote characters\&. Alternate acceptable forms are
+is normally formatted as a key\-value pair, in the form
+\fINAME=VALUE\fR\&. Alternate acceptable forms are
 \fINAME\fR
-(the
+(where the
 \fIVALUE\fR
 defaults to an empty string),
 \fINAME!\fR
-(deletes the
+(unassigns the
 \fINAME\fR
 attribute) and
 \fINAME=VALUE@\fR
-(does not override
+(where
+\fIVALUE\fR
+does not override value of
 \fINAME\fR
-attribute if already defined in the source file)\&.
+attribute if it\(cqs already defined in the source document)\&. Values containing spaces should be enclosed in quotes\&.
 .sp
 This option may be specified more than once\&.
 .RE
@@ -94,28 +96,32 @@ This option may be specified more than once\&.
 Backend output file format:
 \fIdocbook45\fR
 or
-\fIhtml5\fR\&. You can also use the backend alias names
+\fIhtml5\fR
+supported out of the box\&. You can also use the backend alias names
 \fIhtml\fR
 (aliased to
 \fIhtml5\fR) or
 \fIdocbook\fR
 (aliased to
 \fIdocbook45\fR)\&. Defaults to
-\fIhtml5\fR\&.
+\fIhtml5\fR\&. Other options can be passed, but if Asciidoctor cannot find the backend, it will fail during rendering\&.
 .RE
 .PP
 \fB\-d, \-\-doctype\fR=\fIDOCTYPE\fR
 .RS 4
 Document type:
-\fIarticle\fR
+\fIarticle\fR,
+\fIbook\fR
 or
-\fIbook\fR\&. Sets the root element when using the
+\fIinline\fR\&. Sets the root element when using the
 \fIdocbook\fR
 backend and the style class on the HTML body element when using the
 \fIhtml\fR
 backend\&. The
 \fIbook\fR
-document type allows multiple level\-0 section titles in a single document\&. Defaults to
+document type allows multiple level\-0 section titles in a single document\&. The
+\fIinline\fR
+document type allows the content of a single paragraph to be formatted and returned without wrapping it in a containing element\&. Defaults to
 \fIarticle\fR\&.
 .RE
 .SS "Rendering Control"
@@ -127,7 +133,7 @@ Compact the output by removing blank lines\&. Not enabled by default\&.
 .PP
 \fB\-D, \-\-destination\-dir\fR=\fIDIR\fR
 .RS 4
-Destination output directory\&. Defaults to the directory containing the source file, or the working directory if the source is read from a stream\&.
+Destination output directory\&. Defaults to the directory containing the source file, or the working directory if the source is read from a stream\&. If specified, the directory is resolved relative to the working directory\&.
 .RE
 .PP
 \fB\-e, \-\-eruby\fR
@@ -154,7 +160,7 @@ extension\&. If the input is read from standard input, then the output file defa
 \fIOUT_FILE\fR
 is
 \fI\-\fR
-then the standard output is also used\&.
+then the standard output is also used\&. If specified, the file is resolved relative to the working directory\&.
 .RE
 .PP
 \fB\-s, \-\-no\-header\-footer\fR
@@ -164,7 +170,7 @@ Suppress the document header and footer in the output\&.
 .PP
 \fB\-T, \-\-template\-dir\fR=\fIDIR\fR
 .RS 4
-Directory containing custom render templates that override one or more templates from the the built\-in set\&. If there is a folder in the directory that matches the backend, the templates from that folder will be used\&.
+Directory containing custom render templates that override one or more templates from the built\-in set\&. If there is a folder in the directory that matches the backend, the templates from that folder will be used\&.
 .RE
 .SS "Processing Information"
 .PP
@@ -204,7 +210,7 @@ Failure (syntax or usage error; configuration error; document processing failure
 See the \fBAsciidoctor\fR issue tracker: <\fBhttps://github\&.com/asciidoctor/asciidoctor/issues?state=open\fR>
 .SH "AUTHORS"
 .sp
-\fBAsciidoctor\fR was written by Ryan Waldron, Dan Allen and other contributors\&.
+\fBAsciidoctor\fR was written by Dan Allen, Ryan Waldron, Jason Porter, Nick Hengeveld and other contributors\&.
 .sp
 \fBAsciiDoc\fR was written by Stuart Rackham and has received contributions from many other individuals\&.
 .SH "RESOURCES"
@@ -218,4 +224,4 @@ GitHub organization: <\fBhttp://github\&.com/asciidoctor\fR>
 Mailinglist / forum: <\fBhttp://discuss\&.asciidoctor\&.org\fR>
 .SH "COPYING"
 .sp
-Copyright (C) Ryan Waldron\&. Free use of this software is granted under the terms of the MIT License\&.
+Copyright (C) 2012\-2013 Dan Allen and Ryan Waldron\&. Free use of this software is granted under the terms of the MIT License\&.
diff --git a/man/asciidoctor.ad b/man/asciidoctor.ad
index fe6bc45..7cdb428 100644
--- a/man/asciidoctor.ad
+++ b/man/asciidoctor.ad
@@ -1,6 +1,7 @@
 asciidoctor(1)
 ==============
 :doctype: manpage
+:awestruct-layout: base
 
 
 NAME
@@ -52,24 +53,28 @@ Document Settings
     Define, override or delete a document attribute. Command-line attributes
     take precedence over attributes defined in the source file.
 +
-'ATTRIBUTE' is formatted as a key-value pair, in the form 'NAME=VALUE'. Values
-containing spaces should be enclosed in double-quote characters.  Alternate
-acceptable forms are 'NAME' (the 'VALUE' defaults to an empty string), 'NAME!'
-(deletes the 'NAME' attribute) and 'NAME=VALUE@' (does not override 'NAME'
-attribute if already defined in the source file).
+'ATTRIBUTE' is normally formatted as a key-value pair, in the form 'NAME=VALUE'.
+Alternate acceptable forms are 'NAME' (where the 'VALUE' defaults to an empty
+string), 'NAME!' (unassigns the 'NAME' attribute) and 'NAME=VALUE@' (where
+'VALUE' does not override value of 'NAME' attribute if it's already defined in
+the source document). Values containing spaces should be enclosed in quotes.
 +
 This option may be specified more than once. 
 
 *-b, --backend*='BACKEND'::
-    Backend output file format: 'docbook45' or 'html5'.  You can also use the
-    backend alias names 'html' (aliased to 'html5') or 'docbook' (aliased to
-    'docbook45'). Defaults to 'html5'.
+    Backend output file format: 'docbook45' or 'html5' supported out of the box.  
+    You can also use the backend alias names 'html' (aliased to 'html5') or 
+    'docbook' (aliased to 'docbook45'). Defaults to 'html5'. Other options can
+    be passed, but if Asciidoctor cannot find the backend, it will fail during
+    rendering.
 
 *-d, --doctype*='DOCTYPE'::
-    Document type: 'article' or 'book'. Sets the root element when using the
-    'docbook' backend and the style class on the HTML body element when using
-    the 'html' backend. The 'book' document type allows multiple level-0
-    section titles in a single document. Defaults to 'article'.
+    Document type: 'article', 'book' or 'inline'. Sets the root element when
+    using the 'docbook' backend and the style class on the HTML body element
+    when using the 'html' backend. The 'book' document type allows multiple
+    level-0 section titles in a single document. The 'inline' document type
+    allows the content of a single paragraph to be formatted and returned
+    without wrapping it in a containing element. Defaults to 'article'.
 
 Rendering Control
 ~~~~~~~~~~~~~~~~~
@@ -80,6 +85,7 @@ Rendering Control
 *-D, --destination-dir*='DIR'::
     Destination output directory. Defaults to the directory containing the
     source file, or the working directory if the source is read from a stream.
+    If specified, the directory is resolved relative to the working directory.
 
 *-e, --eruby*::
     Specifies the eRuby implementation to use for rendering the built-in
@@ -92,14 +98,15 @@ Rendering Control
     Write output to file 'OUT_FILE'. Defaults to the base name of the input
     file suffixed with 'backend' extension. If the input is read from standard
     input, then the output file defaults to stdout. If 'OUT_FILE' is '-' then
-    the standard output is also used.
+    the standard output is also used. If specified, the file is resolved
+    relative to the working directory.
 
 *-s, --no-header-footer*::
     Suppress the document header and footer in the output.
 
 *-T, --template-dir*='DIR'::
     Directory containing custom render templates that override one or more
-    templates from the the built-in set. If there is a folder in the directory
+    templates from the built-in set. If there is a folder in the directory
     that matches the backend, the templates from that folder will be used.
 
 Processing Information
@@ -139,7 +146,8 @@ See the *Asciidoctor* issue tracker: <**https://github.com/asciidoctor/asciidoct
 
 AUTHORS
 -------
-*Asciidoctor* was written by Ryan Waldron, Dan Allen and other contributors.
+*Asciidoctor* was written by Dan Allen, Ryan Waldron, Jason Porter, Nick
+Hengeveld and other contributors.
 
 *AsciiDoc* was written by Stuart Rackham and has received contributions from
 many other individuals.
@@ -158,5 +166,7 @@ Mailinglist / forum: <**http://discuss.asciidoctor.org**>
 
 COPYING
 -------
-Copyright \(C) Ryan Waldron. Free use of this software is granted under the
-terms of the MIT License.
+Copyright \(C) 2012-2013 Dan Allen and Ryan Waldron. Free use of this
+software is granted under the terms of the MIT License.
+
+// vim: tw=80
diff --git a/test/attributes_test.rb b/test/attributes_test.rb
index 5584c41..b90bd53 100644
--- a/test/attributes_test.rb
+++ b/test/attributes_test.rb
@@ -181,6 +181,69 @@ Yo, {myfrog}!
       assert_match(/_cool_title/, result.css('h2').first.attr('id'))
     end
 
+    test 'interpolates attribute defined in header inside attribute entry in header' do
+      input = <<-EOS
+= Title
+Author Name
+:attribute-a: value
+:attribute-b: {attribute-a}
+
+preamble
+      EOS
+      doc = document_from_string(input, :parse_header_only => true)
+      assert_equal 'value', doc.attributes['attribute-b']
+    end
+
+    test 'interpolates author attribute inside attribute entry in header' do
+      input = <<-EOS
+= Title
+Author Name
+:name: {author}
+
+preamble
+      EOS
+      doc = document_from_string(input, :parse_header_only => true)
+      assert_equal 'Author Name', doc.attributes['name']
+    end
+
+    test 'interpolates revinfo attribute inside attribute entry in header' do
+      input = <<-EOS
+= Title
+Author Name
+2013-01-01
+:date: {revdate}
+
+preamble
+      EOS
+      doc = document_from_string(input, :parse_header_only => true)
+      assert_equal '2013-01-01', doc.attributes['date']
+    end
+
+    test 'attribute entries can resolve previously defined attributes' do
+      input = <<-EOS
+= Title
+Author Name
+v1.0, 2010-01-01: First release!
+:a: value
+:a2: {a}
+:revdate2: {revdate}
+
+{a} == {a2}
+
+{revdate} == {revdate2}
+      EOS
+
+      doc = document_from_string input
+      assert_equal '2010-01-01', doc.attr('revdate')
+      assert_equal '2010-01-01', doc.attr('revdate2')
+      assert_equal 'value', doc.attr('a')
+      assert_equal 'value', doc.attr('a2')
+
+      output = doc.render
+      assert output.include?('value == value')
+      assert output.include?('2010-01-01 == 2010-01-01')
+    end
+
     test 'substitutes inside block title' do
       input = <<-EOS
 :gem_name: asciidoctor
@@ -189,7 +252,7 @@ Yo, {myfrog}!
 To use {gem_name}, the first thing to do is to import it in your Ruby source file.
       EOS
       output = render_embedded_string input
-      assert_xpath '//*[@class="title"]/tt[text()="asciidoctor"]', output, 1
+      assert_xpath '//*[@class="title"]/code[text()="asciidoctor"]', output, 1
     end
 
     test 'renders attribute until it is deleted' do
@@ -269,6 +332,34 @@ of the attribute named foo in your document.
       assert_xpath %(//li[1]/p[text()="docdir: #{docdir}"]), output, 1
       assert_xpath %(//li[2]/p[text()="docfile: #{docfile}"]), output, 1
     end
+
+    test 'assigns attribute defined in attribute reference with set prefix and value' do
+      input = '{set:foo:bar}{foo}' 
+      output = render_embedded_string input 
+      assert_xpath '//p', output, 1
+      assert_xpath '//p[text()="bar"]', output, 1
+    end
+
+    test 'assigns attribute defined in attribute reference with set prefix and no value' do
+      input = "{set:foo}\n{foo}yes"
+      output = render_embedded_string input 
+      assert_xpath '//p', output, 1
+      assert_xpath '//p[normalize-space(text())="yes"]', output, 1
+    end
+
+    test 'assigns attribute defined in attribute reference with set prefix and empty value' do
+      input = "{set:foo:}\n{foo}yes"
+      output = render_embedded_string input 
+      assert_xpath '//p', output, 1
+      assert_xpath '//p[normalize-space(text())="yes"]', output, 1
+    end
+
+    test 'unassigns attribute defined in attribute reference with set prefix' do
+      input = ":foo:\n\n{set:foo!}\n{foo}yes"
+      output = render_embedded_string input
+      assert_xpath '//p', output, 1
+      assert_xpath '//p/child::text()', output, 0
+    end
   end
 
   context "Intrinsic attributes" do
@@ -365,10 +456,10 @@ of the attribute named foo in your document.
     
   end
 
-  context "Block attributes" do
-    test "Position attributes assigned to block" do
+  context 'Block attributes' do
+    test 'Positional attributes assigned to block' do
       input = <<-EOS
-[quote, Name, Source]
+[quote, author, source]
 ____
 A famous quote.
 ____
@@ -377,13 +468,13 @@ ____
       qb = doc.blocks.first
       assert_equal 'quote', qb.attributes['style']
       assert_equal 'quote', qb.attr(:style)
-      assert_equal 'Name', qb.attributes['attribution']
-      assert_equal 'Source', qb.attributes['citetitle']
+      assert_equal 'author', qb.attributes['attribution']
+      assert_equal 'source', qb.attributes['citetitle']
     end
 
-    test "Normal substitutions are performed on single-quoted attributes" do
+    test 'Normal substitutions are performed on single-quoted attributes' do
       input = <<-EOS
-[quote, Name, 'http://wikipedia.org[Source]']
+[quote, author, 'http://wikipedia.org[source]']
 ____
 A famous quote.
 ____
@@ -392,8 +483,68 @@ ____
       qb = doc.blocks.first
       assert_equal 'quote', qb.attributes['style']
       assert_equal 'quote', qb.attr(:style)
-      assert_equal 'Name', qb.attributes['attribution']
-      assert_equal '<a href="http://wikipedia.org">Source</a>', qb.attributes['citetitle']
+      assert_equal 'author', qb.attributes['attribution']
+      assert_equal '<a href="http://wikipedia.org">source</a>', qb.attributes['citetitle']
+    end
+
+    test 'attribute list may begin with space' do
+      input = <<-EOS
+[ quote]
+____
+A famous quote.
+____
+      EOS
+
+      doc = document_from_string input
+      qb = doc.blocks.first
+      assert_equal 'quote', qb.attributes['style']
+    end
+
+    test 'attribute list may begin with comma' do
+      input = <<-EOS
+[, author, source]
+____
+A famous quote.
+____
+      EOS
+
+      doc = document_from_string input
+      qb = doc.blocks.first
+      assert_equal 'quote', qb.attributes['style']
+      assert_equal 'author', qb.attributes['attribution']
+      assert_equal 'source', qb.attributes['citetitle']
+    end
+
+    test 'first attribute in list may be double quoted' do
+      input = <<-EOS
+["quote", "author", "source", role="famous"]
+____
+A famous quote.
+____
+      EOS
+
+      doc = document_from_string input
+      qb = doc.blocks.first
+      assert_equal 'quote', qb.attributes['style']
+      assert_equal 'author', qb.attributes['attribution']
+      assert_equal 'source', qb.attributes['citetitle']
+      assert_equal 'famous', qb.attributes['role']
+    end
+
+    test 'first attribute in list may be single quoted' do
+      input = <<-EOS
+['quote', 'author', 'source', role='famous']
+____
+A famous quote.
+____
+      EOS
+
+      doc = document_from_string input
+      qb = doc.blocks.first
+      assert_equal 'quote', qb.attributes['style']
+      assert_equal 'author', qb.attributes['attribution']
+      assert_equal 'source', qb.attributes['citetitle']
+      assert_equal 'famous', qb.attributes['role']
     end
 
     test "Attribute substitutions are performed on attribute list before parsing attributes" do
@@ -408,6 +559,28 @@ A paragraph
       assert_equal 'lead', para.attributes['role']
     end
 
+    test 'id and role attributes can be specified on block style using shorthand syntax' do
+      input = <<-EOS
+[normal#first.lead]
+A normal paragraph.
+      EOS
+      doc = document_from_string(input)
+      para = doc.blocks.first
+      assert_equal 'first', para.attributes['id']
+      assert_equal 'lead', para.attributes['role']
+    end
+
+    test 'id and role attributes can be specified on section style using shorthand syntax' do
+      input = <<-EOS
+[dedication#dedication.small]
+== Section
+Content.
+      EOS
+      output = render_embedded_string input
+      assert_xpath '/div[@class="sect1 small"]', output, 1
+      assert_xpath '/div[@class="sect1 small"]/h2[@id="dedication"]', output, 1
+    end
+
     test "Block attributes are additive" do
       input = <<-EOS
 [id='foo']
diff --git a/test/blocks_test.rb b/test/blocks_test.rb
index b13af10..a98a9fe 100644
--- a/test/blocks_test.rb
+++ b/test/blocks_test.rb
@@ -18,9 +18,9 @@ context "Blocks" do
 
     test "page break" do
       output = render_embedded_string("page 1\n\n<<<\n\npage 2")
-      assert_xpath '/*[@style="page-break-after: always"]', output, 1
-      assert_xpath '/*[@style="page-break-after: always"]/preceding-sibling::div/p[text()="page 1"]', output, 1
-      assert_xpath '/*[@style="page-break-after: always"]/following-sibling::div/p[text()="page 2"]', output, 1
+      assert_xpath '/*[@style="page-break-after: always;"]', output, 1
+      assert_xpath '/*[@style="page-break-after: always;"]/preceding-sibling::div/p[text()="page 1"]', output, 1
+      assert_xpath '/*[@style="page-break-after: always;"]/following-sibling::div/p[text()="page 2"]', output, 1
     end
   end
 
@@ -117,6 +117,252 @@ block comment
     end
   end
 
+  context 'Quote and Verse Blocks' do
+    test 'quote block with no attribution' do
+      input = <<-EOS
+____
+A famous quote.
+____
+      EOS
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph > p', output, 1
+      assert_css '.quoteblock > .attribution', output, 0
+      assert_xpath '//*[@class = "quoteblock"]//p[text() = "A famous quote."]', output, 1
+    end
+
+    test 'quote block with attribution' do
+      input = <<-EOS
+[quote, Famous Person, Famous Book (1999)]
+____
+A famous quote.
+____
+      EOS
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph > p', output, 1
+      assert_css '.quoteblock > .attribution', output, 1
+      assert_css '.quoteblock > .attribution > cite', output, 1
+      assert_css '.quoteblock > .attribution > cite + br', output, 1
+      assert_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]/cite[text() = "Famous Book (1999)"]', output, 1
+      attribution = xmlnodes_at_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]', output, 1
+      author = attribution.children.last
+      assert_equal "#{expand_entity 8212} Famous Person", author.text.strip
+    end
+
+    test 'quote block with attribute and id and role shorthand' do
+      input = <<-EOS
+[quote#think.big, Donald Trump]
+____
+As long as your going to be thinking anyway, think big.
+____
+      EOS
+
+      output = render_embedded_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '#think.quoteblock.big', output, 1
+      assert_css '.quoteblock > .attribution', output, 1
+    end
+
+    test 'quote block with complex content' do
+      input = <<-EOS
+____
+A famous quote.
+
+NOTE: _That_ was inspiring.
+____
+      EOS
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph + .admonitionblock', output, 1
+    end
+
+    test 'quote block using air quotes with no attribution' do
+      input = <<-EOS
+""
+A famous quote.
+""
+      EOS
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph > p', output, 1
+      assert_css '.quoteblock > .attribution', output, 0
+      assert_xpath '//*[@class = "quoteblock"]//p[text() = "A famous quote."]', output, 1
+    end
+
+    test 'markdown-style quote block with single paragraph and no attribution' do
+      input = <<-EOS
+> A famous quote.
+> Some more inspiring words.
+      EOS
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph > p', output, 1
+      assert_css '.quoteblock > .attribution', output, 0
+      assert_xpath %(//*[@class = "quoteblock"]//p[text() = "A famous quote.\nSome more inspiring words."]), output, 1
+    end
+
+    test 'lazy markdown-style quote block with single paragraph and no attribution' do
+      input = <<-EOS
+> A famous quote.
+Some more inspiring words.
+      EOS
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph > p', output, 1
+      assert_css '.quoteblock > .attribution', output, 0
+      assert_xpath %(//*[@class = "quoteblock"]//p[text() = "A famous quote.\nSome more inspiring words."]), output, 1
+    end
+
+    test 'markdown-style quote block with multiple paragraphs and no attribution' do
+      input = <<-EOS
+> A famous quote.
+>
+> Some more inspiring words.
+      EOS
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph > p', output, 2
+      assert_css '.quoteblock > .attribution', output, 0
+      assert_xpath %((//*[@class = "quoteblock"]//p)[1][text() = "A famous quote."]), output, 1
+      assert_xpath %((//*[@class = "quoteblock"]//p)[2][text() = "Some more inspiring words."]), output, 1
+    end
+
+    test 'markdown-style quote block with multiple blocks and no attribution' do
+      input = <<-EOS
+> A famous quote.
+>
+> NOTE: Some more inspiring words.
+      EOS
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph > p', output, 1
+      assert_css '.quoteblock > blockquote > .admonitionblock', output, 1
+      assert_css '.quoteblock > .attribution', output, 0
+      assert_xpath %((//*[@class = "quoteblock"]//p)[1][text() = "A famous quote."]), output, 1
+      assert_xpath %((//*[@class = "quoteblock"]//*[@class = "admonitionblock note"]//*[@class="content"])[1][normalize-space(text()) = "Some more inspiring words."]), output, 1
+    end
+
+    test 'markdown-style quote block with single paragraph and attribution' do
+      input = <<-EOS
+> A famous quote.
+> Some more inspiring words.
+> -- Famous Person, Famous Source (1999)
+      EOS
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph > p', output, 1
+      assert_xpath %(//*[@class = "quoteblock"]//p[text() = "A famous quote.\nSome more inspiring words."]), output, 1
+      assert_css '.quoteblock > .attribution', output, 1
+      assert_css '.quoteblock > .attribution > cite', output, 1
+      assert_css '.quoteblock > .attribution > cite + br', output, 1
+      assert_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]/cite[text() = "Famous Source (1999)"]', output, 1
+      attribution = xmlnodes_at_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]', output, 1
+      author = attribution.children.last
+      assert_equal "#{expand_entity 8212} Famous Person", author.text.strip
+    end
+
+    test 'quoted paragraph-style quote block with attribution' do
+      input = <<-EOS
+"A famous quote.
+Some more inspiring words."
+-- Famous Person, Famous Source (1999)
+      EOS
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_xpath %(//*[@class = "quoteblock"]/blockquote[normalize-space(text()) = "A famous quote. Some more inspiring words."]), output, 1
+      assert_css '.quoteblock > .attribution', output, 1
+      assert_css '.quoteblock > .attribution > cite', output, 1
+      assert_css '.quoteblock > .attribution > cite + br', output, 1
+      assert_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]/cite[text() = "Famous Source (1999)"]', output, 1
+      attribution = xmlnodes_at_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]', output, 1
+      author = attribution.children.last
+      assert_equal "#{expand_entity 8212} Famous Person", author.text.strip
+    end
+
+    test 'single-line verse block without attribution' do
+      input = <<-EOS
+[verse]
+____
+A famous verse.
+____
+      EOS
+      output = render_string input
+      assert_css '.verseblock', output, 1
+      assert_css '.verseblock > pre', output, 1
+      assert_css '.verseblock > .attribution', output, 0
+      assert_css '.verseblock p', output, 0
+      assert_xpath '//*[@class = "verseblock"]/pre[normalize-space(text()) = "A famous verse."]', output, 1
+    end
+
+    test 'single-line verse block with attribution' do
+      input = <<-EOS
+[verse, Famous Poet, Famous Poem]
+____
+A famous verse.
+____
+      EOS
+      output = render_string input
+      assert_css '.verseblock', output, 1
+      assert_css '.verseblock p', output, 0
+      assert_css '.verseblock > pre', output, 1
+      assert_css '.verseblock > .attribution', output, 1
+      assert_css '.verseblock > .attribution > cite', output, 1
+      assert_css '.verseblock > .attribution > cite + br', output, 1
+      assert_xpath '//*[@class = "verseblock"]/*[@class = "attribution"]/cite[text() = "Famous Poem"]', output, 1
+      attribution = xmlnodes_at_xpath '//*[@class = "verseblock"]/*[@class = "attribution"]', output, 1
+      author = attribution.children.last
+      assert_equal "#{expand_entity 8212} Famous Poet", author.text.strip
+    end
+
+    test 'multi-stanza verse block' do
+      input = <<-EOS
+[verse]
+____
+A famous verse.
+
+Stanza two.
+____
+      EOS
+      output = render_string input
+      assert_xpath '//*[@class = "verseblock"]', output, 1
+      assert_xpath '//*[@class = "verseblock"]/pre', output, 1
+      assert_xpath '//*[@class = "verseblock"]//p', output, 0
+      assert_xpath '//*[@class = "verseblock"]/pre[contains(text(), "A famous verse.")]', output, 1
+      assert_xpath '//*[@class = "verseblock"]/pre[contains(text(), "Stanza two.")]', output, 1
+    end
+
+    test 'verse block does not contain block elements' do
+      input = <<-EOS
+[verse]
+____
+A famous verse.
+
+....
+not a literal
+....
+____
+      EOS
+      output = render_string input
+      assert_css '.verseblock', output, 1
+      assert_css '.verseblock > pre', output, 1
+      assert_css '.verseblock p', output, 0
+      assert_css '.verseblock .literalblock', output, 0
+    end
+    
+  end
+
   context "Example Blocks" do
     test "can render example block" do
       input = <<-EOS
@@ -198,6 +444,20 @@ You just write.
       assert !doc.attributes.has_key?('example-number')
     end
 
+    test 'explicit caption is set on block even if block has no title' do
+      input = <<-EOS
+[caption="Look!"]
+====
+Just write.
+====
+      EOS
+
+      doc = document_from_string input
+      assert_equal 'Look!', doc.blocks.first.caption
+      output = doc.render
+      assert_no_match(/Look/, output)
+    end
+
     test 'automatic caption can be turned off and on and modified' do
       input = <<-EOS
 .first example
@@ -239,7 +499,7 @@ TIP: Override the caption of an admonition block using an attribute entry
        EOS
 
        output = render_embedded_string input
-       assert_xpath '/*[@class="admonitionblock"]//*[@class="icon"]/*[@class="title"][text()="Pro Tip"]', output, 1
+       assert_xpath '/*[@class="admonitionblock tip"]//*[@class="icon"]/*[@class="title"][text()="Pro Tip"]', output, 1
     end
 
     test 'can override caption of admonition block using document attribute' do
@@ -250,7 +510,7 @@ TIP: Override the caption of an admonition block using an attribute entry
        EOS
 
        output = render_embedded_string input
-       assert_xpath '/*[@class="admonitionblock"]//*[@class="icon"]/*[@class="title"][text()="Pro Tip"]', output, 1
+       assert_xpath '/*[@class="admonitionblock tip"]//*[@class="icon"]/*[@class="title"][text()="Pro Tip"]', output, 1
     end
 
     test 'blank caption document attribute should not blank admonition block caption' do
@@ -261,7 +521,7 @@ TIP: Override the caption of an admonition block using an attribute entry
        EOS
 
        output = render_embedded_string input
-       assert_xpath '/*[@class="admonitionblock"]//*[@class="icon"]/*[@class="title"][text()="Tip"]', output, 1
+       assert_xpath '/*[@class="admonitionblock tip"]//*[@class="icon"]/*[@class="title"][text()="Tip"]', output, 1
     end
   end
 
@@ -367,6 +627,23 @@ EOS
       }
     end
 
+    test 'should not compact nested document twice' do
+      input = <<-EOS
+|===
+a|....
+line one
+
+line two
+
+line three
+....
+|===
+      EOS
+
+      output = render_string input, :compact => true
+      assert_xpath %(//pre[text() = "line one\n\nline two\n\nline three"]), output, 1
+    end
+
     test 'should process block with CRLF endlines' do
       input = <<-EOS
 [source]\r
@@ -382,6 +659,160 @@ source line 2\r
       assert_xpath '/*[@class="listingblock"]//pre/code', output, 1
       assert_xpath %(/*[@class="listingblock"]//pre/code[text()="source line 1\nsource line 2"]), output, 1
     end
+
+    test 'should remove block indent if indent attribute is 0' do
+      input = <<-EOS
+[indent="0"]
+----
+    def names
+
+      @names.split ' '
+
+    end
+----
+      EOS
+
+      expected = <<-EOS
+def names
+
+  @names.split ' '
+
+end
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'pre', output, 1
+      assert_css '.listingblock pre', output, 1
+      result = xmlnodes_at_xpath('//pre', output, 1).text
+      assert_equal expected.chomp, result
+    end
+
+    test 'should set block indent to value specified by indent attribute' do
+      input = <<-EOS
+[indent="1"]
+----
+    def names
+
+      @names.split ' '
+
+    end
+----
+      EOS
+
+      expected = <<-EOS
+ def names
+ 
+   @names.split ' '
+ 
+ end
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'pre', output, 1
+      assert_css '.listingblock pre', output, 1
+      result = xmlnodes_at_xpath('//pre', output, 1).text
+      assert_equal expected.chomp, result
+    end
+
+    test 'literal block should honor explicit subs list' do
+      input = <<-EOS
+[subs="verbatim,quotes"]
+----
+Map<String, String> *attributes*; //<1>
+----
+      EOS
+
+      output = render_embedded_string input
+      assert output.include?('Map<String, String> <strong>attributes</strong>;')
+      assert output.include?('1')
+    end
+
+    test 'listing block should honor explicit subs list' do
+      input = <<-EOS
+[subs="specialcharacters,quotes"]
+----
+$ *python functional_tests.py*
+Traceback (most recent call last):
+  File "functional_tests.py", line 4, in <module>
+    assert 'Django' in browser.title
+AssertionError
+----
+      EOS
+
+      output = render_embedded_string input
+
+      assert_css '.listingblock pre', output, 1
+      assert_css '.listingblock pre strong', output, 1
+      assert_css '.listingblock pre em', output, 1
+
+      input2 = <<-EOS
+[subs="specialcharacters,macros"]
+----
+$ pass:quotes[*python functional_tests.py*]
+Traceback (most recent call last):
+  File "functional_tests.py", line 4, in <module>
+    assert pass:quotes['Django'] in browser.title
+AssertionError
+----
+      EOS
+
+      output2 = render_embedded_string input2
+      # FIXME JRuby is adding extra trailing endlines in the second document,
+      # so rstrip is necessary
+      assert_equal output.rstrip, output2.rstrip
+    end
+
+    test 'listing block without title should generate screen element in docbook' do
+      input = <<-EOS
+----
+listing block
+----
+      EOS
+
+      output = render_embedded_string input, :backend => 'docbook'
+      assert_xpath '/screen[text()="listing block"]', output, 1
+    end
+
+    test 'listing block with title should generate screen element inside formalpara element in docbook' do
+      input = <<-EOS
+.title
+----
+listing block
+----
+      EOS
+
+      output = render_embedded_string input, :backend => 'docbook'
+      assert_xpath '/formalpara', output, 1
+      assert_xpath '/formalpara/title[text()="title"]', output, 1
+      assert_xpath '/formalpara/para/screen[text()="listing block"]', output, 1
+    end
+
+    test 'source block with no title or language should generate screen element in docbook' do
+      input = <<-EOS
+[source]
+----
+listing block
+----
+      EOS
+
+      output = render_embedded_string input, :backend => 'docbook'
+      assert_xpath '/screen[text()="listing block"]', output, 1
+    end
+
+    test 'source block with title and no language should generate screen element inside formalpara element in docbook' do
+      input = <<-EOS
+[source]
+.title
+----
+listing block
+----
+      EOS
+
+      output = render_embedded_string input, :backend => 'docbook'
+      assert_xpath '/formalpara', output, 1
+      assert_xpath '/formalpara/title[text()="title"]', output, 1
+      assert_xpath '/formalpara/para/screen[text()="listing block"]', output, 1
+    end
   end
 
   context "Open Blocks" do
@@ -477,15 +908,25 @@ paragraph
       assert_xpath '//*[@class="paragraph"]/p[text() = "paragraph"]', output, 1
     end
 
-    test 'block title above document title gets carried over to preamble' do
+    test 'block title above document title demotes document title to a section title' do
       input = <<-EOS
 .Block title
-= Document Title
+= Section Title
 
-preamble
+section paragraph
       EOS
-      output = render_string input
-      assert_xpath '//*[@id="preamble"]//*[@class="paragraph"]/*[@class="title"][text()="Block title"]', output, 1
+      output, errors = nil
+      redirect_streams do |stdout, stderr|
+        output = render_string input
+        errors = stdout.string
+      end
+      assert_xpath '//*[@id="header"]/*', output, 0
+      assert_xpath '//*[@id="preamble"]/*', output, 0
+      assert_xpath '//*[@id="content"]/h1[text()="Section Title"]', output, 1
+      assert_xpath '//*[@class="paragraph"]', output, 1
+      assert_xpath '//*[@class="paragraph"]/*[@class="title"][text()="Block title"]', output, 1
+      assert !errors.empty?
+      assert_match(/only book doctypes can contain level 0 sections/, errors)
     end
 
     test 'block title above document title gets carried over to first block in first section if no preamble' do
@@ -500,10 +941,36 @@ paragraph
       output = render_string input
       assert_xpath '//*[@class="sect1"]//*[@class="paragraph"]/*[@class="title"][text() = "Block title"]', output, 1
     end
+
+    test 'empty attribute list should not appear in output' do
+      input = <<-EOS
+[]
+--
+Block content
+--
+      EOS
+
+      output = render_embedded_string input
+      assert output.include?('Block content')
+      assert !output.include?('[]')
+    end
+
+    test 'empty block anchor should not appear in output' do
+      input = <<-EOS
+[[]]
+--
+Block content
+--
+      EOS
+
+      output = render_embedded_string input
+      assert output.include?('Block content')
+      assert !output.include?('[[]]')
+    end
   end
 
-  context "Images" do
-    test "can render block image with alt text" do
+  context 'Images' do
+    test 'can render block image with alt text defined in macro' do
       input = <<-EOS
 image::images/tiger.png[Tiger]
       EOS
@@ -512,6 +979,36 @@ image::images/tiger.png[Tiger]
       assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1
     end
 
+    test 'can render block image with alt text defined in macro containing escaped square bracket' do
+      input = <<-EOS
+image::images/tiger.png[A [Bengal\\] Tiger]
+      EOS
+
+      output = render_string input
+      img = xmlnodes_at_xpath '//img', output, 1
+      assert_equal 'A [Bengal] Tiger', img.attr('alt').value
+    end
+
+    test 'can render block image with alt text defined in block attribute above macro' do
+      input = <<-EOS
+[Tiger]
+image::images/tiger.png[]
+      EOS
+
+      output = render_string input
+      assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1
+    end
+
+    test 'alt text in macro overrides alt text above macro' do
+      input = <<-EOS
+[Alt Text]
+image::images/tiger.png[Tiger]
+      EOS
+
+      output = render_string input
+      assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1
+    end
+
     test "can render block image with auto-generated alt text" do
       input = <<-EOS
 image::images/tiger.png[]
@@ -552,7 +1049,43 @@ image::images/tiger.png[Tiger]
       assert_equal 1, doc.attributes['figure-number']
     end
 
-    test 'should pass through image that is a uri reference' do
+    test 'can render block image with explicit caption' do
+      input = <<-EOS
+[caption="Voila! "]
+.The AsciiDoc Tiger
+image::images/tiger.png[Tiger]
+      EOS
+
+      doc = document_from_string input
+      output = doc.render
+      assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1
+      assert_xpath '//*[@class="imageblock"]/*[@class="title"][text() = "Voila! The AsciiDoc Tiger"]', output, 1
+      assert !doc.attributes.has_key?('figure-number')
+    end
+
+    test 'drops line if image target is missing attribute reference' do
+      input = <<-EOS
+image::{bogus}[]
+      EOS
+
+      output = render_embedded_string input
+      assert output.strip.empty?
+    end
+
+    test 'dropped image does not break processing of following section' do
+      input = <<-EOS
+image::{bogus}[]
+
+== Section Title
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'img', output, 0
+      assert_css 'h2', output, 1 
+      assert !output.include?('== Section Title')
+    end
+
+    test 'should pass through image that references uri' do
       input = <<-EOS
 :imagesdir: images
 
@@ -589,19 +1122,147 @@ image::dot.gif[Dot]
     end
 
     # this test will cause a warning to be printed to the console (until we have a message facility)
-    test 'cleans reference to ancestor directories before reading image if safe mode level is at least SAFE' do
+    test 'cleans reference to ancestor directories in imagesdir before reading image if safe mode level is at least SAFE' do
       input = <<-EOS
 :data-uri:
-:imagesdir: ../fixtures
+:imagesdir: ../..//fixtures/./../../fixtures
 
 image::dot.gif[Dot]
       EOS
 
       doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
-      assert_equal '../fixtures', doc.attributes['imagesdir']
+      assert_equal '../..//fixtures/./../../fixtures', doc.attributes['imagesdir']
       output = doc.render
+      # image target resolves to fixtures/dot.gif relative to docdir (which is explicitly set to the directory of this file)
+      # the reference cannot fall outside of the document directory in safe mode
       assert_xpath '//*[@class="imageblock"]//img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Dot"]', output, 1
     end
+
+    test 'cleans reference to ancestor directories in target before reading image if safe mode level is at least SAFE' do
+      input = <<-EOS
+:data-uri:
+:imagesdir: ./
+
+image::../..//fixtures/./../../fixtures/dot.gif[Dot]
+      EOS
+
+      doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
+      assert_equal './', doc.attributes['imagesdir']
+      output = doc.render
+      # image target resolves to fixtures/dot.gif relative to docdir (which is explicitly set to the directory of this file)
+      # the reference cannot fall outside of the document directory in safe mode
+      assert_xpath '//*[@class="imageblock"]//img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Dot"]', output, 1
+    end
+  end
+
+  context 'Media' do
+    test 'should detect and render video macro' do
+      input = <<-EOS
+video::cats-vs-dogs.avi[]
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'video', output, 1
+      assert_css 'video[src="cats-vs-dogs.avi"]', output, 1
+    end
+
+    test 'should detect and render video macro with positional attributes for poster and dimensions' do
+      input = <<-EOS
+video::cats-vs-dogs.avi[cats-and-dogs.png, 200, 300]
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'video', output, 1
+      assert_css 'video[src="cats-vs-dogs.avi"]', output, 1
+      assert_css 'video[poster="cats-and-dogs.png"]', output, 1
+      assert_css 'video[width="200"]', output, 1
+      assert_css 'video[height="300"]', output, 1
+    end
+
+    test 'video macro should honor all options' do
+      input = <<-EOS
+video::cats-vs-dogs.avi[options="autoplay,nocontrols,loop"]
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'video', output, 1
+      assert_css 'video[autoplay]', output, 1
+      assert_css 'video:not([controls])', output, 1
+      assert_css 'video[loop]', output, 1
+    end
+
+    test 'video macro should use imagesdir attribute to resolve target and poster' do
+      input = <<-EOS
+:imagesdir: assets
+
+video::cats-vs-dogs.avi[cats-and-dogs.png, 200, 300]
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'video', output, 1
+      assert_css 'video[src="assets/cats-vs-dogs.avi"]', output, 1
+      assert_css 'video[poster="assets/cats-and-dogs.png"]', output, 1
+      assert_css 'video[width="200"]', output, 1
+      assert_css 'video[height="300"]', output, 1
+    end
+
+    test 'video macro should not use imagesdir attribute to resolve target if target is a URL' do
+      input = <<-EOS
+:imagesdir: assets
+
+video::http://example.org/videos/cats-vs-dogs.avi[]
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'video', output, 1
+      assert_css 'video[src="http://example.org/videos/cats-vs-dogs.avi"]', output, 1
+    end
+
+    test 'should detect and render audio macro' do
+      input = <<-EOS
+audio::podcast.mp3[]
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'audio', output, 1
+      assert_css 'audio[src="podcast.mp3"]', output, 1
+    end
+
+    test 'audio macro should use imagesdir attribute to resolve target' do
+      input = <<-EOS
+:imagesdir: assets
+
+audio::podcast.mp3[]
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'audio', output, 1
+      assert_css 'audio[src="assets/podcast.mp3"]', output, 1
+    end
+
+    test 'audio macro should not use imagesdir attribute to resolve target if target is a URL' do
+      input = <<-EOS
+:imagesdir: assets
+
+video::http://example.org/podcast.mp3[]
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'video', output, 1
+      assert_css 'video[src="http://example.org/podcast.mp3"]', output, 1
+    end
+
+    test 'audio macro should honor all options' do
+      input = <<-EOS
+audio::podcast.mp3[options="autoplay,nocontrols,loop"]
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'audio', output, 1
+      assert_css 'audio[autoplay]', output, 1
+      assert_css 'audio:not([controls])', output, 1
+      assert_css 'audio[loop]', output, 1
+    end
   end
 
   context 'Admonition icons' do
@@ -614,7 +1275,7 @@ You can use icons for admonitions by setting the 'icons' attribute.
       EOS
 
       output = render_string input, :safe => Asciidoctor::SafeMode::SERVER
-      assert_xpath '//*[@class="admonitionblock"]//*[@class="icon"]/img[@src="images/icons/tip.png"][@alt="Tip"]', output, 1
+      assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/img[@src="./images/icons/tip.png"][@alt="Tip"]', output, 1
     end
 
     test 'can resolve icon relative to custom iconsdir' do
@@ -627,7 +1288,7 @@ You can use icons for admonitions by setting the 'icons' attribute.
       EOS
 
       output = render_string input, :safe => Asciidoctor::SafeMode::SERVER
-      assert_xpath '//*[@class="admonitionblock"]//*[@class="icon"]/img[@src="icons/tip.png"][@alt="Tip"]', output, 1
+      assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/img[@src="icons/tip.png"][@alt="Tip"]', output, 1
     end
 
     test 'embeds base64-encoded data uri of icon when data-uri attribute is set and safe mode level is less than SECURE' do
@@ -642,7 +1303,7 @@ You can use icons for admonitions by setting the 'icons' attribute.
       EOS
 
       output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
-      assert_xpath '//*[@class="admonitionblock"]//*[@class="icon"]/img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Tip"]', output, 1
+      assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Tip"]', output, 1
     end
 
     test 'does not embed base64-encoded data uri of icon when safe mode level is SECURE or greater' do
@@ -657,7 +1318,7 @@ You can use icons for admonitions by setting the 'icons' attribute.
       EOS
 
       output = render_string input, :attributes => {'icons' => ''}
-      assert_xpath '//*[@class="admonitionblock"]//*[@class="icon"]/img[@src="fixtures/tip.gif"][@alt="Tip"]', output, 1
+      assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/img[@src="fixtures/tip.gif"][@alt="Tip"]', output, 1
     end
 
     test 'cleans reference to ancestor directories before reading icon if safe mode level is at least SAFE' do
@@ -672,7 +1333,19 @@ You can use icons for admonitions by setting the 'icons' attribute.
       EOS
 
       output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
-      assert_xpath '//*[@class="admonitionblock"]//*[@class="icon"]/img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Tip"]', output, 1
+      assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Tip"]', output, 1
+    end
+
+    test 'can use font-based icons' do
+      input = <<-EOS
+:icons: font
+
+[TIP]
+You can use icons for admonitions by setting the 'icons' attribute.
+      EOS
+
+      output = render_string input, :safe => Asciidoctor::SafeMode::SERVER
+      assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/i[@class="icon-tip"]', output, 1
     end
   end
 
@@ -765,7 +1438,7 @@ html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table)
 ----
       EOS
       output = render_string input, :safe => Asciidoctor::SafeMode::SAFE
-      assert_xpath '//pre[@class="highlight CodeRay"]/code[@class="ruby"]//span[@class = "constant"][text() = "CodeRay"]', output, 1
+      assert_xpath '//pre[@class="CodeRay"]/code[@class="ruby language-ruby"]//span[@class = "constant"][text() = "CodeRay"]', output, 1
       assert_match(/\.CodeRay \{/, output)
     end
 
@@ -782,7 +1455,7 @@ html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table)
 ----
       EOS
       output = render_string input, :safe => Asciidoctor::SafeMode::SAFE
-      assert_xpath '//pre[@class="highlight CodeRay"]/code[@class="ruby"]//span[@style = "color:#036;font-weight:bold"][text() = "CodeRay"]', output, 1
+      assert_xpath '//pre[@class="CodeRay"]/code[@class="ruby language-ruby"]//span[@style = "color:#036;font-weight:bold"][text() = "CodeRay"]', output, 1
       assert_no_match(/\.CodeRay \{/, output)
     end
 
@@ -812,4 +1485,267 @@ html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table)
     end
   end
 
+  context 'Abstract and Part Intro' do
+    test 'should make abstract on open block without title a quote block for article' do
+      input = <<-EOS
+= Article
+
+[abstract]
+--
+This article is about stuff.
+
+And other stuff.
+--
+      EOS
+
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock.abstract', output, 1
+      assert_css '#preamble .quoteblock', output, 1
+      assert_css '.quoteblock > blockquote', output, 1
+      assert_css '.quoteblock > blockquote > .paragraph', output, 2
+    end
+
+    test 'should make abstract on open block with title a quote block with title for article' do
+      input = <<-EOS
+= Article
+
+.My abstract
+[abstract]
+--
+This article is about stuff.
+--
+      EOS
+
+      output = render_string input
+      assert_css '.quoteblock', output, 1
+      assert_css '.quoteblock.abstract', output, 1
+      assert_css '#preamble .quoteblock', output, 1
+      assert_css '.quoteblock > .title', output, 1
+      assert_css '.quoteblock > .title + blockquote', output, 1
+      assert_css '.quoteblock > .title + blockquote > .paragraph', output, 1
+    end
+
+    test 'should allow abstract in document with title if doctype is book' do
+      input = <<-EOS
+= Book
+:doctype: book
+
+[abstract]
+Abstract for book with title is valid
+      EOS
+
+      output = render_string input
+      assert_css '.abstract', output, 1
+    end
+
+    test 'should not allow abstract as direct child of document if doctype is book' do
+      input = <<-EOS
+:doctype: book
+
+[abstract]
+Abstract for book without title is invalid.
+      EOS
+
+      output = render_string input
+      assert_css '.abstract', output, 0
+    end
+
+    test 'should make abstract on open block without title rendered to DocBook' do
+      input = <<-EOS
+= Article
+
+[abstract]
+--
+This article is about stuff.
+
+And other stuff.
+--
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_css 'abstract', output, 1
+      assert_css 'abstract > simpara', output, 2
+    end
+
+    test 'should make abstract on open block with title rendered to DocBook' do
+      input = <<-EOS
+= Article
+
+.My abstract
+[abstract]
+--
+This article is about stuff.
+--
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_css 'abstract', output, 1
+      assert_css 'abstract > title', output, 1
+      assert_css 'abstract > title + simpara', output, 1
+    end
+
+    test 'should allow abstract in document with title if doctype is book rendered to DocBook' do
+      input = <<-EOS
+= Book
+:doctype: book
+
+[abstract]
+Abstract for book with title is valid
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_css 'abstract', output, 1
+    end
+
+    test 'should not allow abstract as direct child of document if doctype is book rendered to DocBook' do
+      input = <<-EOS
+:doctype: book
+
+[abstract]
+Abstract for book is invalid.
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_css 'abstract', output, 0
+    end
+
+    # TODO partintro shouldn't be recognized if doctype is not book, should be in proper place
+    test 'should accept partintro on open block without title' do
+      input = <<-EOS
+= Book
+:doctype: book
+
+= Part 1
+
+[partintro]
+--
+This is a part intro.
+
+It can have multiple paragraphs.
+--
+      EOS
+
+      output = render_string input
+      assert_css '.openblock', output, 1
+      assert_css '.openblock.partintro', output, 1
+      assert_css '.openblock .title', output, 0
+      assert_css '.openblock .content', output, 1
+      assert_xpath %(//h1[@id="_part_1"]/following-sibling::*[#{contains_class(:openblock)}]), output, 1
+      assert_xpath %(//*[#{contains_class(:openblock)}]/*[@class="content"]/*[@class="paragraph"]), output, 2
+    end
+
+    test 'should accept partintro on open block with title' do
+      input = <<-EOS
+= Book
+:doctype: book
+
+= Part 1
+
+.Intro title
+[partintro]
+--
+This is a part intro with a title.
+--
+      EOS
+
+      output = render_string input
+      assert_css '.openblock', output, 1
+      assert_css '.openblock.partintro', output, 1
+      assert_css '.openblock .title', output, 1
+      assert_css '.openblock .content', output, 1
+      assert_xpath %(//h1[@id="_part_1"]/following-sibling::*[#{contains_class(:openblock)}]), output, 1
+      assert_xpath %(//*[#{contains_class(:openblock)}]/*[@class="title"][text() = "Intro title"]), output, 1
+      assert_xpath %(//*[#{contains_class(:openblock)}]/*[@class="content"]/*[@class="paragraph"]), output, 1
+    end
+
+    test 'should exclude partintro if not a child of part' do
+      input = <<-EOS
+= Book
+:doctype: book
+
+[partintro]
+part intro paragraph
+      EOS
+
+      output = render_string input
+      assert_css '.partintro', output, 0
+    end
+
+    test 'should not allow partintro unless doctype is book' do
+      input = <<-EOS
+[partintro]
+part intro paragraph
+      EOS
+
+      output = render_string input
+      assert_css '.partintro', output, 0
+    end
+
+    test 'should accept partintro on open block without title rendered to DocBook' do
+      input = <<-EOS
+= Book
+:doctype: book
+
+= Part 1
+
+[partintro]
+--
+This is a part intro.
+
+It can have multiple paragraphs.
+--
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_css 'partintro', output, 1
+      assert_css 'part#_part_1 > partintro', output, 1
+      assert_css 'partintro > simpara', output, 2
+    end
+
+    test 'should accept partintro on open block with title rendered to DocBook' do
+      input = <<-EOS
+= Book
+:doctype: book
+
+= Part 1
+
+.Intro title
+[partintro]
+--
+This is a part intro with a title.
+--
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_css 'partintro', output, 1
+      assert_css 'part#_part_1 > partintro', output, 1
+      assert_css 'partintro > title', output, 1
+      assert_css 'partintro > title + simpara', output, 1
+    end
+
+    test 'should exclude partintro if not a child of part rendered to DocBook' do
+      input = <<-EOS
+= Book
+:doctype: book
+
+[partintro]
+part intro paragraph
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_css 'partintro', output, 0
+    end
+
+    test 'should not allow partintro unless doctype is book rendered to DocBook' do
+      input = <<-EOS
+[partintro]
+part intro paragraph
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_css 'partintro', output, 0
+    end
+  end
+
 end
diff --git a/test/document_test.rb b/test/document_test.rb
index e0c8dd5..b9e1a57 100644
--- a/test/document_test.rb
+++ b/test/document_test.rb
@@ -19,6 +19,40 @@ context 'Document' do
       assert_equal Asciidoctor::SafeMode::SECURE, doc.safe
     end
 
+    test 'safe mode level set using string' do
+      doc = Asciidoctor::Document.new [], :safe => 'server'
+      assert_equal Asciidoctor::SafeMode::SERVER, doc.safe
+
+      doc = Asciidoctor::Document.new [], :safe => 'foo'
+      assert_equal Asciidoctor::SafeMode::SECURE, doc.safe
+    end
+
+    test 'safe mode level set using symbol' do
+      doc = Asciidoctor::Document.new [], :safe => :server
+      assert_equal Asciidoctor::SafeMode::SERVER, doc.safe
+
+      doc = Asciidoctor::Document.new [], :safe => :foo
+      assert_equal Asciidoctor::SafeMode::SECURE, doc.safe
+    end
+
+    test 'safe mode level set using integer' do
+      doc = Asciidoctor::Document.new [], :safe => 10
+      assert_equal Asciidoctor::SafeMode::SERVER, doc.safe
+
+      doc = Asciidoctor::Document.new [], :safe => 100
+      assert_equal 100, doc.safe
+    end
+
+    test 'safe mode attributes are set on document' do
+      doc = Asciidoctor::Document.new
+      assert_equal Asciidoctor::SafeMode::SECURE, doc.attr('safe-mode-level')
+      assert_equal 'secure', doc.attr('safe-mode-name')
+      assert doc.attr?('safe-mode-secure')
+      assert !doc.attr?('safe-mode-unsafe')
+      assert !doc.attr?('safe-mode-safe')
+      assert !doc.attr?('safe-mode-server')
+    end
+
     test 'safe mode level can be set in the constructor' do
       doc = Asciidoctor::Document.new [], :safe => Asciidoctor::SafeMode::SAFE
       assert_equal Asciidoctor::SafeMode::SAFE, doc.safe
@@ -89,11 +123,71 @@ preamble
       assert !doc.attr?('docfile')
       assert_equal doc.base_dir, doc.attr('docdir')
     end
+
+    test 'should accept attributes as array' do
+	  # NOTE there's a tab character before idseparator
+      doc = Asciidoctor.load('text', :attributes => %w(toc numbered   source-highlighter=coderay idprefix	idseparator=-))
+      assert doc.attributes.is_a?(Hash)
+      assert doc.attr?('toc')
+      assert_equal '', doc.attr('toc')
+      assert doc.attr?('numbered')
+      assert_equal '', doc.attr('numbered')
+      assert doc.attr?('source-highlighter')
+      assert_equal 'coderay', doc.attr('source-highlighter')
+      assert doc.attr?('idprefix')
+      assert_equal '', doc.attr('idprefix')
+      assert doc.attr?('idseparator')
+      assert_equal '-', doc.attr('idseparator')
+    end
+
+    test 'should accept attributes as empty array' do
+      doc = Asciidoctor.load('text', :attributes => [])
+      assert doc.attributes.is_a?(Hash)
+    end
+
+    test 'should accept attributes as string' do
+	  # NOTE there's a tab character before idseparator
+      doc = Asciidoctor.load('text', :attributes => 'toc numbered  source-highlighter=coderay idprefix	idseparator=-')
+      assert doc.attributes.is_a?(Hash)
+      assert doc.attr?('toc')
+      assert_equal '', doc.attr('toc')
+      assert doc.attr?('numbered')
+      assert_equal '', doc.attr('numbered')
+      assert doc.attr?('source-highlighter')
+      assert_equal 'coderay', doc.attr('source-highlighter')
+      assert doc.attr?('idprefix')
+      assert_equal '', doc.attr('idprefix')
+      assert doc.attr?('idseparator')
+      assert_equal '-', doc.attr('idseparator')
+    end
+
+    test 'should accept values containing spaces in attributes string' do
+	  # NOTE there's a tab character before self:
+      doc = Asciidoctor.load('text', :attributes => 'idprefix idseparator=-   note-caption=Note\ to\	self: toc')
+      assert doc.attributes.is_a?(Hash)
+      assert doc.attr?('idprefix')
+      assert_equal '', doc.attr('idprefix')
+      assert doc.attr?('idseparator')
+      assert_equal '-', doc.attr('idseparator')
+      assert doc.attr?('note-caption')
+      assert_equal "Note to	self:", doc.attr('note-caption')
+    end
+
+    test 'should accept attributes as empty string' do
+      doc = Asciidoctor.load('text', :attributes => '')
+      assert doc.attributes.is_a?(Hash)
+    end
+
+    test 'should accept attributes as nil' do
+      doc = Asciidoctor.load('text', :attributes => nil)
+      assert doc.attributes.is_a?(Hash)
+    end
   end
 
   context 'Render APIs' do
     test 'should render document to string' do
       sample_input_path = fixture_path('sample.asciidoc')
+
       output = Asciidoctor.render_file(sample_input_path, :header_footer => true)
       assert !output.empty?
       assert_xpath '/html', output, 1
@@ -103,6 +197,177 @@ preamble
       assert_xpath '/html/body/*[@id="header"]/h1[text() = "Document Title"]', output, 1
     end
 
+    test 'should accept attributes as array' do
+      sample_input_path = fixture_path('sample.asciidoc')
+      output = Asciidoctor.render_file(sample_input_path, :attributes => %w(numbered idprefix idseparator=-))
+      assert_css '#section-a', output, 1
+    end
+
+    test 'should accept attributes as string' do
+      sample_input_path = fixture_path('sample.asciidoc')
+      output = Asciidoctor.render_file(sample_input_path, :attributes => 'numbered idprefix idseparator=-')
+      assert_css '#section-a', output, 1
+    end
+
+    test 'should include docinfo files for html backend' do
+      sample_input_path = fixture_path('basic.asciidoc')
+
+      output = Asciidoctor.render_file(sample_input_path,
+          :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo' => ''})
+      assert !output.empty?
+      assert_css 'script[src="modernizr.js"]', output, 1
+      assert_css 'meta[http-equiv="imagetoolbar"]', output, 0
+
+      output = Asciidoctor.render_file(sample_input_path,
+          :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo1' => ''})
+      assert !output.empty?
+      assert_css 'script[src="modernizr.js"]', output, 0
+      assert_css 'meta[http-equiv="imagetoolbar"]', output, 1
+
+      output = Asciidoctor.render_file(sample_input_path,
+          :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo2' => ''})
+      assert !output.empty?
+      assert_css 'script[src="modernizr.js"]', output, 1
+      assert_css 'meta[http-equiv="imagetoolbar"]', output, 1
+    end
+
+    test 'should include docinfo files for docbook backend' do
+      sample_input_path = fixture_path('basic.asciidoc')
+
+      output = Asciidoctor.render_file(sample_input_path,
+          :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo' => ''})
+      assert !output.empty?
+      assert_css 'productname', output, 0
+      assert_css 'copyright', output, 1
+
+      output = Asciidoctor.render_file(sample_input_path,
+          :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo1' => ''})
+      assert !output.empty?
+      assert_css 'productname', output, 1
+      assert_css 'copyright', output, 0
+
+      output = Asciidoctor.render_file(sample_input_path,
+          :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo2' => ''})
+      assert !output.empty?
+      assert_css 'productname', output, 1
+      assert_css 'copyright', output, 1
+    end
+
+    test 'should not include docinfo files by default' do
+      sample_input_path = fixture_path('basic.asciidoc')
+
+      output = Asciidoctor.render_file(sample_input_path,
+          :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER)
+      assert !output.empty?
+      assert_css 'script[src="modernizr.js"]', output, 0
+      assert_css 'meta[http-equiv="imagetoolbar"]', output, 0
+
+      output = Asciidoctor.render_file(sample_input_path,
+          :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER)
+      assert !output.empty?
+      assert_css 'productname', output, 0
+      assert_css 'copyright', output, 0
+    end
+
+    test 'should not include docinfo files if safe mode is SECURE or greater' do
+      sample_input_path = fixture_path('basic.asciidoc')
+
+      output = Asciidoctor.render_file(sample_input_path,
+          :header_footer => true, :attributes => {'docinfo2' => ''})
+      assert !output.empty?
+      assert_css 'script[src="modernizr.js"]', output, 0
+      assert_css 'meta[http-equiv="imagetoolbar"]', output, 0
+
+      output = Asciidoctor.render_file(sample_input_path,
+          :header_footer => true, :backend => 'docbook', :attributes => {'docinfo2' => ''})
+      assert !output.empty?
+      assert_css 'productname', output, 0
+      assert_css 'copyright', output, 0
+    end
+
+    test 'should link to default stylesheet by default' do
+      sample_input_path = fixture_path('basic.asciidoc')
+      output = Asciidoctor.render_file(sample_input_path, :header_footer => true)
+      assert_css 'html:root > head > link[rel="stylesheet"][href="./asciidoctor.css"]', output, 1
+    end
+
+    test 'should link to default stylesheet by default if linkcss is unset in document' do
+      input = <<-EOS
+= Document Title
+:linkcss!:
+
+text
+      EOS
+
+      output = Asciidoctor.render(input, :header_footer => true)
+      assert_css 'html:root > head > link[rel="stylesheet"][href="./asciidoctor.css"]', output, 1
+    end
+
+    test 'should link to default stylesheet by default if linkcss is unset' do
+      input = <<-EOS
+= Document Title
+
+text
+      EOS
+
+      output = Asciidoctor.render(input, :header_footer => true, :attributes => {'linkcss!' => ''})
+      assert_css 'html:root > head > link[rel="stylesheet"][href="./asciidoctor.css"]', output, 1
+    end
+
+    test 'should embed default stylesheet if safe mode is less than secure and linkcss is unset' do
+      sample_input_path = fixture_path('basic.asciidoc')
+      output = Asciidoctor.render_file(sample_input_path, :header_footer => true,
+          :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'linkcss!' => ''})
+      assert_css 'html:root > head > style', output, 1
+      stylenode = xmlnodes_at_css 'html:root > head > style', output, 1
+      styles = stylenode.first.content
+      assert !styles.nil?
+      assert !styles.strip.empty?
+    end
+
+    test 'should not link to stylesheet if stylesheet is unset' do
+      input = <<-EOS
+= Document Title
+
+text
+      EOS
+
+      output = Asciidoctor.render(input, :header_footer => true, :attributes => {'stylesheet!' => ''})
+      assert_css 'html:root > head > link[rel="stylesheet"]', output, 0
+    end
+
+    test 'should link to custom stylesheet if specified in stylesheet attribute' do
+      input = <<-EOS
+= Document Title
+
+text
+      EOS
+
+      output = Asciidoctor.render(input, :header_footer => true, :attributes => {'stylesheet' => './custom.css'})
+      assert_css 'html:root > head > link[rel="stylesheet"][href="./custom.css"]', output, 1
+    end
+
+    test 'should resolve custom stylesheet relative to stylesdir' do
+      input = <<-EOS
+= Document Title
+
+text
+      EOS
+
+      output = Asciidoctor.render(input, :header_footer => true, :attributes => {'stylesheet' => 'custom.css', 'stylesdir' => './stylesheets'})
+      assert_css 'html:root > head > link[rel="stylesheet"][href="./stylesheets/custom.css"]', output, 1
+    end
+
+    test 'should resolve custom stylesheet to embed relative to stylesdir' do
+      sample_input_path = fixture_path('basic.asciidoc')
+      output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :safe => Asciidoctor::SafeMode::SAFE,
+          :attributes => {'stylesheet' => 'custom.css', 'stylesdir' => './stylesheets', 'linkcss!' => ''})
+      stylenode = xmlnodes_at_css 'html:root > head > style', output, 1
+      styles = stylenode.first.content
+      assert !styles.nil?
+      assert !styles.strip.empty?
+    end
+
     test 'should render document in place' do
       sample_input_path = fixture_path('sample.asciidoc')
       sample_output_path = fixture_path('sample.html')
@@ -139,6 +404,27 @@ preamble
       end
     end
 
+    test 'should render document to file when base dir is set' do
+      sample_input_path = fixture_path('sample.asciidoc')
+      sample_output_path = fixture_path('result.html')
+      fixture_dir = fixture_path('')
+      begin
+        Asciidoctor.render_file(sample_input_path, :to_file => 'result.html', :base_dir => fixture_dir)
+        assert File.exist?(sample_output_path)
+        output = File.read(sample_output_path)
+        assert !output.empty?
+        assert_xpath '/html', output, 1
+        assert_xpath '/html/head', output, 1
+        assert_xpath '/html/body', output, 1
+        assert_xpath '/html/head/title[text() = "Document Title"]', output, 1
+        assert_xpath '/html/body/*[@id="header"]/h1[text() = "Document Title"]', output, 1
+      rescue => e
+        flunk e.message
+      ensure
+        FileUtils::rm(sample_output_path, :force => true)
+      end
+    end
+
     test 'in_place option must not be used with to_file option' do
       sample_input_path = fixture_path('sample.asciidoc')
       sample_output_path = fixture_path('result.html')
@@ -177,7 +463,7 @@ preamble
       end
     end
 
-    test 'missing directories should be created if specified' do
+    test 'missing directories should be created if mkdirs is enabled' do
       sample_input_path = fixture_path('sample.asciidoc')
       output_dir = File.join(File.join(File.dirname(sample_input_path), 'test_output'), 'subdir')
       sample_output_path = File.join(output_dir, 'sample.html')
@@ -219,7 +505,7 @@ preamble
       assert !renderer.nil?
       views = renderer.views
       assert !views.nil?
-      assert_equal 30, views.size
+      assert_equal 36, views.size
       assert views.has_key? 'document'
       assert views['document'].is_a?(Asciidoctor::HTML5::DocumentTemplate)
       assert_equal 'ERB', views['document'].eruby.to_s
@@ -235,7 +521,7 @@ preamble
       assert !renderer.nil?
       views = renderer.views
       assert !views.nil?
-      assert_equal 30, views.size
+      assert_equal 36, views.size
       assert views.has_key? 'document'
       assert views['document'].is_a?(Asciidoctor::DocBook45::DocumentTemplate)
       assert_equal 'ERB', views['document'].eruby.to_s
@@ -280,27 +566,67 @@ preamble
 
     test 'document with title attribute entry overrides doctitle' do
      input = <<-EOS
-= Title
-:title: Document Title
+= Document Title
+:title: Override
 
-preamble
+{doctitle}
 
 == First Section
      EOS
      doc = document_from_string input
-     assert_equal 'Document Title', doc.doctitle
-     assert_equal 'Document Title', doc.title
+     assert_equal 'Override', doc.doctitle
+     assert_equal 'Override', doc.title
      assert doc.has_header?
-     assert_equal 'Title', doc.header.title
-     assert_equal 'Title', doc.first_section.title
+     assert_equal 'Document Title', doc.header.title
+     assert_equal 'Document Title', doc.first_section.title
+     assert_xpath '//*[@id="preamble"]//p[text()="Document Title"]', doc.render, 1
+    end
+
+    test 'document with title attribute entry overrides doctitle attribute entry' do
+     input = <<-EOS
+= Document Title
+:snapshot: {doctitle}
+:doctitle: doctitle
+:title: Override
+
+{snapshot}, {doctitle}
+
+== First Section
+     EOS
+     doc = document_from_string input
+     assert_equal 'Override', doc.doctitle
+     assert_equal 'Override', doc.title
+     assert doc.has_header?
+     assert_equal 'doctitle', doc.header.title
+     assert_equal 'doctitle', doc.first_section.title
+     assert_xpath '//*[@id="preamble"]//p[text()="Document Title, doctitle"]', doc.render, 1
     end
 
     test 'document with doctitle attribute entry overrides header title and doctitle' do
      input = <<-EOS
-= Title
+= Document Title
+:snapshot: {doctitle}
 :doctitle: Override
 
-preamble
+{snapshot}, {doctitle}
+
+== First Section
+     EOS
+     doc = document_from_string input
+     assert_equal 'Override', doc.doctitle
+     assert_nil doc.title
+     assert doc.has_header?
+     assert_equal 'Override', doc.header.title
+     assert_equal 'Override', doc.first_section.title
+     assert_xpath '//*[@id="preamble"]//p[text()="Document Title, Override"]', doc.render, 1
+    end
+
+    test 'doctitle attribute entry above header overrides header title and doctitle' do
+     input = <<-EOS
+:doctitle: Override
+= Document Title
+
+{doctitle}
 
 == First Section
      EOS
@@ -310,6 +636,7 @@ preamble
      assert doc.has_header?
      assert_equal 'Override', doc.header.title
      assert_equal 'Override', doc.first_section.title
+     assert_xpath '//*[@id="preamble"]//p[text()="Override"]', doc.render, 1
     end
 
     test 'should recognize document title when preceded by blank lines' do
@@ -357,14 +684,94 @@ more info...
       EOS
       output = render_string input
       assert_xpath '//*[@id="header"]/span[@id="author"][text() = "Stuart Rackham"]', output, 1
-      assert_xpath '//*[@id="header"]/span[@id="email"][contains(text(), "founder at asciidoc.org")]', output, 1
+      assert_xpath '//*[@id="header"]/span[@id="email"]/a[@href="mailto:founder at asciidoc.org"][text() = "founder at asciidoc.org"]', output, 1
       assert_xpath '//*[@id="header"]/span[@id="revnumber"][text() = "version 8.6.8,"]', output, 1
       assert_xpath '//*[@id="header"]/span[@id="revdate"][text() = "2012-07-12"]', output, 1
       assert_xpath '//*[@id="header"]/span[@id="revremark"][text() = "See changelog."]', output, 1
     end
 
+    test 'with metadata to DocBook' do
+      input = <<-EOS
+= AsciiDoc
+Stuart Rackham <founder at asciidoc.org>
+v8.6.8, 2012-07-12: See changelog.
+
+== Version 8.6.8
+
+more info...
+      EOS
+      output = render_string input, :backend => 'docbook'
+      assert_xpath '/article/articleinfo', output, 1
+      assert_xpath '/article/articleinfo/title[text() = "AsciiDoc"]', output, 1
+      assert_xpath '/article/articleinfo/date[text() = "2012-07-12"]', output, 1
+      assert_xpath '/article/articleinfo/author/firstname[text() = "Stuart"]', output, 1
+      assert_xpath '/article/articleinfo/author/surname[text() = "Rackham"]', output, 1
+      assert_xpath '/article/articleinfo/author/email[text() = "founder at asciidoc.org"]', output, 1
+      assert_xpath '/article/articleinfo/revhistory', output, 1
+      assert_xpath '/article/articleinfo/revhistory/revision', output, 1
+      assert_xpath '/article/articleinfo/revhistory/revision/revnumber[text() = "8.6.8"]', output, 1
+      assert_xpath '/article/articleinfo/revhistory/revision/date[text() = "2012-07-12"]', output, 1
+      assert_xpath '/article/articleinfo/revhistory/revision/authorinitials[text() = "SR"]', output, 1
+      assert_xpath '/article/articleinfo/revhistory/revision/revremark[text() = "See changelog."]', output, 1
+    end
+
+    test 'with author defined using attribute entry to DocBook' do
+      input = <<-EOS
+= Document Title
+:author: Doc Writer
+:email: thedoctor at asciidoc.org
+
+content
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_xpath '//articleinfo/author', output, 1
+      assert_xpath '//articleinfo/author/firstname[text() = "Doc"]', output, 1
+      assert_xpath '//articleinfo/author/surname[text() = "Writer"]', output, 1
+      assert_xpath '//articleinfo/author/email[text() = "thedoctor at asciidoc.org"]', output, 1
+      assert_xpath '//articleinfo/authorinitials[text() = "DW"]', output, 1
+    end
+
+    test 'should create authorgroup in DocBook when multiple authors' do
+      input = <<-EOS
+= Document Title
+Doc Writer <thedoctor at asciidoc.org>; Junior Writer <junior at asciidoctor.org>
+
+content
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_xpath '//articleinfo/author', output, 0
+      assert_xpath '//articleinfo/authorgroup', output, 1
+      assert_xpath '//articleinfo/authorgroup/author', output, 2
+      assert_xpath '//articleinfo/authorgroup/author[1]/firstname[text() = "Doc"]', output, 1
+      assert_xpath '//articleinfo/authorgroup/author[2]/firstname[text() = "Junior"]', output, 1
+    end
+
+    test 'with authors defined using attribute entry to DocBook' do
+      input = <<-EOS
+= Document Title
+:authors: Doc Writer; Junior Writer
+:email_1: thedoctor at asciidoc.org
+:email_2: junior at asciidoc.org
+
+content
+      EOS
+
+      output = render_string input, :backend => 'docbook'
+      assert_xpath '//articleinfo/author', output, 0
+      assert_xpath '//articleinfo/authorgroup', output, 1
+      assert_xpath '//articleinfo/authorgroup/author', output, 2
+      assert_xpath '(//articleinfo/authorgroup/author)[1]/firstname[text() = "Doc"]', output, 1
+      assert_xpath '(//articleinfo/authorgroup/author)[1]/email[text() = "thedoctor at asciidoc.org"]', output, 1
+      assert_xpath '(//articleinfo/authorgroup/author)[2]/firstname[text() = "Junior"]', output, 1
+      assert_xpath '(//articleinfo/authorgroup/author)[2]/email[text() = "junior at asciidoc.org"]', output, 1
+    end
+
     test 'with header footer' do
-      result = render_string("= Title\n\npreamble")
+      doc = document_from_string "= Title\n\npreamble"
+      assert !doc.attr?('embedded')
+      result = doc.render
       assert_xpath '/html', result, 1
       assert_xpath '//*[@id="header"]', result, 1
       assert_xpath '//*[@id="header"]/h1', result, 1
@@ -373,7 +780,9 @@ more info...
     end
 
     test 'no header footer' do
-      result = render_string("= Title\n\npreamble", :header_footer => false)
+      doc = document_from_string "= Title\n\npreamble", :header_footer => false
+      assert doc.attr?('embedded')
+      result = doc.render
       assert_xpath '/html', result, 0
       assert_xpath '/h1', result, 0
       assert_xpath '/*[@id="header"]', result, 0
@@ -381,7 +790,7 @@ more info...
       assert_xpath '/*[@id="preamble"]', result, 1
     end
 
-    test 'wip enable title when no header footer' do
+    test 'enable title when no header footer' do
       result = render_string("= Title\n\npreamble", :header_footer => false, :attributes => {'notitle!' => ''})
       assert_xpath '/html', result, 0
       assert_xpath '/h1', result, 1
@@ -428,6 +837,24 @@ finally a reference to the second footnote footnoteref:[note2].
       text = xmlnodes_at_xpath '//div[@id="footnotes"]/div[@id="_footnote_2"]/text()', output, 1
       assert_equal '. Second footnote.', text.text.strip
     end
+
+    test 'renders footnotes block in embedded document by default' do
+      input = <<-EOS
+Text that has supporting information{empty}footnote:[An example footnote.].
+      EOS
+
+      output = render_string input, :header_footer => false
+      assert_css '#footnotes', output, 1
+    end
+
+    test 'does not render footnotes block in embedded document if nofootnotes attribute is set' do
+      input = <<-EOS
+Text that has supporting information{empty}footnote:[An example footnote.].
+      EOS
+
+      output = render_string input, :header_footer => false, :attributes => {'nofootnotes' => ''}
+      assert_css '#footnotes', output, 0
+    end
   end
 
   context 'Backends and Doctypes' do 
@@ -499,6 +926,19 @@ chapter body
       assert_xpath '/book/simpara[text() = "text"]', result, 1
     end
 
+    test 'docbook45 backend parses out subtitle' do
+      input = <<-EOS
+= Document Title: Subtitle
+:doctype: book
+
+text
+      EOS
+      result = render_string input, :backend => 'docbook45'
+      assert_xpath '/book', result, 1
+      assert_xpath '/book/bookinfo/title[text() = "Document Title"]', result, 1
+      assert_xpath '/book/bookinfo/subtitle[text() = "Subtitle"]', result, 1
+    end
+
     test 'should be able to set backend using :backend option key' do
       doc = Asciidoctor::Document.new([], :backend => 'html5')
       assert_equal 'html5', doc.attributes['backend']
diff --git a/test/fixtures/basic-docinfo.html b/test/fixtures/basic-docinfo.html
new file mode 100644
index 0000000..2b1cbbb
--- /dev/null
+++ b/test/fixtures/basic-docinfo.html
@@ -0,0 +1 @@
+<script src="modernizr.js"></script>
diff --git a/test/fixtures/basic-docinfo.xml b/test/fixtures/basic-docinfo.xml
new file mode 100644
index 0000000..0a78b32
--- /dev/null
+++ b/test/fixtures/basic-docinfo.xml
@@ -0,0 +1,4 @@
+<copyright>
+  <year>2013</year>
+  <holder>Acme, Inc.</holder>
+</copyright>
diff --git a/test/fixtures/basic.asciidoc b/test/fixtures/basic.asciidoc
new file mode 100644
index 0000000..fc56ac7
--- /dev/null
+++ b/test/fixtures/basic.asciidoc
@@ -0,0 +1,4 @@
+= Document Title
+Doc Writer <doc.writer at asciidoc.org>
+
+Body content.
diff --git a/test/fixtures/docinfo.html b/test/fixtures/docinfo.html
new file mode 100644
index 0000000..e35e34b
--- /dev/null
+++ b/test/fixtures/docinfo.html
@@ -0,0 +1 @@
+<meta http-equiv="imagetoolbar" content="false">
diff --git a/test/fixtures/docinfo.xml b/test/fixtures/docinfo.xml
new file mode 100644
index 0000000..37b3115
--- /dev/null
+++ b/test/fixtures/docinfo.xml
@@ -0,0 +1,2 @@
+<productname>Asciidoctor</productname>
+<productnumber>1.0.0</productnumber>
diff --git a/test/fixtures/encoding.asciidoc b/test/fixtures/encoding.asciidoc
index cbc1005..48375b6 100644
--- a/test/fixtures/encoding.asciidoc
+++ b/test/fixtures/encoding.asciidoc
@@ -3,3 +3,11 @@ Gregory Romé has written an AsciiDoc plugin for the Redmine project management
 https://github.com/foo-users/foo
 へと `vicmd` キーマップを足してみている試み、
 アニメーションgifです。
+
+tag::romé[]
+Gregory Romé has written an AsciiDoc plugin for the Redmine project management application.
+end::romé[]
+
+== Überschrift
+
+* Codierungen sind verrückt auf älteren Versionen von Ruby
diff --git a/test/fixtures/include-file.asciidoc b/test/fixtures/include-file.asciidoc
index 21ed5fb..4f1fc0e 100644
--- a/test/fixtures/include-file.asciidoc
+++ b/test/fixtures/include-file.asciidoc
@@ -1 +1,22 @@
-included content
+first line of included content
+second line of included content
+third line of included content
+fourth line of included content
+fifth line of included content
+sixth line of included content
+seventh line of included content
+eighth line of included content
+
+// tag::snippetA[]
+snippetA content
+// end::snippetA[]
+
+non-tagged content
+
+// tag::snippetB[]
+snippetB content
+// end::snippetB[]
+
+more non-tagged content
+
+last line of included content
diff --git a/test/fixtures/stylesheets/custom.css b/test/fixtures/stylesheets/custom.css
new file mode 100644
index 0000000..60f1eab
--- /dev/null
+++ b/test/fixtures/stylesheets/custom.css
@@ -0,0 +1,3 @@
+body {
+  color: red;
+}
diff --git a/test/invoker_test.rb b/test/invoker_test.rb
index 73c94c1..b64e662 100644
--- a/test/invoker_test.rb
+++ b/test/invoker_test.rb
@@ -1,3 +1,4 @@
+# encoding: UTF-8
 require 'test_helper'
 require 'asciidoctor/cli/options'
 require 'asciidoctor/cli/invoker'
@@ -110,7 +111,7 @@ context 'Invoker' do
       assert_xpath '/html/head/title[text() = "Document Title"]', output, 1
       assert_xpath '/html/body/*[@id="header"]/h1[text() = "Document Title"]', output, 1
     ensure
-      FileUtils::rm(sample_outpath)
+      FileUtils::rm_f(sample_outpath)
     end
   end
 
@@ -119,12 +120,14 @@ context 'Invoker' do
     sample_outpath = File.join(destination_path, 'sample.html')
     begin
       FileUtils::mkdir_p(destination_path) 
+      # QUESTION should -D be relative to working directory or source directory?
       invoker = invoke_cli %w(-D test/test_output)
+      #invoker = invoke_cli %w(-D ../../test/test_output)
       doc = invoker.document
       assert_equal sample_outpath, doc.attr('outfile')
       assert File.exist?(sample_outpath)
     ensure
-      FileUtils::rm(sample_outpath)
+      FileUtils::rm_f(sample_outpath)
       FileUtils::rmdir(destination_path)
     end
   end
@@ -137,7 +140,21 @@ context 'Invoker' do
       assert_equal sample_outpath, doc.attr('outfile')
       assert File.exist?(sample_outpath)
     ensure
-      FileUtils::rm(sample_outpath)
+      FileUtils::rm_f(sample_outpath)
+    end
+  end
+
+  test 'should copy default css to target directory if copycss is specified' do
+    sample_outpath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'sample-output.html'))
+    default_stylesheet = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'asciidoctor.css'))
+    begin
+      invoker = invoke_cli %W(-o #{sample_outpath} -a copycss)
+      invoker.document
+      assert File.exist?(sample_outpath)
+      assert File.exist?(default_stylesheet)
+    ensure
+      FileUtils::rm_f(sample_outpath)
+      FileUtils::rm_f(default_stylesheet)
     end
   end
 
@@ -149,17 +166,33 @@ context 'Invoker' do
   end
 
   test 'should not compact output by default' do
-    invoker = invoke_cli_to_buffer(%w(-s -o -), '-') { 'content' }
+    # NOTE we are relying on the fact that the template leaves blank lines
+    # this will always fail when using a template engine which strips blank lines by default
+    invoker = invoke_cli_to_buffer(%w(-o -), '-') { '* content' }
     output = invoker.read_output
     assert_match(/\n[[:blank:]]*\n/, output)
   end
 
   test 'should compact output if specified' do
-    invoker = invoke_cli_to_buffer(%w(-C -s -o -), '-') { 'content' }
+    # NOTE we are relying on the fact that the template leaves blank lines
+    # this will always succeed when using a template engine which strips blank lines by default
+    invoker = invoke_cli_to_buffer(%w(-C -s -o -), '-') { '* content' }
     output = invoker.read_output
     assert_no_match(/\n[[:blank:]]*\n/, output)
   end
 
+  test 'should output a trailing endline to stdout' do
+    invoker = nil
+    output = nil
+    redirect_streams do |stdout, stderr|
+      invoker = invoke_cli %w(-o -)
+      output = stdout.string
+    end
+    assert !invoker.nil?
+    assert !output.nil?
+    assert output.end_with?("\n")
+  end
+
   test 'should set backend to html5 if specified' do
     invoker = invoke_cli_to_buffer %w(-b html5 -o -)
     doc = invoker.document
@@ -202,6 +235,23 @@ context 'Invoker' do
     assert_xpath '//h2[@id="idsection_a"]', output, 1
   end
 
+  test 'should set attribute with value containing equal sign' do
+    invoker = invoke_cli_to_buffer %w(--trace -a toc -a toc-title=t=o=c -o -)
+    doc = invoker.document
+    assert_equal 't=o=c', doc.attr('toc-title')
+    output = invoker.read_output
+    assert_xpath '//*[@id="toctitle"][text() = "t=o=c"]', output, 1
+  end
+
+  test 'should set attribute with quoted value containing a space' do
+	# emulating commandline arguments: --trace -a toc -a note-caption="Note to self:" -o -
+    invoker = invoke_cli_to_buffer %w(--trace -a toc -a note-caption=Note\ to\ self: -o -)
+    doc = invoker.document
+    assert_equal 'Note to self:', doc.attr('note-caption')
+    output = invoker.read_output
+    assert_xpath %(//*[#{contains_class('admonitionblock')}]//*[@class='title'][text() = 'Note to self:']), output, 1
+  end
+
   test 'should not set attribute ending in @ if defined in document' do
     invoker = invoke_cli_to_buffer %w(--trace -a idprefix=id@ -s -o -)
     doc = invoker.document
@@ -215,7 +265,7 @@ context 'Invoker' do
     doc = invoker.document
     assert_equal '', doc.attr('icons')
     output = invoker.read_output
-    assert_xpath '//*[@class="admonitionblock"]//img[@alt="Note"]', output, 1
+    assert_xpath '//*[@class="admonitionblock note"]//img[@alt="Note"]', output, 1
   end
 
   test 'should unset attribute ending in bang' do
@@ -259,4 +309,33 @@ context 'Invoker' do
     assert_equal 'erubis', doc.instance_variable_get('@options')[:eruby]
   end
 
+  test 'should force default external encoding to UTF-8' do
+    executable = File.expand_path(File.join(File.dirname(__FILE__), '..', 'bin', 'asciidoctor'))
+    input_path = fixture_path 'encoding.asciidoc'
+    old_lang = ENV['LANG']
+    ENV['LANG'] = 'US-ASCII'
+    begin
+      # using open3 to work around a bug in JRuby process_manager.rb,
+      # which tries to run a gsub on stdout prematurely breaking the test
+      require 'open3'
+      #cmd = "#{executable} -o - --trace #{input_path}"
+      cmd = "#{File.join RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']} #{executable} -o - --trace #{input_path}"
+      _, stdout, stderr = Open3.popen3 cmd
+      stderr_lines = stderr.readlines
+      puts stderr_lines.join unless stderr_lines.empty?
+      assert stderr_lines.empty?, 'Command failed. Expected to receive a rendered document.'
+      stdout_lines = stdout.readlines
+      assert !stdout_lines.empty?
+      if Asciidoctor::FORCE_ENCODING
+        stdout_lines.each do |l|
+          l.force_encoding Encoding::UTF_8
+        end
+      end
+      stdout_str = stdout_lines.join
+      assert stdout_str.include?('Codierungen sind verrückt auf älteren Versionen von Ruby') 
+    ensure
+      ENV['LANG'] = old_lang
+    end
+  end
+
 end
diff --git a/test/lexer_test.rb b/test/lexer_test.rb
index 9d568fe..df78aff 100644
--- a/test/lexer_test.rb
+++ b/test/lexer_test.rb
@@ -71,6 +71,14 @@ context "Lexer" do
     assert_equal expected, attributes
   end
 
+  test "collect unnamed attribute in second position after empty attribute" do
+    attributes = {}
+    line = ', John Smith'
+    expected = {1 => nil, 2 => 'John Smith'}
+    Asciidoctor::AttributeList.new(line).parse_into(attributes)
+    assert_equal expected, attributes
+  end
+
   test "collect unnamed attributes" do
     attributes = {}
     line = "first, second one, third"
@@ -154,7 +162,15 @@ context "Lexer" do
   test "collect options attribute" do
     attributes = {}
     line = "quote, options='opt1,opt2 , opt3'"
-    expected = {1 => 'quote', 'options' => 'opt1,opt2 , opt3', 'opt1-option' => nil, 'opt2-option' => nil, 'opt3-option' => nil}
+    expected = {1 => 'quote', 'options' => 'opt1,opt2 , opt3', 'opt1-option' => '', 'opt2-option' => '', 'opt3-option' => ''}
+    Asciidoctor::AttributeList.new(line).parse_into(attributes)
+    assert_equal expected, attributes
+  end
+
+  test "collect opts attribute as options" do
+    attributes = {}
+    line = "quote, opts='opt1,opt2 , opt3'"
+    expected = {1 => 'quote', 'options' => 'opt1,opt2 , opt3', 'opt1-option' => '', 'opt2-option' => '', 'opt3-option' => ''}
     Asciidoctor::AttributeList.new(line).parse_into(attributes)
     assert_equal expected, attributes
   end
@@ -174,18 +190,94 @@ context "Lexer" do
     assert_equal expected, attributes
   end
 
+  test 'parse style attribute with id and role' do
+    attributes = {1 => 'style#id.role'}
+    style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes)
+    assert_equal 'style', style
+    assert_nil original_style
+    assert_equal 'style', attributes['style']
+    assert_equal 'id', attributes['id']
+    assert_equal 'role', attributes['role']
+    assert_equal 'style#id.role', attributes[1]
+  end
+
+  test 'parse style attribute with style, role and id' do
+    attributes = {1 => 'style.role#id'}
+    style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes)
+    assert_equal 'style', style
+    assert_nil original_style
+    assert_equal 'style', attributes['style']
+    assert_equal 'id', attributes['id']
+    assert_equal 'role', attributes['role']
+    assert_equal 'style.role#id', attributes[1]
+  end
+
+  test 'parse style attribute with style, id and multiple roles' do
+    attributes = {1 => 'style#id.role1.role2'}
+    style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes)
+    assert_equal 'style', style
+    assert_nil original_style
+    assert_equal 'style', attributes['style']
+    assert_equal 'id', attributes['id']
+    assert_equal 'role1 role2', attributes['role']
+    assert_equal 'style#id.role1.role2', attributes[1]
+  end
+
+  test 'parse style attribute with style, multiple roles and id' do
+    attributes = {1 => 'style.role1.role2#id'}
+    style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes)
+    assert_equal 'style', style
+    assert_nil original_style
+    assert_equal 'style', attributes['style']
+    assert_equal 'id', attributes['id']
+    assert_equal 'role1 role2', attributes['role']
+    assert_equal 'style.role1.role2#id', attributes[1]
+  end
+
+  test 'parse style attribute with positional and original style' do
+    attributes = {1 => 'new_style', 'style' => 'original_style'}
+    style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes)
+    assert_equal 'new_style', style
+    assert_equal 'original_style', original_style
+    assert_equal 'new_style', attributes['style']
+    assert_equal 'new_style', attributes[1]
+  end
+
+  test 'parse style attribute with id and role only' do
+    attributes = {1 => '#id.role'}
+    style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes)
+    assert_nil style
+    assert_nil original_style
+    assert_equal 'id', attributes['id']
+    assert_equal 'role', attributes['role']
+    assert_equal '#id.role', attributes[1]
+  end
+
+  test 'parse empty style attribute' do
+    attributes = {1 => nil}
+    style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes)
+    assert_nil style
+    assert_nil original_style
+    assert_nil attributes['id']
+    assert_nil attributes['role']
+    assert_nil attributes[1]
+  end
+
   test "parse author first" do
     metadata, = parse_header_metadata 'Stuart'
-    assert_equal 3, metadata.size
-    assert_equal 'Stuart', metadata['author']
+    assert_equal 5, metadata.size
+    assert_equal 1, metadata['authorcount']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'Stuart', metadata['firstname']
     assert_equal 'S', metadata['authorinitials']
   end
 
   test "parse author first last" do
     metadata, = parse_header_metadata 'Yukihiro Matsumoto'
-    assert_equal 4, metadata.size
+    assert_equal 6, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Yukihiro Matsumoto', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'Yukihiro', metadata['firstname']
     assert_equal 'Matsumoto', metadata['lastname']
     assert_equal 'YM', metadata['authorinitials']
@@ -193,8 +285,10 @@ context "Lexer" do
 
   test "parse author first middle last" do
     metadata, = parse_header_metadata 'David Heinemeier Hansson'
-    assert_equal 5, metadata.size
+    assert_equal 7, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'David Heinemeier Hansson', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'David', metadata['firstname']
     assert_equal 'Heinemeier', metadata['middlename']
     assert_equal 'Hansson', metadata['lastname']
@@ -203,8 +297,10 @@ context "Lexer" do
 
   test "parse author first middle last email" do
     metadata, = parse_header_metadata 'David Heinemeier Hansson <rails at ruby-lang.org>'
-    assert_equal 6, metadata.size
+    assert_equal 8, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'David Heinemeier Hansson', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'David', metadata['firstname']
     assert_equal 'Heinemeier', metadata['middlename']
     assert_equal 'Hansson', metadata['lastname']
@@ -214,8 +310,10 @@ context "Lexer" do
 
   test "parse author first email" do
     metadata, = parse_header_metadata 'Stuart <founder at asciidoc.org>'
-    assert_equal 4, metadata.size
+    assert_equal 6, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Stuart', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'Stuart', metadata['firstname']
     assert_equal 'founder at asciidoc.org', metadata['email']
     assert_equal 'S', metadata['authorinitials']
@@ -223,8 +321,10 @@ context "Lexer" do
 
   test "parse author first last email" do
     metadata, = parse_header_metadata 'Stuart Rackham <founder at asciidoc.org>'
-    assert_equal 5, metadata.size
+    assert_equal 7, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Stuart Rackham', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'Stuart', metadata['firstname']
     assert_equal 'Rackham', metadata['lastname']
     assert_equal 'founder at asciidoc.org', metadata['email']
@@ -233,8 +333,10 @@ context "Lexer" do
 
   test "parse author with hyphen" do
     metadata, = parse_header_metadata 'Tim Berners-Lee <founder at www.org>'
-    assert_equal 5, metadata.size
+    assert_equal 7, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Tim Berners-Lee', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'Tim', metadata['firstname']
     assert_equal 'Berners-Lee', metadata['lastname']
     assert_equal 'founder at www.org', metadata['email']
@@ -243,8 +345,10 @@ context "Lexer" do
 
   test "parse author with single quote" do
     metadata, = parse_header_metadata 'Stephen O\'Grady <founder at redmonk.com>'
-    assert_equal 5, metadata.size
+    assert_equal 7, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Stephen O\'Grady', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'Stephen', metadata['firstname']
     assert_equal 'O\'Grady', metadata['lastname']
     assert_equal 'founder at redmonk.com', metadata['email']
@@ -253,8 +357,10 @@ context "Lexer" do
 
   test "parse author with dotted initial" do
     metadata, = parse_header_metadata 'Heiko W. Rupp <hwr at example.de>'
-    assert_equal 6, metadata.size
+    assert_equal 8, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Heiko W. Rupp', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'Heiko', metadata['firstname']
     assert_equal 'W.', metadata['middlename']
     assert_equal 'Rupp', metadata['lastname']
@@ -264,8 +370,10 @@ context "Lexer" do
 
   test "parse author with underscore" do
     metadata, = parse_header_metadata 'Tim_E Fella'
-    assert_equal 4, metadata.size
+    assert_equal 6, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Tim E Fella', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'Tim E', metadata['firstname']
     assert_equal 'Fella', metadata['lastname']
     assert_equal 'TF', metadata['authorinitials']
@@ -273,8 +381,10 @@ context "Lexer" do
 
   test "parse author condenses whitespace" do
     metadata, = parse_header_metadata '   Stuart       Rackham     <founder at asciidoc.org>'
-    assert_equal 5, metadata.size
+    assert_equal 7, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Stuart Rackham', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'Stuart', metadata['firstname']
     assert_equal 'Rackham', metadata['lastname']
     assert_equal 'founder at asciidoc.org', metadata['email']
@@ -283,15 +393,26 @@ context "Lexer" do
 
   test "parse invalid author line becomes author" do
     metadata, = parse_header_metadata '   Stuart       Rackham, founder of AsciiDoc   <founder at asciidoc.org>'
-    assert_equal 3, metadata.size
+    assert_equal 5, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Stuart Rackham, founder of AsciiDoc <founder at asciidoc.org>', metadata['author']
+    assert_equal metadata['author'], metadata['authors']
     assert_equal 'Stuart Rackham, founder of AsciiDoc <founder at asciidoc.org>', metadata['firstname']
     assert_equal 'S', metadata['authorinitials']
   end
 
+  test 'parse multiple authors' do
+    metadata, = parse_header_metadata 'Doc Writer <doc.writer at asciidoc.org>; John Smith <john.smith at asciidoc.org>'
+    assert_equal 2, metadata['authorcount']
+    assert_equal 'Doc Writer, John Smith', metadata['authors']
+    assert_equal 'Doc Writer', metadata['author']
+    assert_equal 'Doc Writer', metadata['author_1']
+    assert_equal 'John Smith', metadata['author_2']
+  end
+
   test "parse rev number date remark" do
     metadata, = parse_header_metadata "Ryan Waldron\nv0.0.7, 2013-12-18: The first release you can stand on"
-    assert_equal 7, metadata.size
+    assert_equal 9, metadata.size
     assert_equal '0.0.7', metadata['revnumber']
     assert_equal '2013-12-18', metadata['revdate']
     assert_equal 'The first release you can stand on', metadata['revremark']
@@ -299,20 +420,20 @@ context "Lexer" do
 
   test "parse rev date" do
     metadata, = parse_header_metadata "Ryan Waldron\n2013-12-18"
-    assert_equal 5, metadata.size
+    assert_equal 7, metadata.size
     assert_equal '2013-12-18', metadata['revdate']
   end
 
   # while compliant w/ AsciiDoc, this is just sloppy parsing
   test "treats arbitrary text on rev line as revdate" do
     metadata, = parse_header_metadata "Ryan Waldron\nfoobar\n"
-    assert_equal 5, metadata.size
+    assert_equal 7, metadata.size
     assert_equal 'foobar', metadata['revdate']
   end
 
   test "parse rev date remark" do
     metadata, = parse_header_metadata "Ryan Waldron\n2013-12-18:  The first release you can stand on"
-    assert_equal 6, metadata.size
+    assert_equal 8, metadata.size
     assert_equal '2013-12-18', metadata['revdate']
     assert_equal 'The first release you can stand on', metadata['revremark']
   end
@@ -331,7 +452,8 @@ context "Lexer" do
 
   test "skip line comments before author" do
     metadata, = parse_header_metadata "// Asciidoctor\n// release artist\nRyan Waldron"
-    assert_equal 4, metadata.size
+    assert_equal 6, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Ryan Waldron', metadata['author']
     assert_equal 'Ryan', metadata['firstname']
     assert_equal 'Waldron', metadata['lastname']
@@ -340,7 +462,8 @@ context "Lexer" do
 
   test "skip block comment before author" do
     metadata, = parse_header_metadata "////\nAsciidoctor\nrelease artist\n////\nRyan Waldron"
-    assert_equal 4, metadata.size
+    assert_equal 6, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Ryan Waldron', metadata['author']
     assert_equal 'Ryan', metadata['firstname']
     assert_equal 'Waldron', metadata['lastname']
@@ -349,7 +472,8 @@ context "Lexer" do
 
   test "skip block comment before rev" do
     metadata, = parse_header_metadata "Ryan Waldron\n////\nAsciidoctor\nrelease info\n////\nv0.0.7, 2013-12-18"
-    assert_equal 6, metadata.size
+    assert_equal 8, metadata.size
+    assert_equal 1, metadata['authorcount']
     assert_equal 'Ryan Waldron', metadata['author']
     assert_equal '0.0.7', metadata['revnumber']
     assert_equal '2013-12-18', metadata['revdate']
@@ -363,4 +487,95 @@ context "Lexer" do
     assert_equal 'SJR', blankdoc.attributes['authorinitials']
   end
 
+  test 'reset block indent to 0' do
+    input = <<-EOS
+    def names
+
+      @name.split ' '
+
+    end
+    EOS
+
+    expected = <<-EOS
+def names
+
+  @name.split ' '
+
+end
+    EOS
+
+    lines = input.lines.entries
+    Asciidoctor::Lexer.reset_block_indent! lines
+    assert_equal expected, lines.join
+  end
+
+  test 'reset block indent mixed with tabs and spaces to 0' do
+    input = <<-EOS
+    def names
+
+\t  @name.split ' '
+
+    end
+    EOS
+
+    expected = <<-EOS
+def names
+
+  @name.split ' '
+
+end
+    EOS
+
+    lines = input.lines.entries
+    Asciidoctor::Lexer.reset_block_indent! lines
+    assert_equal expected, lines.join
+  end
+
+  test 'reset block indent to non-zero' do
+    input = <<-EOS
+    def names
+
+      @name.split ' '
+
+    end
+    EOS
+
+    expected = <<-EOS
+  def names
+  
+    @name.split ' '
+  
+  end
+    EOS
+
+    lines = input.lines.entries
+    Asciidoctor::Lexer.reset_block_indent! lines, 2
+    assert_equal expected, lines.join
+  end
+
+  test 'preserve block indent' do
+    input = <<-EOS
+    def names
+    
+      @name.split ' '
+    
+    end
+    EOS
+
+    expected = input
+
+    lines = input.lines.entries
+    Asciidoctor::Lexer.reset_block_indent! lines, nil
+    assert_equal expected, lines.join
+  end
+
+  test 'reset block indent hands empty lines gracefully' do
+    input = []
+    expected = input
+
+    lines = input.dup
+    Asciidoctor::Lexer.reset_block_indent! lines
+    assert_equal expected, lines
+  end
+
 end
diff --git a/test/links_test.rb b/test/links_test.rb
index 1dbb246..7774144 100644
--- a/test/links_test.rb
+++ b/test/links_test.rb
@@ -30,6 +30,10 @@ context 'Links' do
     assert_xpath '//a[@href="http://asciidoc.org"][text()="http://asciidoc.org"]', render_string('(http://asciidoc.org) is the project page for AsciiDoc.'), 1
   end
 
+  test 'qualified url containing round brackets' do
+    assert_xpath '//a[@href="http://jruby.org/apidocs/org/jruby/Ruby.html#addModule(org.jruby.RubyModule)"][text()="addModule() adds a Ruby module"]', render_string('http://jruby.org/apidocs/org/jruby/Ruby.html#addModule(org.jruby.RubyModule)[addModule() adds a Ruby module]'), 1
+  end
+
   test 'qualified url adjacent to text in square brackets' do
     assert_xpath '//a[@href="http://asciidoc.org"][text()="AsciiDoc"]', render_string(']http://asciidoc.org[AsciiDoc] project page.'), 1
   end
@@ -70,6 +74,26 @@ context 'Links' do
     assert_xpath '//a[@href="http://github.com/asciidoctor"]', render_string('Asciidoctor GitHub organization: <**http://github.com/asciidoctor**>'), 1
   end
 
+  test 'link with quoted text should not be separated into attributes when linkattrs is set' do
+    assert_xpath '//a[@href="http://search.example.com"][text()="Google, Yahoo, Bing"]', render_embedded_string('http://search.example.com["Google, Yahoo, Bing"]', :attributes => {'linkattrs' => ''}), 1
+  end
+
+  test 'role and window attributes on link are processed when linkattrs is set' do
+    assert_xpath '//a[@href="http://google.com"][@class="external"][@target="_blank"]', render_embedded_string('http://google.com[Google, role="external", window="_blank"]', :attributes => {'linkattrs' => ''}), 1
+  end
+
+  test 'link text that ends in ^ should set link window to _blank' do
+    assert_xpath '//a[@href="http://google.com"][@target="_blank"]', render_embedded_string('http://google.com[Google^]'), 1
+  end
+
+  test 'inline irc link' do
+    assert_xpath '//a[@href="irc://irc.freenode.net"][text()="irc://irc.freenode.net"]', render_embedded_string('irc://irc.freenode.net'), 1
+  end
+
+  test 'inline irc link with text' do
+    assert_xpath '//a[@href="irc://irc.freenode.net"][text()="Freenode IRC"]', render_embedded_string('irc://irc.freenode.net[Freenode IRC]'), 1
+  end
+
   test 'inline ref' do
     doc = document_from_string 'Here you can read about tigers.[[tigers]]'
     output = doc.render
@@ -115,6 +139,14 @@ context 'Links' do
     assert_xpath %{//a[@href="#tigers"][text() = "about\ntigers"]}, render_string("Want to learn <<tigers,about\ntigers>>?"), 1
   end
 
+  test 'xref with escaped text' do
+    # when \x0 was used as boundary character for passthrough, it was getting stripped
+    # now using \e as boundary character, which resolves issue
+    input = 'See the <<tigers , `[tigers]`>> section for data about tigers'
+    output = render_embedded_string input
+    assert_xpath %(//a[@href="#tigers"]/code[text()="[tigers]"]), output, 1
+  end
+
   test 'xref using macro syntax' do
     doc = document_from_string 'xref:tigers[]'
     doc.references[:ids]['tigers'] = '[tigers]'
diff --git a/test/lists_test.rb b/test/lists_test.rb
index 1f690f0..06dc323 100644
--- a/test/lists_test.rb
+++ b/test/lists_test.rb
@@ -16,6 +16,28 @@ List
       assert_xpath '//ul/li', output, 3
     end
 
+    test 'indented dash elements using spaces' do
+      input = <<-EOS
+ - Foo
+ - Boo
+ - Blech
+      EOS
+      output = render_string input
+      assert_xpath '//ul', output, 1
+      assert_xpath '//ul/li', output, 3
+    end
+
+    test 'indented dash elements using tabs' do
+      input = <<-EOS
+\t-\tFoo
+\t-\tBoo
+\t-\tBlech
+      EOS
+      output = render_string input
+      assert_xpath '//ul', output, 1
+      assert_xpath '//ul/li', output, 3
+    end
+
     test "dash elements separated by blank lines should merge lists" do
       input = <<-EOS
 List
@@ -150,6 +172,40 @@ wrapped content
       assert_xpath "//ul/li[1]/p[text() = 'Foo\n:foo: bar']", output, 1
     end
 
+    test 'a list item with a nested marker terminates non-indented paragraph for text of list item' do
+      input = <<-EOS
+- Foo
+Bar
+* Foo
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'ul ul', output, 1
+      assert !output.include?('* Foo')
+    end
+
+    test 'a list item for a different list terminates non-indented paragraph for text of list item' do
+      input = <<-EOS
+== Example 1
+
+- Foo
+Bar
+. Foo
+
+== Example 2
+
+* Item
+text
+term:: def
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'ul ol', output, 1
+      assert !output.include?('* Foo')
+      assert_css 'ul dl', output, 1
+      assert !output.include?('term:: def')
+    end
+
     test 'an indented wrapped line is unindented and folded into text of list item' do
       input = <<-EOS
 List
@@ -186,6 +242,40 @@ second wrapped line
       assert_equal 'second wrapped line', lines[2].chomp
     end
 
+    test 'a list item with a nested marker terminates indented paragraph for text of list item' do
+      input = <<-EOS
+- Foo
+  Bar
+* Foo
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'ul ul', output, 1
+      assert !output.include?('* Foo')
+    end
+
+    test 'a list item for a different list terminates indented paragraph for text of list item' do
+      input = <<-EOS
+== Example 1
+
+- Foo
+  Bar
+. Foo
+
+== Example 2
+
+* Item
+  text
+term:: def
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'ul ol', output, 1
+      assert !output.include?('* Foo')
+      assert_css 'ul dl', output, 1
+      assert !output.include?('term:: def')
+    end
+
     test "a literal paragraph offset by blank lines in list content is appended as a literal block" do
       input = <<-EOS
 List
@@ -232,6 +322,23 @@ para
       assert_xpath '(//ul/li)[1]/*[@class="literalblock"]/following-sibling::*[@class="paragraph"]/p[text()="para"]', output, 1
     end
 
+    test 'an admonition paragraph attached by a line continuation to a list item with wrapped text should produce admonition' do
+      input = <<-EOS
+- first-line text
+  wrapped text
++
+NOTE: This is a note.
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'ul', output, 1
+      assert_css 'ul > li', output, 1
+      assert_css 'ul > li > p', output, 1
+      assert_xpath %(//ul/li/p[text()="first-line text\nwrapped text"]), output, 1
+      assert_css 'ul > li > p + .admonitionblock.note', output, 1
+      assert_xpath '//ul/li/*[@class="admonitionblock note"]//td[@class="content"][normalize-space(text())="This is a note."]', output, 1
+    end
+
     test 'appends line as paragraph if attached by continuation following line comment' do
       input = <<-EOS
 - list item 1
@@ -331,6 +438,28 @@ List
       assert_xpath '//ul/li', output, 3
     end
 
+    test 'indented asterisk elements using spaces' do
+      input = <<-EOS
+ * Foo
+ * Boo
+ * Blech
+      EOS
+      output = render_string input
+      assert_xpath '//ul', output, 1
+      assert_xpath '//ul/li', output, 3
+    end
+
+    test 'indented asterisk elements using tabs' do
+      input = <<-EOS
+\t*\tFoo
+\t*\tBoo
+\t*\tBlech
+      EOS
+      output = render_string input
+      assert_xpath '//ul', output, 1
+      assert_xpath '//ul/li', output, 3
+    end
+
     test "asterisk elements separated by blank lines should merge lists" do
       input = <<-EOS
 List
@@ -455,6 +584,21 @@ item
       assert_xpath '//ul/li', output, 2
       assert_xpath '//h2[@id = "sec"][text() = "Section"]', output, 1
     end
+
+    test 'should not find section title immediately below last list item' do
+      input = <<-EOS
+* first
+* second
+== Not a section
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'ul', output, 1
+      assert_css 'ul > li', output, 2
+      assert_css 'h2', output, 0
+      assert output.include?('== Not a section')
+      assert_xpath %((//li)[2]/p[text() = "second\n== Not a section"]), output, 1
+    end
   end
 
   context "Lists with inline markup" do
@@ -472,7 +616,7 @@ List
       assert_xpath '//ul/li', output, 3
       assert_xpath '(//ul/li)[1]//strong', output, 1
       assert_xpath '(//ul/li)[2]//em', output, 1
-      assert_xpath '(//ul/li)[3]//tt', output, 1
+      assert_xpath '(//ul/li)[3]//code', output, 1
     end
 
     test "attribute substitutions" do
@@ -893,7 +1037,7 @@ Lists
       assert_xpath '//ul/li[1]/p', output, 1
       assert_xpath '(//ul/li[1]/p/following-sibling::*)[1][@id="beck"][@class = "listingblock"]', output, 1
       assert_xpath '(//ul/li[1]/p/following-sibling::*)[1][@id="beck"]/div[@class="title"][starts-with(text(),"Read")]', output, 1
-      assert_xpath '(//ul/li[1]/p/following-sibling::*)[1][@id="beck"]//code[@class="ruby"][starts-with(text(),"5.times")]', output, 1
+      assert_xpath '(//ul/li[1]/p/following-sibling::*)[1][@id="beck"]//code[@class="ruby language-ruby"][starts-with(text(),"5.times")]', output, 1
     end
 
     test 'trailing block attribute line attached by continuation should not create block' do
@@ -1287,6 +1431,28 @@ List
       assert_xpath '//ol/li', output, 3
     end
 
+    test 'indented dot elements using spaces' do
+      input = <<-EOS
+ . Foo
+ . Boo
+ . Blech
+      EOS
+      output = render_string input
+      assert_xpath '//ol', output, 1
+      assert_xpath '//ol/li', output, 3
+    end
+
+    test 'indented dot elements using tabs' do
+      input = <<-EOS
+\t.\tFoo
+\t.\tBoo
+\t.\tBlech
+      EOS
+      output = render_string input
+      assert_xpath '//ol', output, 1
+      assert_xpath '//ol/li', output, 3
+    end
+
     test "dot elements separated by blank lines should merge lists" do
       input = <<-EOS
 List
@@ -1409,6 +1575,21 @@ term1:: def1
       assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1
     end
 
+    test "single-line indented adjacent elements with tabs" do
+      input = <<-EOS
+term1::\tdef1
+\tterm2::\tdef2
+      EOS
+      output = render_string input
+      assert_xpath '//dl', output, 1
+      assert_xpath '//dl/dt', output, 2
+      assert_xpath '//dl/dt/following-sibling::dd', output, 2
+      assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1
+      assert_xpath '(//dl/dt)[1]/following-sibling::dd/p[text() = "def1"]', output, 1
+      assert_xpath '(//dl/dt)[2][normalize-space(text()) = "term2"]', output, 1
+      assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1
+    end
+
     test "single-line elements separated by blank line should create a single list" do
       input = <<-EOS
 term1:: def1
@@ -1517,6 +1698,22 @@ def2
       assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1
     end
 
+    test 'consecutive terms share same varlistentry in docbook' do
+      input = <<-EOS
+term::
+alt term::
+definition
+
+last::
+      EOS
+      output = render_embedded_string input, :backend => 'docbook'
+      assert_xpath '//varlistentry', output, 2
+      assert_xpath '(//varlistentry)[1]/term', output, 2
+      assert_xpath '(//varlistentry)[2]/term', output, 1
+      assert_xpath '(//varlistentry)[2]/listitem', output, 1
+      assert_xpath '(//varlistentry)[2]/listitem[normalize-space(text())=""]', output, 1
+    end
+
     test "multi-line elements with blank line before paragraph content" do
       input = <<-EOS
 term1::
@@ -2028,6 +2225,23 @@ term 2:: def 2
       assert_css '.dlist dt:not([class])', output, 2
     end
 
+    test 'consecutive glossary terms should share same glossentry element in docbook' do
+      input = <<-EOS
+[glossary]
+term::
+alt term::
+definition
+
+last::
+      EOS
+      output = render_embedded_string input, :backend => 'docbook'
+      assert_xpath '/glossentry', output, 2
+      assert_xpath '(/glossentry)[1]/glossterm', output, 2
+      assert_xpath '(/glossentry)[2]/glossterm', output, 1
+      assert_xpath '(/glossentry)[2]/glossdef', output, 1
+      assert_xpath '(/glossentry)[2]/glossdef[normalize-space(text())=""]', output, 1
+    end
+
     test 'should render horizontal list with proper markup' do
       input = <<-EOS
 [horizontal]
@@ -2040,7 +2254,7 @@ second term:: definition
       output = render_embedded_string input
       assert_css '.hdlist', output, 1
       assert_css '.hdlist table', output, 1
-      assert_css '.hdlist table colgroup col', output, 2
+      assert_css '.hdlist table colgroup', output, 0
       assert_css '.hdlist table tr', output, 2
       assert_xpath '/*[@class="hdlist"]/table/tr[1]/td', output, 2
       assert_xpath '/*[@class="hdlist"]/table/tr[1]/td[@class="hdlist1"]', output, 1
@@ -2055,20 +2269,132 @@ second term:: definition
       assert_xpath '((//tr)[2]/td)[2]/p[normalize-space(text())="definition"]', output, 1
     end
 
-    test 'should render qanda list with proper semantics' do
+    test 'should set col widths of item and label if specified' do
+      input = <<-EOS
+[horizontal]
+[labelwidth="25", itemwidth="75"]
+term:: def
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'table', output, 1
+      assert_css 'table > colgroup', output, 1
+      assert_css 'table > colgroup > col', output, 2
+      assert_xpath '(//table/colgroup/col)[1][@style="width:25%;"]', output, 1
+      assert_xpath '(//table/colgroup/col)[2][@style="width:75%;"]', output, 1
+    end
+
+    test 'consecutive terms in horizontal list should share same cell' do
+      input = <<-EOS
+[horizontal]
+term::
+alt term::
+definition
+
+last::
+      EOS
+      output = render_embedded_string input 
+      assert_xpath '//tr', output, 2
+      assert_xpath '(//tr)[1]/td[@class="hdlist1"]', output, 1
+      assert_xpath '(//tr)[1]/td[@class="hdlist1"]/br', output, 2
+      assert_xpath '(//tr)[2]/td[@class="hdlist2"]', output, 1
+    end
+
+    test 'consecutive terms in horizontal list should share same entry in docbook' do
+      input = <<-EOS
+[horizontal]
+term::
+alt term::
+definition
+
+last::
+      EOS
+      output = render_embedded_string input, :backend => 'docbook' 
+      assert_xpath '//row', output, 2
+      assert_xpath '(//row)[1]/entry', output, 2
+      assert_xpath '((//row)[1]/entry)[1]/simpara', output, 2
+      assert_xpath '(//row)[2]/entry', output, 2
+      assert_xpath '((//row)[2]/entry)[2][normalize-space(text())=""]', output, 1
+    end
+
+    test 'should render horizontal list in docbook with proper markup' do
+      input = <<-EOS
+.Terms
+[horizontal]
+first term:: definition
++
+more detail
+
+second term:: definition
+      EOS
+      output = render_embedded_string input, :backend => 'docbook'
+      assert_xpath '/table', output, 1
+      assert_xpath '/table[@tabstyle="horizontal"]', output, 1
+      assert_xpath '/table[@tabstyle="horizontal"]/title[text()="Terms"]', output, 1
+      assert_xpath '/table//row', output, 2
+      assert_xpath '(/table//row)[1]/entry', output, 2
+      assert_xpath '(/table//row)[2]/entry', output, 2
+      assert_xpath '((/table//row)[1]/entry)[2]/simpara', output, 2
+    end
+
+    test 'should render qanda list in HTML with proper semantics' do
       input = <<-EOS
 [qanda]
-Question one::
-        Answer one.
-Question two::
-        Answer two.
+Question 1::
+        Answer 1.
+Question 2::
+        Answer 2.
       EOS
       output = render_embedded_string input
       assert_css '.qlist.qanda', output, 1
-      assert_css '.qlist ol', output, 1
-      assert_css '.qlist ol li', output, 2
-      assert_css '.qlist ol li:nth-child(1) p em', output, 1
-      assert_css '.qlist ol li:nth-child(1) p', output, 2
+      assert_css '.qanda > ol', output, 1
+      assert_css '.qanda > ol > li', output, 2
+      (1..2).each do |idx|
+        assert_css ".qanda > ol > li:nth-child(#{idx}) > p", output, 2
+        assert_css ".qanda > ol > li:nth-child(#{idx}) > p:first-child > em", output, 1
+        assert_xpath "/*[@class = 'qlist qanda']/ol/li[#{idx}]/p[1]/em[normalize-space(text()) = 'Question #{idx}']", output, 1
+        assert_css ".qanda > ol > li:nth-child(#{idx}) > p:last-child > *", output, 0
+        assert_xpath "/*[@class = 'qlist qanda']/ol/li[#{idx}]/p[2][normalize-space(text()) = 'Answer #{idx}.']", output, 1
+      end
+    end
+
+    test 'should render qanda list in DocBook with proper semantics' do
+      input = <<-EOS
+[qanda]
+Question 1::
+        Answer 1.
+Question 2::
+        Answer 2.
+      EOS
+      output = render_embedded_string input, :backend => 'docbook'
+      assert_css 'qandaset', output, 1
+      assert_css 'qandaset > qandaentry', output, 2
+      (1..2).each do |idx|
+        assert_css "qandaset > qandaentry:nth-child(#{idx}) > question", output, 1
+        assert_css "qandaset > qandaentry:nth-child(#{idx}) > question > simpara", output, 1
+        assert_xpath "/qandaset/qandaentry[#{idx}]/question/simpara[normalize-space(text()) = 'Question #{idx}']", output, 1
+        assert_css "qandaset > qandaentry:nth-child(#{idx}) > answer", output, 1
+        assert_css "qandaset > qandaentry:nth-child(#{idx}) > answer > simpara", output, 1
+        assert_xpath "/qandaset/qandaentry[#{idx}]/answer/simpara[normalize-space(text()) = 'Answer #{idx}.']", output, 1
+      end
+    end
+
+    test 'consecutive questions should share same question element in docbook' do
+      input = <<-EOS
+[qanda]
+question::
+follow-up question::
+response
+
+last question::
+      EOS
+      output = render_embedded_string input, :backend => 'docbook'
+      assert_xpath '//qandaentry', output, 2
+      assert_xpath '(//qandaentry)[1]/question', output, 1
+      assert_xpath '(//qandaentry)[1]/question/simpara', output, 2
+      assert_xpath '(//qandaentry)[2]/question', output, 1
+      assert_xpath '(//qandaentry)[2]/answer', output, 1
+      assert_xpath '(//qandaentry)[2]/answer[normalize-space(text())=""]', output, 1
     end
 
     test 'should render bibliography list with proper semantics' do
@@ -2640,6 +2966,55 @@ para
       assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]', output, 1
       assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]/p[text()="para"]', output, 1
     end
+
+    test 'attached paragraph does not break on adjacent nested labeled list term' do
+      input = <<-EOS
+term1:: def
++
+more definition
+not a term::: def
+      EOS
+
+      output = render_embedded_string input
+      assert_css '.dlist > dl > dt', output, 1
+      assert_css '.dlist > dl > dd', output, 1
+      assert_css '.dlist > dl > dd > .paragraph', output, 1
+      assert output.include?('not a term::: def')
+    end
+
+    # FIXME pending
+=begin
+    test 'attached paragraph does not break on adjacent sibling labeled list term' do
+      input = <<-EOS
+term1:: def
++
+more definition
+not a term:: def
+      EOS
+
+      output = render_embedded_string input
+      assert_css '.dlist > dl > dt', output, 1
+      assert_css '.dlist > dl > dd', output, 1
+      assert_css '.dlist > dl > dd > .paragraph', output, 1
+      assert output.include?('not a term:: def')
+    end
+=end
+
+    test 'attached styled paragraph does not break on adjacent nested labeled list term' do
+      input = <<-EOS
+term1:: def
++
+[quote]
+more definition
+not a term::: def
+      EOS
+
+      output = render_embedded_string input
+      assert_css '.dlist > dl > dt', output, 1
+      assert_css '.dlist > dl > dd', output, 1
+      assert_css '.dlist > dl > dd > .quoteblock', output, 1
+      assert output.include?('not a term::: def')
+    end
   
     test 'appends line as paragraph if attached by continuation following blank line and line comment when term has no inline definition' do
       input = <<-EOS
@@ -3058,7 +3433,7 @@ end
 context 'Callout lists' do
   test 'listing block with sequential callouts followed by adjacent callout list' do
     input = <<-EOS
-[source]
+[source, ruby]
 ----
 require 'asciidoctor' # <1>
 doc = Asciidoctor::Document.new('Hello, World!') # <2>
@@ -3082,7 +3457,7 @@ puts doc.render # <3>
 
   test 'listing block with sequential callouts followed by non-adjacent callout list' do
     input = <<-EOS
-[source]
+[source, ruby]
 ----
 require 'asciidoctor' # <1>
 doc = Asciidoctor::Document.new('Hello, World!') # <2>
@@ -3110,7 +3485,7 @@ Paragraph.
 
   test 'listing block with a callout that refers to two different lines' do
     input = <<-EOS
-[source]
+[source, ruby]
 ----
 require 'asciidoctor' # <1>
 doc = Asciidoctor::Document.new('Hello, World!') # <2>
@@ -3132,7 +3507,7 @@ puts doc.render # <2>
 
   test 'listing block with non-sequential callouts followed by adjacent callout list' do
     input = <<-EOS
-[source]
+[source, ruby]
 ----
 require 'asciidoctor' # <2>
 doc = Asciidoctor::Document.new('Hello, World!') # <3>
@@ -3157,13 +3532,13 @@ puts doc.render # <1>
   test 'two listing blocks can share the same callout list' do
     input = <<-EOS
 .Import library
-[source]
+[source, ruby]
 ----
 require 'asciidoctor' # <1>
 ----
 
 .Use library
-[source]
+[source, ruby]
 ----
 doc = Asciidoctor::Document.new('Hello, World!') # <2>
 puts doc.render # <3>
@@ -3188,14 +3563,14 @@ puts doc.render # <3>
   test 'two listing blocks each followed by an adjacent callout list' do
     input = <<-EOS
 .Import library
-[source]
+[source, ruby]
 ----
 require 'asciidoctor' # <1>
 ----
 <1> Describe the first line
 
 .Use library
-[source]
+[source, ruby]
 ----
 doc = Asciidoctor::Document.new('Hello, World!') # <1>
 puts doc.render # <2>
@@ -3220,7 +3595,7 @@ puts doc.render # <2>
 
   test 'callout list with block content' do
     input = <<-EOS
-[source]
+[source, ruby]
 ----
 require 'asciidoctor' # <1>
 doc = Asciidoctor::Document.new('Hello, World!') # <2>
@@ -3247,7 +3622,7 @@ You can write this to file rather than printing to stdout.
 
   test 'escaped callout should not be interpreted as a callout' do
     input = <<-EOS
-[source]
+[source, ruby]
 ----
 require 'asciidoctor' # \\<1>
 ----
@@ -3279,7 +3654,7 @@ Violets are blue <2>
 
   test 'callout list with icons enabled' do
     input = <<-EOS
-[source]
+[source, ruby]
 ----
 require 'asciidoctor' # <1>
 doc = Asciidoctor::Document.new('Hello, World!') # <2>
@@ -3292,11 +3667,34 @@ puts doc.render # <3>
     output = render_embedded_string input, :attributes => {'icons' => ''}
     assert_css '.listingblock code > img', output, 3
     (1..3).each do |i|
-      assert_xpath %((/div[@class="listingblock"]//code/img)[#{i}][@src="images/icons/callouts/#{i}.png"][@alt="#{i}"]), output, 1
+      assert_xpath %((/div[@class="listingblock"]//code/img)[#{i}][@src="./images/icons/callouts/#{i}.png"][@alt="#{i}"]), output, 1
     end
     assert_css '.colist table td img', output, 3
     (1..3).each do |i|
-      assert_xpath %((/div[@class="colist arabic"]//td/img)[#{i}][@src="images/icons/callouts/#{i}.png"][@alt="#{i}"]), output, 1
+      assert_xpath %((/div[@class="colist arabic"]//td/img)[#{i}][@src="./images/icons/callouts/#{i}.png"][@alt="#{i}"]), output, 1
+    end
+  end
+
+  test 'callout list with font-based icons enabled' do
+    input = <<-EOS
+[source]
+----
+require 'asciidoctor' # <1>
+doc = Asciidoctor::Document.new('Hello, World!') # <2>
+puts doc.render # <3>
+----
+<1> Describe the first line
+<2> Describe the second line
+<3> Describe the third line
+    EOS
+    output = render_embedded_string input, :attributes => {'icons' => 'font'}
+    assert_css '.listingblock code > i', output, 3
+    (1..3).each do |i|
+      assert_xpath %((/div[@class="listingblock"]//code/i)[#{i}][@class="conum"][text() = "#{i}"]), output, 1
+    end
+    assert_css '.colist table td i', output, 3
+    (1..3).each do |i|
+      assert_xpath %((/div[@class="colist arabic"]//td/i)[#{i}][@class="conum"][text() = "#{i}"]), output, 1
     end
   end
 end
diff --git a/test/options_test.rb b/test/options_test.rb
index feb089a..7c51cd3 100644
--- a/test/options_test.rb
+++ b/test/options_test.rb
@@ -20,9 +20,9 @@ context 'Options' do
 
   test 'should return error code 1 when option has invalid argument' do
     redirect_streams do |stdout, stderr|
-      exitval = Asciidoctor::Cli::Options.parse!(%w(-b foo input.ad))
+      exitval = Asciidoctor::Cli::Options.parse!(%w(-d chapter input.ad)) # had to change for #320
       assert_equal 1, exitval
-      assert_equal 'asciidoctor: invalid argument: -b foo', stderr.string.chomp
+      assert_equal 'asciidoctor: invalid argument: -d chapter', stderr.string.chomp
     end
   end
 
@@ -65,4 +65,31 @@ context 'Options' do
     assert_equal '', options[:attributes]['icons']
   end
 
+  test 'should only split attribute key/value pairs on first equal sign' do
+    options = Asciidoctor::Cli::Options.parse!(%w(-a name=value=value test/fixtures/sample.asciidoc))
+
+    assert_equal 'value=value', options[:attributes]['name']
+  end
+
+  test 'should allow any backend to be specified' do
+    options = Asciidoctor::Cli::Options.parse!(%w(-b my_custom_backend test/fixtures/sample.asciidoc))
+
+    assert_equal 'my_custom_backend', options[:attributes]['backend']
+  end
+
+  test 'article doctype assignment' do
+    options = Asciidoctor::Cli::Options.parse!(%w(-d article test/fixtures/sample.asciidoc))
+    assert_equal 'article', options[:attributes]['doctype']
+  end
+
+  test 'book doctype assignment' do
+    options = Asciidoctor::Cli::Options.parse!(%w(-d book test/fixtures/sample.asciidoc))
+    assert_equal 'book', options[:attributes]['doctype']
+  end
+
+  test 'inline doctype assignment' do
+    options = Asciidoctor::Cli::Options.parse!(%w(-d inline test/fixtures/sample.asciidoc))
+    assert_equal 'inline', options[:attributes]['doctype']
+  end
+
 end
diff --git a/test/paragraphs_test.rb b/test/paragraphs_test.rb
index a15ebd4..8f47ed0 100644
--- a/test/paragraphs_test.rb
+++ b/test/paragraphs_test.rb
@@ -1,21 +1,53 @@
 require 'test_helper'
 
-context "Paragraphs" do
+context 'Paragraphs' do
   context 'Normal' do
-    test "rendered correctly" do
-      assert_xpath "//p", render_string("Plain text for the win.\n\nYes, plainly."), 2
+    test 'should treat plain text separated by blank lines as paragraphs' do
+      input = <<-EOS
+Plain text for the win!
+
+Yep. Text. Plain and simple.
+      EOS
+      output = render_embedded_string input
+      assert_css 'p', output, 2
+      assert_xpath '(//p)[1][text() = "Plain text for the win!"]', output, 1
+      assert_xpath '(//p)[2][text() = "Yep. Text. Plain and simple."]', output, 1
     end
 
-    test "with title" do
-      rendered = render_string(".Titled\nParagraph.\n\nWinning")
+    test 'should associate block title with paragraph' do
+      input = <<-EOS
+.Titled
+Paragraph.
+
+Winning.
+      EOS
+      output = render_embedded_string input
       
-      assert_xpath "//div[@class='title']", rendered
-      assert_xpath "//p", rendered, 2
+      assert_css 'p', output, 2
+      assert_xpath '(//p)[1]/preceding-sibling::*[@class = "title"]', output, 1
+      assert_xpath '(//p)[1]/preceding-sibling::*[@class = "title"][text() = "Titled"]', output, 1
+      assert_xpath '(//p)[2]/preceding-sibling::*[@class = "title"]', output, 0
     end
 
-    test "no duplicate block before next section" do
-      rendered = render_string("Title\n=====\n\nPreamble.\n\n== First Section\n\nParagraph 1\n\nParagraph 2\n\n\n== Second Section\n\nLast words")
-      assert_xpath '//p[text()="Paragraph 2"]', rendered, 1
+    test 'no duplicate block before next section' do
+      input = <<-EOS
+= Title
+
+Preamble
+
+== First Section
+
+Paragraph 1
+
+Paragraph 2
+
+== Second Section
+
+Last words
+      EOS
+
+      output = render_string input
+      assert_xpath '//p[text() = "Paragraph 2"]', output, 1
     end
 
     test 'does not treat wrapped line as a list item' do
@@ -40,6 +72,62 @@ paragraph
       assert_xpath %(//p[text()="paragraph\n.wrapped line"]), output, 1
     end
 
+    test 'interprets normal paragraph style as normal paragraph' do
+      input = <<-EOS
+[normal]
+Normal paragraph.
+Nothing special.
+      EOS
+
+      output = render_embedded_string input
+      assert_css 'p', output, 1
+    end
+
+    test 'normal paragraph terminates at block attribute list' do
+      input = <<-EOS
+normal text
+[literal]
+literal text
+      EOS
+      output = render_embedded_string input
+      assert_css '.paragraph:root', output, 1
+      assert_css '.literalblock:root', output, 1
+    end
+
+    test 'normal paragraph terminates at block delimiter' do
+      input = <<-EOS
+normal text
+--
+text in open block
+--
+      EOS
+      output = render_embedded_string input
+      assert_css '.paragraph:root', output, 1
+      assert_css '.openblock:root', output, 1
+    end
+
+    test 'normal paragraph terminates at list continuation' do
+      input = <<-EOS
+normal text
++
+      EOS
+      output = render_embedded_string input
+      assert_css '.paragraph:root', output, 2
+      assert_xpath %((/*[@class="paragraph"])[1]/p[text() = "normal text"]), output, 1
+      assert_xpath %((/*[@class="paragraph"])[2]/p[text() = "+"]), output, 1
+    end
+
+    test 'normal style turns literal paragraph into normal paragraph' do
+      input = <<-EOS
+[normal]
+ normal paragraph,
+ despite the leading indent
+      EOS
+
+      output = render_embedded_string input
+      assert_css '.paragraph:root > p', output, 1
+    end
+
     test 'expands index term macros in DocBook backend' do
       input = <<-EOS
 Here is an index entry for ((tigers)).
@@ -81,14 +169,32 @@ Note that multi-entry terms generate separate index entries.
       assert_xpath '(//indexterm)[7]/*', output, 2
       assert_xpath '(//indexterm)[8]/*', output, 1
     end
+
+    test 'normal paragraph should honor explicit subs list' do
+      input = <<-EOS
+[subs="specialcharacters"]
+*Hey Jude*
+      EOS
+
+      output = render_embedded_string input
+      assert output.include?('*Hey Jude*')
+    end
   end
 
-  context "code" do
-    test "single-line literal paragraphs" do
-      assert_xpath "//pre", render_string("    LITERALS\n\n    ARE LITERALLY\n\n    AWESOMMMME.")
+  context 'Literal' do
+    test 'single-line literal paragraphs' do
+      input = <<-EOS
+ LITERALS
+
+ ARE LITERALLY
+
+ AWESOME!
+      EOS
+      output = render_embedded_string input
+      assert_xpath '//pre', output, 3
     end
 
-    test "multi-line literal paragraph" do
+    test 'multi-line literal paragraph' do
       input = <<-EOS
 Install instructions:
 
@@ -97,106 +203,154 @@ Install instructions:
 
 You're good to go!
       EOS
-      output = render_string(input)
-      assert_xpath "//pre", output, 1
-      assert_match(/^gem install asciidoctor/, output, "Indentation should be trimmed from literal block")
+      output = render_embedded_string input
+      assert_xpath '//pre', output, 1
+      # indentation should be trimmed from literal block
+      assert_xpath %(//pre[text() = "yum install ruby rubygems\ngem install asciidoctor"]), output, 1
     end
 
-    test "literal paragraph" do
-      assert_xpath "//*[@class='literalblock']//pre[text()='blah blah blah']", render_string("[literal]\nblah blah blah")
+    test 'literal paragraph' do
+      input = <<-EOS
+[literal]
+this text is literally literal
+      EOS
+      output = render_embedded_string input
+      assert_xpath %(/*[@class="literalblock"]//pre[text()="this text is literally literal"]), output, 1
     end
 
-    test "listing paragraph" do
-      assert_xpath "//*[@class='listingblock']//pre[text()='blah blah blah']", render_string("[listing]\nblah blah blah")
+    test 'should read content below literal style verbatim' do
+      input = <<-EOS
+[literal]
+image::not-an-image-block[]
+      EOS
+      output = render_embedded_string input
+      assert_xpath %(/*[@class="literalblock"]//pre[text()="image::not-an-image-block[]"]), output, 1
+      assert_css 'img', output, 0
     end
 
-    test "source code paragraph" do
-      assert_xpath "//pre[@class='highlight']/code", render_string("[source]\nblah blah blah")
+    test 'listing paragraph' do
+      input = <<-EOS
+[listing]
+this text is a listing
+      EOS
+      output = render_embedded_string input
+      assert_xpath %(/*[@class="listingblock"]//pre[text()="this text is a listing"]), output, 1
     end
 
-    test "source code paragraph with language" do
-      assert_xpath "//pre[@class='highlight']/code[@class='perl']", render_string("[source, perl]\ndie 'zomg perl sucks';")
+    test 'source paragraph' do
+      input = <<-EOS
+[source]
+use the source, luke!
+      EOS
+      output = render_embedded_string input
+      assert_xpath %(/*[@class="listingblock"]//pre[@class="highlight"]/code[text()="use the source, luke!"]), output, 1
     end
-  end
 
-  context "quote" do
-    test "quote block" do
-      output = render_string("____\nFamous quote.\n____")
-      assert_xpath '//*[@class = "quoteblock"]', output, 1
-      assert_xpath '//*[@class = "quoteblock"]//p[text() = "Famous quote."]', output, 1
+    test 'source code paragraph with language' do
+      input = <<-EOS
+[source, perl]
+die 'zomg perl sucks';
+      EOS
+      output = render_embedded_string input
+      assert_xpath %(/*[@class="listingblock"]//pre[@class="highlight"]/code[@class="perl language-perl"][text()="die 'zomg perl sucks';"]), output, 1
     end
 
-    test "quote block with attribution" do
-      output = render_string("[quote, A famous person, A famous book (1999)]\n____\nFamous quote.\n____")
-      assert_xpath '//*[@class = "quoteblock"]', output, 1
-      assert_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]', output, 1
-      assert_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]/cite[text() = "A famous book (1999)"]', output, 1
-      # TODO I can't seem to match the attribution (author) w/ xpath
+    test 'literal paragraph terminates at block attribute list' do
+      input = <<-EOS
+ literal text
+[normal]
+normal text
+      EOS
+      output = render_embedded_string input
+      assert_xpath %(/*[@class="literalblock"]), output, 1
+      assert_xpath %(/*[@class="paragraph"]), output, 1
     end
 
-    test "quote block with section body" do
-      output = render_string("____\nFamous quote.\n\nNOTE: That was inspiring.\n____")
-      assert_xpath '//*[@class = "quoteblock"]', output, 1
-      assert_xpath '//*[@class = "quoteblock"]//*[@class = "admonitionblock"]', output, 1
+    test 'literal paragraph terminates at block delimiter' do
+      input = <<-EOS
+ literal text
+--
+normal text
+--
+      EOS
+      output = render_embedded_string input
+      assert_xpath %(/*[@class="literalblock"]), output, 1
+      assert_xpath %(/*[@class="openblock"]), output, 1
     end
 
+    test 'literal paragraph terminates at list continuation' do
+      input = <<-EOS
+ literal text
++
+      EOS
+      output = render_embedded_string input
+      assert_xpath %(/*[@class="literalblock"]), output, 1
+      assert_xpath %(/*[@class="literalblock"]//pre[text() = "literal text"]), output, 1
+      assert_xpath %(/*[@class="paragraph"]), output, 1
+      assert_xpath %(/*[@class="paragraph"]/p[text() = "+"]), output, 1
+    end
+  end
+
+  context 'Quote' do
     test "single-line quote paragraph" do
-      output = render_string("[quote]\nFamous quote.")
+      input = <<-EOS
+[quote]
+Famous quote.
+      EOS
+      output = render_string input
       assert_xpath '//*[@class = "quoteblock"]', output, 1
       assert_xpath '//*[@class = "quoteblock"]//p', output, 0
       assert_xpath '//*[@class = "quoteblock"]//*[contains(text(), "Famous quote.")]', output, 1
     end
 
-    test "verse paragraph" do
-      output = render_string("[verse]\nFamous verse.")
-      assert_xpath '//*[@class = "verseblock"]', output, 1
-      assert_xpath '//*[@class = "verseblock"]/pre', output, 1
-      assert_xpath '//*[@class = "verseblock"]//p', output, 0
-      assert_xpath '//*[@class = "verseblock"]/pre[normalize-space(text()) = "Famous verse."]', output, 1
+    test 'quote paragraph terminates at list continuation' do
+      input = <<-EOS
+[quote]
+A famouse quote.
++
+      EOS
+      output = render_embedded_string input
+      assert_css '.quoteblock:root', output, 1
+      assert_css '.paragraph:root', output, 1
+      assert_xpath %(/*[@class="paragraph"]/p[text() = "+"]), output, 1
     end
 
-    test "single-line verse block" do
-      output = render_string("[verse]\n____\nFamous verse.\n____")
+    test "verse paragraph" do
+      output = render_string("[verse]\nFamous verse.")
       assert_xpath '//*[@class = "verseblock"]', output, 1
       assert_xpath '//*[@class = "verseblock"]/pre', output, 1
       assert_xpath '//*[@class = "verseblock"]//p', output, 0
       assert_xpath '//*[@class = "verseblock"]/pre[normalize-space(text()) = "Famous verse."]', output, 1
     end
 
-    test "multi-line verse block" do
-      output = render_string("[verse]\n____\nFamous verse.\n\nStanza two.\n____")
-      assert_xpath '//*[@class = "verseblock"]', output, 1
-      assert_xpath '//*[@class = "verseblock"]/pre', output, 1
-      assert_xpath '//*[@class = "verseblock"]//p', output, 0
-      assert_xpath '//*[@class = "verseblock"]/pre[contains(text(), "Famous verse.")]', output, 1
-      assert_xpath '//*[@class = "verseblock"]/pre[contains(text(), "Stanza two.")]', output, 1
-    end
+    test 'quote paragraph should honor explicit subs list' do
+      input = <<-EOS
+[subs="specialcharacters"]
+[quote]
+*Hey Jude*
+      EOS
 
-    test "verse block does not contain block elements" do
-      output = render_string("[verse]\n____\nFamous verse.\n\n....\nnot a literal\n....\n____")
-      assert_xpath '//*[@class = "verseblock"]', output, 1
-      assert_xpath '//*[@class = "verseblock"]/pre', output, 1
-      assert_xpath '//*[@class = "verseblock"]//p', output, 0
-      assert_xpath '//*[@class = "verseblock"]//*[@class = "literalblock"]', output, 0
+      output = render_embedded_string input
+      assert output.include?('*Hey Jude*')
     end
   end
 
   context "special" do
     test "note multiline syntax" do
       Asciidoctor::ADMONITION_STYLES.each do |style|
-        assert_xpath "//div[@class='admonitionblock']", render_string("[#{style}]\nThis is a winner.")
+        assert_xpath "//div[@class='admonitionblock #{style.downcase}']", render_string("[#{style}]\nThis is a winner.")
       end
     end
 
     test "note block syntax" do
       Asciidoctor::ADMONITION_STYLES.each do |style|
-        assert_xpath "//div[@class='admonitionblock']", render_string("[#{style}]\n====\nThis is a winner.\n====")
+        assert_xpath "//div[@class='admonitionblock #{style.downcase}']", render_string("[#{style}]\n====\nThis is a winner.\n====")
       end
     end
 
     test "note inline syntax" do
       Asciidoctor::ADMONITION_STYLES.each do |style|
-        assert_xpath "//div[@class='admonitionblock']", render_string("#{style}: This is important, fool!")
+        assert_xpath "//div[@class='admonitionblock #{style.downcase}']", render_string("#{style}: This is important, fool!")
       end
     end
 
@@ -212,5 +366,104 @@ Content goes here
       result = render_string(input)
       assert_xpath "//*[@class='sidebarblock']//p", result, 1
     end
+
+    context 'Styled Paragraphs' do
+      test 'should wrap text in simpara for styled paragraphs when rendered to DocBook' do
+        input = <<-EOS
+= Book
+:doctype: book
+
+[preface]
+= About this book
+
+[abstract]
+An abstract for the book.
+
+= Part 1
+
+[partintro]
+An intro to this part.
+
+[sidebar]
+Just a side note.
+
+[example]
+As you can see here.
+
+[quote]
+Wise words from a wise person.
+        EOS
+
+        output = render_string input, :backend => 'docbook'
+        assert_css 'abstract > simpara', output, 1
+        assert_css 'partintro > simpara', output, 1
+        assert_css 'sidebar > simpara', output, 1
+        assert_css 'informalexample > simpara', output, 1
+        assert_css 'blockquote > simpara', output, 1
+      end
+
+      test 'should wrap text in simpara for styled paragraphs with title when rendered to DocBook' do
+        input = <<-EOS
+= Book
+:doctype: book
+
+[preface]
+= About this book
+
+[abstract]
+.Abstract title
+An abstract for the book.
+
+= Part 1
+
+[partintro]
+.Part intro title
+An intro to this part.
+
+[sidebar]
+.Sidebar title
+Just a side note.
+
+[example]
+.Example title
+As you can see here.
+
+[quote]
+.Quote title
+Wise words from a wise person.
+        EOS
+
+        output = render_string input, :backend => 'docbook'
+        assert_css 'abstract > title', output, 1
+        assert_xpath '//abstract/title[text() = "Abstract title"]', output, 1
+        assert_css 'abstract > title + simpara', output, 1
+        assert_css 'partintro > title', output, 1
+        assert_xpath '//partintro/title[text() = "Part intro title"]', output, 1
+        assert_css 'partintro > title + simpara', output, 1
+        assert_css 'sidebar > title', output, 1
+        assert_xpath '//sidebar/title[text() = "Sidebar title"]', output, 1
+        assert_css 'sidebar > title + simpara', output, 1
+        assert_css 'example > title', output, 1
+        assert_xpath '//example/title[text() = "Example title"]', output, 1
+        assert_css 'example > title + simpara', output, 1
+        assert_css 'blockquote > title', output, 1
+        assert_xpath '//blockquote/title[text() = "Quote title"]', output, 1
+        assert_css 'blockquote > title + simpara', output, 1
+      end
+    end
+
+    context 'Inline doctype' do
+      test 'should only format and output text in first paragraph when doctype is inline' do
+        input = "http://asciidoc.org[AsciiDoc] is a _lightweight_ markup language...\n\nignored"
+        output = render_string input, :doctype => 'inline'
+        assert_equal '<a href="http://asciidoc.org">AsciiDoc</a> is a <em>lightweight</em> markup language…', output
+      end
+
+      test 'should output empty string if first block is not a paragraph' do
+        input = '* bullet'
+        output = render_string input, :doctype => 'inline'
+        assert output.empty?
+      end
+    end
   end
 end
diff --git a/test/paths_test.rb b/test/paths_test.rb
new file mode 100644
index 0000000..3940c38
--- /dev/null
+++ b/test/paths_test.rb
@@ -0,0 +1,174 @@
+require 'test_helper'
+
+context 'Path Resolver' do
+  context 'Web Paths' do
+    def setup
+      @resolver = Asciidoctor::PathResolver.new
+    end
+
+    test 'target with absolute path' do
+      assert_equal '/images', @resolver.web_path('/images')
+      assert_equal '/images', @resolver.web_path('/images', '')
+      assert_equal '/images', @resolver.web_path('/images', nil)
+    end
+
+    test 'target with relative path' do
+      assert_equal 'images', @resolver.web_path('images')
+      assert_equal 'images', @resolver.web_path('images', '')
+      assert_equal 'images', @resolver.web_path('images', nil)
+    end
+
+    test 'target with path relative to current directory' do
+      assert_equal './images', @resolver.web_path('./images')
+      assert_equal './images', @resolver.web_path('./images', '')
+      assert_equal './images', @resolver.web_path('./images', nil)
+    end
+
+    test 'target with absolute path ignores start path' do
+      assert_equal '/images', @resolver.web_path('/images', 'foo')
+      assert_equal '/images', @resolver.web_path('/images', '/foo')
+      assert_equal '/images', @resolver.web_path('/images', './foo')
+    end
+
+    test 'target with relative path appended to start path' do
+      assert_equal 'assets/images', @resolver.web_path('images', 'assets')
+      assert_equal '/assets/images', @resolver.web_path('images', '/assets')
+      assert_equal './assets/images', @resolver.web_path('images', './assets')
+    end
+
+    test 'target with path relative to current directory appended to start path' do
+      assert_equal 'assets/images', @resolver.web_path('./images', 'assets')
+      assert_equal '/assets/images', @resolver.web_path('./images', '/assets')
+      assert_equal './assets/images', @resolver.web_path('./images', './assets')
+    end
+
+    test 'normalize target' do
+      assert_equal '../images', @resolver.web_path('../images/../images')
+    end
+
+    test 'append target to start path and normalize' do
+      assert_equal '../images', @resolver.web_path('../images/../images', '../images')
+      assert_equal '../../images', @resolver.web_path('../images', '..')
+    end
+
+    test 'normalize parent directory that follows root' do
+      assert_equal '/tiger.png', @resolver.web_path('/../tiger.png')
+      assert_equal '/tiger.png', @resolver.web_path('/../../tiger.png')
+    end
+
+    test 'uses start when target is empty' do
+      assert_equal 'assets/images', @resolver.web_path('', 'assets/images')
+      assert_equal 'assets/images', @resolver.web_path(nil, 'assets/images')
+    end
+
+    test 'posixfies windows paths' do
+      assert_equal '/images', @resolver.web_path('\\images')
+      assert_equal '../images', @resolver.web_path('..\\images')
+      assert_equal '/images', @resolver.web_path('\\..\\images')
+      assert_equal 'assets/images', @resolver.web_path('assets\\images')
+      assert_equal '../assets/images', @resolver.web_path('assets\\images', '..\\images\\..')
+    end
+  end
+
+  context 'System Paths' do
+    JAIL = '/home/doctor/docs'
+
+    def setup
+      @resolver = Asciidoctor::PathResolver.new
+    end
+
+    test 'prevents access to paths outside of jail' do
+      assert_equal "#{JAIL}/css", @resolver.system_path('../../../../../css', "#{JAIL}/assets/stylesheets", JAIL)
+      assert_equal "#{JAIL}/css", @resolver.system_path('/../../../../../css', "#{JAIL}/assets/stylesheets", JAIL)
+      assert_equal "#{JAIL}/css", @resolver.system_path('../../../css', '../../..', JAIL)
+    end
+
+    test 'throws exception for illegal path access if recover is false' do
+      begin
+        @resolver.system_path('../../../../../css', "#{JAIL}/assets/stylesheets", JAIL, :recover => false)
+        flunk 'Expecting SecurityError to be raised'
+      rescue SecurityError
+      end
+    end
+
+    test 'resolves start path if target is empty' do
+      assert_equal "#{JAIL}/assets/stylesheets", @resolver.system_path('', "#{JAIL}/assets/stylesheets", JAIL)
+      assert_equal "#{JAIL}/assets/stylesheets", @resolver.system_path(nil, "#{JAIL}/assets/stylesheets", JAIL)
+    end
+
+    test 'resolves start path if target is dot' do
+      assert_equal "#{JAIL}/assets/stylesheets", @resolver.system_path('.', "#{JAIL}/assets/stylesheets", JAIL)
+      assert_equal "#{JAIL}/assets/stylesheets", @resolver.system_path('./', "#{JAIL}/assets/stylesheets", JAIL)
+    end
+
+    test 'treats absolute target as relative when jail is specified' do
+      assert_equal "#{JAIL}/assets/stylesheets", @resolver.system_path('/', "#{JAIL}/assets/stylesheets", JAIL)
+      assert_equal "#{JAIL}/assets/stylesheets/foo", @resolver.system_path('/foo', "#{JAIL}/assets/stylesheets", JAIL)
+      assert_equal "#{JAIL}/assets/foo", @resolver.system_path('/../foo', "#{JAIL}/assets/stylesheets", JAIL)
+    end
+
+    test 'allows use of absolute target or start if resolved path is sub-path of jail' do
+      assert_equal "#{JAIL}/my/path", @resolver.system_path("#{JAIL}/my/path", '', JAIL)
+      assert_equal "#{JAIL}/my/path", @resolver.system_path("#{JAIL}/my/path", nil, JAIL)
+      assert_equal "#{JAIL}/my/path", @resolver.system_path('', "#{JAIL}/my/path", JAIL)
+      assert_equal "#{JAIL}/my/path", @resolver.system_path(nil, "#{JAIL}/my/path", JAIL)
+      assert_equal "#{JAIL}/my/path", @resolver.system_path('path', "#{JAIL}/my", JAIL)
+    end
+
+    test 'uses jail path if start path is empty' do
+      assert_equal "#{JAIL}/images/tiger.png", @resolver.system_path('images/tiger.png', '', JAIL)
+      assert_equal "#{JAIL}/images/tiger.png", @resolver.system_path('images/tiger.png', nil, JAIL)
+    end
+
+    test 'raises security error if start is not contained within jail' do
+      begin
+        @resolver.system_path('images/tiger.png', '/etc', JAIL)
+        flunk 'Expecting SecurityError to be raised'
+      rescue SecurityError
+      end
+
+      begin
+        @resolver.system_path('.', '/etc', JAIL)
+        flunk 'Expecting SecurityError to be raised'
+      rescue SecurityError
+      end
+    end
+
+    test 'resolves absolute directory if jail is not specified' do
+      assert_equal '/usr/share/stylesheet.css', @resolver.system_path('/usr/share/stylesheet.css', '/home/dallen/docs/assets/stylesheets')
+    end
+
+    test 'resolves ancestor directory of start if jail is not specified' do
+      assert_equal '/usr/share/stylesheet.css', @resolver.system_path('../../../../../usr/share/stylesheet.css', '/home/dallen/docs/assets/stylesheets')
+    end
+
+    test 'resolves absolute path if start is absolute and target is relative' do
+      assert_equal '/usr/share/assets/stylesheet.css', @resolver.system_path('assets/stylesheet.css', '/usr/share')
+    end
+
+    test 'resolves relative target relative to current directory if start is empty' do
+      pwd = File.expand_path(Dir.pwd)
+      assert_equal "#{pwd}/images/tiger.png", @resolver.system_path('images/tiger.png', '')
+      assert_equal "#{pwd}/images/tiger.png", @resolver.system_path('images/tiger.png', nil)
+    end
+
+    test 'resolves and normalizes start with target is empty' do
+      pwd = File.expand_path(Dir.pwd)
+      assert_equal '/home/doctor/docs', @resolver.system_path('', '/home/doctor/docs')
+      assert_equal '/home/doctor/docs', @resolver.system_path(nil, '/home/doctor/docs')
+      assert_equal "#{pwd}/assets/images", @resolver.system_path(nil, 'assets/images')
+      assert_equal "#{JAIL}/assets/images", @resolver.system_path('', '../assets/images', JAIL)
+    end
+
+    test 'posixfies windows paths' do
+      assert_equal "#{JAIL}/assets/css", @resolver.system_path('..\\css', 'assets\\stylesheets', JAIL)
+    end
+
+    test 'resolves windows paths when file separator is backlash' do
+      @resolver.file_separator = '\\'
+      assert_equal 'C:/data/docs', @resolver.system_path('..', "C:\\data\\docs\\assets", 'C:\\data\\docs')
+      assert_equal 'C:/data/docs', @resolver.system_path('..\\..', "C:\\data\\docs\\assets", 'C:\\data\\docs')
+      assert_equal 'C:/data/docs/css', @resolver.system_path('..\\..\\css', "C:\\data\\docs\\assets", 'C:\\data\\docs')
+    end
+  end
+end
diff --git a/test/reader_test.rb b/test/reader_test.rb
index 52ff8a8..4053101 100644
--- a/test/reader_test.rb
+++ b/test/reader_test.rb
@@ -153,13 +153,14 @@ This is a paragraph outside the block.
   end
 
   context 'Include Macro' do
-    test 'include macro is disabled by default' do
+    test 'include macro is disabled by default and becomes a link' do
       input = <<-EOS
 include::include-file.asciidoc[]
       EOS
       para = block_from_string input, :attributes => { 'include-depth' => 0 }
       assert_equal 1, para.buffer.size
-      assert_equal 'include::include-file.asciidoc[]', para.buffer.join
+      #assert_equal 'include::include-file.asciidoc[]', para.buffer.join
+      assert_equal 'link:include-file.asciidoc[include-file.asciidoc]', para.buffer.join
     end
 
     test 'include macro is enabled when safe mode is less than SECURE' do
@@ -172,6 +173,89 @@ include::fixtures/include-file.asciidoc[]
       assert_match(/included content/, output)
     end
 
+    test 'missing file referenced by include macro does not crash processor' do
+      input = <<-EOS
+include::fixtures/no-such-file.ad[]
+      EOS
+
+      begin
+        doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
+        assert_equal 0, doc.blocks.size
+      rescue
+        flunk('include macro should not raise exception on missing file')
+      end
+    end
+
+    test 'include macro supports line selection' do
+      input = <<-EOS
+include::fixtures/include-file.asciidoc[lines=1;3..4;6..-1]
+      EOS
+
+      output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :header_footer => false, :attributes => {'docdir' => File.dirname(__FILE__)}
+      assert_match(/first line/, output)
+      assert_no_match(/second line/, output)
+      assert_match(/third line/, output)
+      assert_match(/fourth line/, output)
+      assert_no_match(/fifth line/, output)
+      assert_match(/sixth line/, output)
+      assert_match(/seventh line/, output)
+      assert_match(/eighth line/, output)
+      assert_match(/last line of included content/, output)
+    end
+
+    test 'include macro supports line selection using quoted attribute value' do
+      input = <<-EOS
+include::fixtures/include-file.asciidoc[lines="1, 3..4 , 6 .. -1"]
+      EOS
+
+      output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :header_footer => false, :attributes => {'docdir' => File.dirname(__FILE__)}
+      assert_match(/first line/, output)
+      assert_no_match(/second line/, output)
+      assert_match(/third line/, output)
+      assert_match(/fourth line/, output)
+      assert_no_match(/fifth line/, output)
+      assert_match(/sixth line/, output)
+      assert_match(/seventh line/, output)
+      assert_match(/eighth line/, output)
+      assert_match(/last line of included content/, output)
+    end
+
+    test 'include macro supports tagged selection' do
+      input = <<-EOS
+include::fixtures/include-file.asciidoc[tags=snippetA;snippetB]
+      EOS
+
+      output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :header_footer => false, :attributes => {'docdir' => File.dirname(__FILE__)}
+      assert_match(/snippetA content/, output)
+      assert_match(/snippetB content/, output)
+      assert_no_match(/non-tagged content/, output)
+      assert_no_match(/included content/, output)
+    end
+
+    test 'lines attribute takes precedence over tags attribute in include macro' do
+      input = <<-EOS
+include::fixtures/include-file.asciidoc[lines=1, tags=snippetA;snippetB]
+      EOS
+
+      output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :header_footer => false, :attributes => {'docdir' => File.dirname(__FILE__)}
+      assert_match(/first line of included content/, output)
+      assert_no_match(/snippetA content/, output)
+      assert_no_match(/snippetB content/, output)
+    end
+
+    test 'indent of included file can be reset to size of indent attribute' do
+      input = <<-EOS
+[source, xml]
+----
+include::fixtures/basic-docinfo.xml[lines=2..3, indent=0]
+----
+      EOS
+
+      output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :header_footer => false, :attributes => {'docdir' => File.dirname(__FILE__)}
+      result = xmlnodes_at_xpath('//pre', output, 1).text
+      assert_equal "<year>2013</year>\n<holder>Acme, Inc.</holder>", result
+    end
+
     test "block is called to handle an include macro" do
       input = <<-EOS
 first line
@@ -191,6 +275,39 @@ last line
       assert_match(/^:includefile: include-file.asciidoc$/, lines.join)
     end
 
+    test 'attributes are substituted in target of include macro' do
+      input = <<-EOS
+:fixturesdir: fixtures
+:ext: asciidoc
+
+include::{fixturesdir}/include-file.{ext}[]
+      EOS
+
+      doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
+      output = doc.render
+      assert_match(/included content/, output)
+    end
+
+    test 'line is dropped if target of include macro resolves to empty' do
+      input = <<-EOS
+include::{foodir}/include-file.asciidoc[]
+      EOS
+
+      output = render_embedded_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
+      assert output.strip.empty?
+    end
+
+    test 'line is dropped but not following line if target of include macro resolves to empty' do
+      input = <<-EOS
+include::{foodir}/include-file.asciidoc[]
+yo
+      EOS
+
+      output = render_embedded_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)}
+      assert_xpath '//p', output, 1
+      assert_xpath '//p[text()="yo"]', output, 1
+    end
+
     test 'escaped include macro is left unprocessed' do
       input = <<-EOS
 \\include::include-file.asciidoc[]
@@ -329,6 +446,22 @@ There was much rejoicing.
       assert_equal "On our quest we go...\nThere is a holy grail!\nThere was much rejoicing.", lines.join.strip
     end
 
+    test 'ifndef with defined attribute does not include text in brackets' do
+      input = <<-EOS
+On our quest we go...
+ifndef::hardships[There is a holy grail!]
+There was no rejoicing.
+      EOS
+       
+      doc = Asciidoctor::Document.new [], :attributes => {'hardships' => ''}
+      reader = Asciidoctor::Reader.new(input.lines.entries, doc, true)
+      lines = []
+      while reader.has_more_lines?
+        lines << reader.get_line
+      end
+      assert_equal "On our quest we go...\nThere was no rejoicing.", lines.join.strip
+    end
+
     test 'include with non-matching nested exclude' do
       input = <<-EOS
 ifdef::grail[]
diff --git a/test/sections_test.rb b/test/sections_test.rb
index 414c0b5..2bddafa 100644
--- a/test/sections_test.rb
+++ b/test/sections_test.rb
@@ -208,6 +208,50 @@ text
     end
   end
 
+  context "level 5" do 
+    test "with single line syntax" do
+      assert_xpath "//h6[@id='_my_title'][text() = 'My Title']", render_string(":fragment:\n====== My Title")
+    end
+  end
+
+  context 'Markdown-style headings' do
+    test 'single-line document title with leading marker' do
+      input = <<-EOS
+# Document Title
+      EOS
+      output = render_string input
+      assert_xpath "//h1[not(@id)][text() = 'Document Title']", output, 1
+    end
+
+    test 'single-line document title with symmetric markers' do
+      input = <<-EOS
+# Document Title #
+      EOS
+      output = render_string input
+      assert_xpath "//h1[not(@id)][text() = 'Document Title']", output, 1
+    end
+
+    test 'single-line section title with leading marker' do
+      input = <<-EOS
+## Section One
+
+blah blah
+      EOS
+      output = render_string input
+      assert_xpath "//h2[@id='_section_one'][text() = 'Section One']", output, 1
+    end
+
+    test 'single-line section title with symmetric markers' do
+      input = <<-EOS
+## Section One ##
+
+blah blah
+      EOS
+      output = render_string input
+      assert_xpath "//h2[@id='_section_one'][text() = 'Section One']", output, 1
+    end
+  end
+
   context 'Floating Title' do
     test 'should create floating title if style is float' do
       input = <<-EOS
@@ -257,6 +301,23 @@ not in section
       assert floatingtitle.is_a?(Asciidoctor::Block)
       assert !floatingtitle.is_a?(Asciidoctor::Section)
       assert_equal :floating_title, floatingtitle.context
+      assert_equal '_plain_ol_heading', floatingtitle.id
+      assert doc.references[:ids].has_key?('_plain_ol_heading')
+    end
+
+    test 'can assign explicit id to floating title' do
+      input = <<-EOS
+[[unchained]]
+[float]
+=== Plain Ol' Heading
+
+not in section
+      EOS
+
+      doc = document_from_string input
+      floating_title = doc.blocks.first
+      assert_equal 'unchained', floating_title.id
+      assert doc.references[:ids].has_key?('unchained')
     end
 
     test 'should not include floating title in toc' do
@@ -273,7 +334,7 @@ not in section
 
       output = render_string input
       assert_xpath '//*[@id="toc"]', output, 1
-      assert_xpath %(//*[@id="toc"]//a[contains(text(), " Section ")]), output, 2
+      assert_xpath %(//*[@id="toc"]//a[contains(text(), "Section ")]), output, 2
       assert_xpath %(//*[@id="toc"]//a[text()="Miss Independent"]), output, 0
     end
 
@@ -321,6 +382,114 @@ not in section
     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
+
+      output, errors = nil
+      redirect_streams do |stdout, stderr|
+        output = render_string input
+        errors = stdout.string
+      end
+
+      assert !errors.empty?
+      assert_match(/only book doctypes can contain level 0 sections/, errors)
+    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
+      errors = nil
+      redirect_streams do |stdout, stderr|
+        output = render_string input
+        errors = stdout.string
+      end
+
+      assert errors.empty?
+      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 floating title' do
+      input = <<-EOS
+= Master Document
+Doc Writer
+
+:leveloffset: 1
+
+[float]
+= Floating Title
+      EOS
+
+      output = render_string input
+      assert_xpath '//h2[@class="float"][text() = "Floating Title"]', 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 = render_string input
+      assert_xpath '//*[@class = "sect1"]/h2[text() = "Standalone Document"]', output, 1
+      assert_xpath '//*[@class = "sect1"]/h2[text() = "Level 1 Section"]', output, 1
+    end
+  end
+
   context 'Section Numbering' do
     test 'should create section number with one entry for level 1' do
       sect1 = Asciidoctor::Section.new(nil)
@@ -429,6 +598,48 @@ paragraph
     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 = render_embedded_string 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 link section if sectlinks document attribute is set' do
+      input = <<-EOS
+== Installation
+
+Installation section.
+
+=== Linux
+
+Linux installation instructions.
+      EOS
+
+      output = render_embedded_string 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 and caption to appendix section' do
       input = <<-EOS
@@ -440,7 +651,7 @@ Details
 
       output = block_from_string input
       assert_equal 'appendix', output.sectname
-      assert_equal 'Appendix A: ', output.attr('caption')
+      assert_equal 'Appendix A: ', output.caption
     end
 
     test 'should render appendix title prefixed with caption' do
@@ -560,6 +771,152 @@ Terms
       assert_xpath '//*[@id="toc"]/ol//li/a[text()="Gotchas"]', output, 1
       assert_xpath '//*[@id="toc"]/ol//li/a[text()="Glossary"]', output, 1
     end
+
+    test 'level 0 special sections in multipart book should be rendered as level 1' do
+      input = <<-EOS
+= Multipart Book
+Doc Writer
+:doctype: book
+
+[preface]
+= Preface
+
+Preface text
+
+[appendix]
+= Appendix
+
+Appendix text
+      EOS
+
+      output = render_string input
+      assert_xpath '//h2[@id = "_preface"]', output, 1
+      assert_xpath '//h2[@id = "_appendix"]', output, 1
+    end
+
+    test 'should output docbook elements that coorespond 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
+
+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 = render_embedded_string input, :backend => 'docbook'
+      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 = render_embedded_string input, :backend => 'docbook'
+      assert_xpath '/abstract[@id="abstract_title"]', output, 1
+      assert_xpath '/abstract[@id="abstract_title"]/title[text()="Abstract Title"]', output, 1
+    end
   end
 
   context "heading patterns in blocks" do
@@ -606,7 +963,7 @@ This should be a tip, not a heading.
 ====
       EOS
       output = render_string input
-      assert_xpath "//*[@class='admonitionblock']//p[text() = 'This should be a tip, not a heading.']", output, 1
+      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 labeled list" do
@@ -669,11 +1026,49 @@ fin.
   end
 
   context 'Table of Contents' do
-    test 'should render table of contents if toc attribute is set' do
+    test 'should render unnumbered table of contents in header if toc attribute is set' do
       input = <<-EOS
-Article
-=======
+= 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 = render_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"]/ol', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol[@type="none"][@class="sectlevel1"]', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]//ol', output, 2
+      assert_xpath '//*[@id="header"]//*[@id="toc"]//ol[@type="none"]', output, 2
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li', output, 4
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li[1]/a[@href="#_section_one"][text()="Section One"]', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li/ol', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li/ol[@type="none"]', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li/ol[@class="sectlevel2"]', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li/ol/li', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li/ol/li/a[@href="#_interlude"][text()="Interlude"]', output, 1
+      assert_xpath '((//*[@id="header"]//*[@id="toc"]/ol)[1]/li)[4]/a[@href="#_section_three"][text()="Section Three"]', output, 1
+    end
+
+    test 'should render numbered table of contents in header if toc and numbered attributes are set' do
+      input = <<-EOS
+= Article
 :toc:
+:numbered:
 
 == Section One
 
@@ -692,22 +1087,388 @@ While they were waiting...
 That's all she wrote!
       EOS
       output = render_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"]/ol', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol[@type="none"]', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]//ol', output, 2
+      assert_xpath '//*[@id="header"]//*[@id="toc"]//ol[@type="none"]', output, 2
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li', output, 4
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li/ol/li', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li/ol/li/a[@href="#_interlude"][text()="2.1. Interlude"]', output, 1
+      assert_xpath '((//*[@id="header"]//*[@id="toc"]/ol)[1]/li)[4]/a[@href="#_section_three"][text()="3. Section Three"]', output, 1
+    end
+
+    test 'should render 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 = render_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"]/ol', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol[@type="none"]', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]//ol', output, 2
+      assert_xpath '//*[@id="header"]//*[@id="toc"]//ol[@type="none"]', output, 2
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li', output, 4
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
+      assert_xpath '((//*[@id="header"]//*[@id="toc"]/ol)[1]/li)[4]/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 = render_string input
       assert_xpath '//*[@id="toc"]', output, 1
-      assert_xpath '//*[@id="toc"]/*[@id="toctitle"][text()="Table of Contents"]', output, 1
       assert_xpath '//*[@id="toc"]/ol', output, 1
-      assert_xpath '//*[@id="toc"]//ol', output, 2
-      assert_xpath '//*[@id="toc"]/ol/li', output, 4
-      assert_xpath '//*[@id="toc"]/ol/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
-      assert_xpath '//*[@id="toc"]/ol/li/ol/li', output, 1
-      assert_xpath '//*[@id="toc"]/ol/li/ol/li/a[@href="#_interlude"][text()="2.1. Interlude"]', output, 1
+      assert_xpath '//*[@id="toc"]/ol[@type="none"][@class="sectlevel0"]', output, 1
+      assert_xpath '//*[@id="toc"]/ol[@type="none"][@class="sectlevel0"]/li', output, 4
+      assert_xpath '(//*[@id="toc"]/ol[@type="none"][@class="sectlevel0"]/li)[1]/a[text()="Part 1"]', output, 1
+      assert_xpath '(//*[@id="toc"]/ol[@type="none"][@class="sectlevel0"]/li)[3]/a[text()="Part 2"]', output, 1
+      assert_xpath '(//*[@id="toc"]/ol[@type="none"][@class="sectlevel0"]/li)[2]/ol', output, 1
+      assert_xpath '(//*[@id="toc"]/ol[@type="none"][@class="sectlevel0"]/li)[2]/ol[@type="none"][@class="sectlevel1"]', output, 1
+      assert_xpath '(//*[@id="toc"]/ol[@type="none"][@class="sectlevel0"]/li)[2]/ol/li', output, 2
+      assert_xpath '((//*[@id="toc"]/ol[@type="none"][@class="sectlevel0"]/li)[2]/ol/li)[1]/a[text()="1. First Section of Part 1"]', output, 1
+    end
+
+    test 'should render 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 = render_string input
+      assert_xpath '//body[@class="article toc2"]', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1
+      assert_xpath '//*[@id="header"]//*[@id="toc"]/ol/li[1]/a[@href="#_section_one"][text()="1. Section One"]', 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 = render_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 render table of contents in preamble if toc-placement attribute value is preamble' do
+      input = <<-EOS
+= Article
+:toc:
+:toc-placement: preamble
+
+Once upon a time...
+
+== Section One
+
+It was a dark and stormy night...
+
+== Section Two
+
+They couldn't believe their eyes when...
+      EOS
+
+      output = render_string input
+      assert_xpath '//*[@id="preamble"]/*[@id="toc"]', output, 1
+    end
+
+    test 'should not render 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 = render_string input
+      assert_xpath '//*[@id="toc"]', output, 0
+    end
+
+    test 'should render table of contents at location of toc macro' do
+      input = <<-EOS
+= Article
+:toc:
+:toc-placement!:
+
+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 = render_string input
+      assert_css '#preamble #toc', output, 1
+      assert_css '#preamble .paragraph + #toc', output, 1
+    end
+
+    test 'should render table of contents at location of toc macro in embedded document' do
+      input = <<-EOS
+= Article
+:toc:
+:toc-placement!:
+
+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 = render_string input, :header_footer => false
+      assert_css '#preamble:root #toc', output, 1
+      assert_css '#preamble:root .paragraph + #toc', output, 1
+    end
+
+    test 'should not assign toc id to more than one toc' 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 = render_string input
+
+      assert_css '#toc', output, 1
+      assert_css '#toctitle', output, 1
+      assert_xpath '(//*[@class="toc"])[2][not(@id)]', output, 1
+      assert_xpath '(//*[@class="toc"])[2]/*[@class="title"][not(@id)]', 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!:
+: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 = render_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!:
+: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 = render_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
   end
 
-  context "book doctype" do
-    test "document title with level 0 headings" do
+  context 'article doctype' do
+    test 'should create sections only in docbook backend' do
       input = <<-EOS
-Book
-====
+= 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 = render_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
@@ -728,11 +1489,105 @@ That's all she wrote!
       EOS
 
       output = render_string(input)
-      assert_xpath '//h1', output, 4
-      assert_xpath '//h2', output, 1
+      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, 1
+      assert_css '#content h2', output, 1
       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 create parts and chapters in docbook backend' do
+      input = <<-EOS
+= Book
+Doc Writer
+:doctype: book
+
+= Part 1
+
+The adventure.
+
+== Chapter One
+
+It was a dark and stormy night...
+
+== Chapter Two
+
+They couldn't believe their eyes when...
+
+= Part 2
+
+The return.
+
+== Chapter Three
+
+While they were returning...
+
+== Chapter Four
+
+That's all she wrote!
+      EOS
+
+      output = render_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
+
+[appendix]
+= Appendix
+
+Appendix content
+
+=== Appendix subsection
+
+Appendix subsection content
+      EOS
+
+      output = nil
+      errors = nil
+      redirect_streams do |stdout, stderr|
+        output = render_string input, :backend => 'docbook'
+        errors = stdout.string
+      end
+      assert errors.empty?
+      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
diff --git a/test/substitutions_test.rb b/test/substitutions_test.rb
index 2965acb..e062161 100644
--- a/test/substitutions_test.rb
+++ b/test/substitutions_test.rb
@@ -9,7 +9,7 @@ context 'Substitutions' do
       para = block_from_string("[blue]'http://asciidoc.org[AsciiDoc]' & [red]*Ruby*\n§ Making +++<u>documentation</u>+++ together +\nsince (C) {inception_year}.")
       para.document.attributes['inception_year'] = '2012'
       result = para.apply_normal_subs(para.buffer) 
-      assert_equal %{<em><span class="blue"><a href="http://asciidoc.org">AsciiDoc</a></span></em> & <strong><span class="red">Ruby</span></strong>\n§ Making <u>documentation</u> together<br>\nsince © 2012.}, result
+      assert_equal %{<em class="blue"><a href="http://asciidoc.org">AsciiDoc</a></em> & <strong class="red">Ruby</strong>\n§ Making <u>documentation</u> together<br>\nsince © 2012.}, result
     end
   end
 
@@ -158,7 +158,7 @@ context 'Substitutions' do
     test 'single-line constrained monospaced string' do
       para = block_from_string(%q{`a few <\{monospaced\}> words`})
       # NOTE must use apply_normal_subs because constrained monospaced is handled as a passthrough
-      assert_equal '<tt>a few <{monospaced}> words</tt>', para.apply_normal_subs(para.buffer)
+      assert_equal '<code>a few <{monospaced}> words</code>', para.apply_normal_subs(para.buffer)
     end
 
     test 'escaped single-line constrained monospaced string' do
@@ -170,7 +170,7 @@ context 'Substitutions' do
     test 'multi-line constrained monospaced string' do
       para = block_from_string(%Q{`a few\n<\{monospaced\}> words`})
       # NOTE must use apply_normal_subs because constrained monospaced is handled as a passthrough
-      assert_equal "<tt>a few\n<{monospaced}> words</tt>", para.apply_normal_subs(para.buffer)
+      assert_equal "<code>a few\n<{monospaced}> words</code>", para.apply_normal_subs(para.buffer)
     end
 
     test 'single-line unconstrained strong chars' do
@@ -195,7 +195,7 @@ context 'Substitutions' do
 
     test 'unconstrained strong chars with role' do
       para = block_from_string(%q{Git[blue]**Hub**})
-      assert_equal %q{Git<strong><span class="blue">Hub</span></strong>}, para.sub_quotes(para.buffer.join)
+      assert_equal %q{Git<strong class="blue">Hub</strong>}, para.sub_quotes(para.buffer.join)
     end
 
     # TODO this is not the same result as AsciiDoc, though I don't understand why AsciiDoc gets what it gets
@@ -221,7 +221,7 @@ context 'Substitutions' do
 
     test 'unconstrained emphasis chars with role' do
       para = block_from_string(%q{[gray]__Git__Hub})
-      assert_equal %q{<em><span class="gray">Git</span></em>Hub}, para.sub_quotes(para.buffer.join)
+      assert_equal %q{<em class="gray">Git</em>Hub}, para.sub_quotes(para.buffer.join)
     end
 
     test 'escaped unconstrained emphasis chars with role' do
@@ -231,17 +231,17 @@ context 'Substitutions' do
 
     test 'single-line unconstrained monospaced chars' do
       para = block_from_string(%q{Git++Hub++})
-      assert_equal 'Git<tt>Hub</tt>', para.sub_quotes(para.buffer.join)
+      assert_equal 'Git<code>Hub</code>', para.sub_quotes(para.buffer.join)
     end
 
     test 'escaped single-line unconstrained monospaced chars' do
       para = block_from_string(%q{Git\++Hub++})
-      assert_equal 'Git+<tt>Hub</tt>+', para.sub_quotes(para.buffer.join)
+      assert_equal 'Git+<code>Hub</code>+', para.sub_quotes(para.buffer.join)
     end
 
     test 'multi-line unconstrained monospaced chars' do
       para = block_from_string(%Q{Git++\nH\nu\nb++})
-      assert_equal "Git<tt>\nH\nu\nb</tt>", para.sub_quotes(para.buffer.join)
+      assert_equal "Git<code>\nH\nu\nb</code>", para.sub_quotes(para.buffer.join)
     end
 
     test 'single-line superscript chars' do
@@ -255,8 +255,8 @@ context 'Substitutions' do
     end
 
     test 'multi-line superscript chars' do
-      para = block_from_string(%Q{x^(n\n+\n1)^})
-      assert_equal "x<sup>(n\n+\n1)</sup>", para.sub_quotes(para.buffer.join)
+      para = block_from_string(%Q{x^(n\n-\n1)^})
+      assert_equal "x<sup>(n\n-\n1)</sup>", para.sub_quotes(para.buffer.join)
     end
 
     test 'single-line subscript chars' do
@@ -286,6 +286,42 @@ context 'Substitutions' do
       assert_equal %q{<a href="/home.html">Home</a>}, para.sub_macros(para.buffer.join)
     end
 
+    test 'a mailto macro should be interpreted as a mailto link' do
+      para = block_from_string('mailto:doc.writer at asciidoc.org[]')
+      assert_equal %q{<a href="mailto:doc.writer at asciidoc.org">doc.writer at asciidoc.org</a>}, para.sub_macros(para.buffer.join)
+    end
+
+    test 'a mailto macro with text should be interpreted as a mailto link' do
+      para = block_from_string('mailto:doc.writer at asciidoc.org[Doc Writer]')
+      assert_equal %q{<a href="mailto:doc.writer at asciidoc.org">Doc Writer</a>}, para.sub_macros(para.buffer.join)
+    end
+
+    test 'a mailto macro with text and subject should be interpreted as a mailto link' do
+      para = block_from_string('mailto:doc.writer at asciidoc.org[Doc Writer, Pull request]', :attributes => {'linkattrs' => ''})
+      assert_equal %q{<a href="mailto:doc.writer at asciidoc.org?subject=Pull%20request">Doc Writer</a>}, para.sub_macros(para.buffer.join)
+    end
+
+    test 'a mailto macro with text, subject and body should be interpreted as a mailto link' do
+      para = block_from_string('mailto:doc.writer at asciidoc.org[Doc Writer, Pull request, Please accept my pull request]', :attributes => {'linkattrs' => ''})
+      assert_equal %q{<a href="mailto:doc.writer at asciidoc.org?subject=Pull%20request&body=Please%20accept%20my%20pull%20request">Doc Writer</a>}, para.sub_macros(para.buffer.join)
+    end
+
+    test 'should recognize inline email addresses' do
+      para = block_from_string('doc.writer at asciidoc.org')
+      assert_equal %q{<a href="mailto:doc.writer at asciidoc.org">doc.writer at asciidoc.org</a>}, para.sub_macros(para.buffer.join)
+      para = block_from_string('<doc.writer at asciidoc.org>')
+      assert_equal %q{<<a href="mailto:doc.writer at asciidoc.org">doc.writer at asciidoc.org</a>>}, para.apply_normal_subs(para.buffer)
+      para = block_from_string('author+website at 4fs.no')
+      assert_equal %q{<a href="mailto:author+website at 4fs.no">author+website at 4fs.no</a>}, para.sub_macros(para.buffer.join)
+      para = block_from_string('john at domain.uk.co')
+      assert_equal %q{<a href="mailto:john at domain.uk.co">john at domain.uk.co</a>}, para.sub_macros(para.buffer.join)
+    end
+
+    test 'should ignore escaped inline email address' do
+      para = block_from_string('\doc.writer at asciidoc.org')
+      assert_equal %q{doc.writer at asciidoc.org}, para.sub_macros(para.buffer.join)
+    end
+
     test 'a single-line raw url should be interpreted as a link' do
       para = block_from_string('http://google.com')
       assert_equal %q{<a href="http://google.com">http://google.com</a>}, para.sub_macros(para.buffer.join)
@@ -312,6 +348,11 @@ context 'Substitutions' do
       assert_equal %q{http://google.com}, para.sub_macros(para.buffer.join)
     end
 
+    test 'a comma separated list of links should not include commas in links' do
+      para = block_from_string('http://foo.com, http://bar.com, http://example.org')
+      assert_equal %q{<a href="http://foo.com">http://foo.com</a>, <a href="http://bar.com">http://bar.com</a>, <a href="http://example.org">http://example.org</a>}, para.sub_macros(para.buffer.join)
+    end
+
     test 'a single-line image macro should be interpreted as an image' do
       para = block_from_string('image:tiger.png[]')
       assert_equal %{<span class="image"><img src="tiger.png" alt="tiger"></span>}, para.sub_macros(para.buffer.join).gsub(/>\s+</, '><')
@@ -322,6 +363,11 @@ context 'Substitutions' do
       assert_equal %{<span class="image"><img src="tiger.png" alt="Tiger"></span>}, para.sub_macros(para.buffer.join).gsub(/>\s+</, '><')
     end
 
+    test 'a single-line image macro with text containing escaped square bracket should be interpreted as an image with alt text' do
+      para = block_from_string('image:tiger.png[[Another\] Tiger]')
+      assert_equal %{<span class="image"><img src="tiger.png" alt="[Another] Tiger"></span>}, para.sub_macros(para.buffer.join).gsub(/>\s+</, '><')
+    end
+
     test 'a single-line image macro with text and dimensions should be interpreted as an image with alt text and dimensions' do
       para = block_from_string('image:tiger.png[Tiger, 200, 100]')
       assert_equal %{<span class="image"><img src="tiger.png" alt="Tiger" width="200" height="100"></span>},
@@ -334,6 +380,19 @@ context 'Substitutions' do
           para.sub_macros(para.buffer.join).gsub(/>\s+</, '><')
     end
 
+    test 'a multi-line image macro with text and dimensions should be interpreted as an image with alt text and dimensions' do
+      para = block_from_string(%(image:tiger.png[Another\nAwesome\nTiger, 200,\n100]))
+      assert_equal %{<span class="image"><img src="tiger.png" alt="Another Awesome Tiger" width="200" height="100"></span>},
+          para.sub_macros(para.buffer.join).gsub(/>\s+</, '><')
+    end
+
+    test 'a block image macro should not be detected within paragraph text' do
+      para = block_from_string(%(Not an inline image macro image::tiger.png[].))
+      result = para.sub_macros(para.buffer.join)
+      assert !result.include?('<img ')
+      assert result.include?('image::tiger.png[]')
+    end
+
     test 'a single-line footnote macro should be registered and rendered as a footnote' do
       para = block_from_string('Sentence text footnote:[An example footnote.].')
       assert_equal %(Sentence text <span class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnote_1" title="View footnote.">1</a>]</span>.), para.sub_macros(para.buffer.join)
@@ -555,13 +614,129 @@ context 'Substitutions' do
       assert_equal ['Big cats', 'Tigers'], terms[0]
       assert_equal ['panthera tigris'], terms[1]
     end
+
+    context 'Button macro' do
+      test 'btn macro' do
+        para = block_from_string('btn:[Save]', :attributes => {'experimental' => ''})
+        assert_equal %q{<b class="button">Save</b>}, para.sub_macros(para.buffer.join)
+      end
+
+      test 'btn macro for docbook backend' do
+        para = block_from_string('btn:[Save]', :backend => 'docbook', :attributes => {'experimental' => ''})
+        assert_equal %q{<guibutton>Save</guibutton>}, para.sub_macros(para.buffer.join)
+      end
+    end
+
+    context 'Keyboard macro' do
+      test 'kbd macro with single key' do
+        para = block_from_string('kbd:[F3]', :attributes => {'experimental' => ''})
+        assert_equal %q{<kbd>F3</kbd>}, para.sub_macros(para.buffer.join)
+      end
+  
+      test 'kbd macro with single key, docbook backend' do
+        para = block_from_string('kbd:[F3]', :backend => 'docbook', :attributes => {'experimental' => ''})
+        assert_equal %q{<keycap>F3</keycap>}, para.sub_macros(para.buffer.join)
+      end
+  
+      test 'kbd macro with key combination' do
+        para = block_from_string('kbd:[Ctrl+Shift+T]', :attributes => {'experimental' => ''})
+        assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></kbd>}, para.sub_macros(para.buffer.join)
+      end
+  
+      test 'kbd macro with key combination with spaces' do
+        para = block_from_string('kbd:[Ctrl + Shift + T]', :attributes => {'experimental' => ''})
+        assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></kbd>}, para.sub_macros(para.buffer.join)
+      end
+  
+      test 'kbd macro with key combination delimited by commas' do
+        para = block_from_string('kbd:[Ctrl,Shift,T]', :attributes => {'experimental' => ''})
+        assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></kbd>}, para.sub_macros(para.buffer.join)
+      end
+  
+      test 'kbd macro with key combination containing a plus key no spaces' do
+        para = block_from_string('kbd:[Ctrl++]', :attributes => {'experimental' => ''})
+        assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>+</kbd></kbd>}, para.sub_macros(para.buffer.join)
+      end
+  
+      test 'kbd macro with key combination delimited by commands containing a comma key' do
+        para = block_from_string('kbd:[Ctrl,,]', :attributes => {'experimental' => ''})
+        assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>,</kbd></kbd>}, para.sub_macros(para.buffer.join)
+      end
+  
+      test 'kbd macro with key combination containing a plus key with spaces' do
+        para = block_from_string('kbd:[Ctrl + +]', :attributes => {'experimental' => ''})
+        assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>+</kbd></kbd>}, para.sub_macros(para.buffer.join)
+      end
+  
+      test 'kbd macro with key combination containing escaped bracket' do
+        para = block_from_string('kbd:[Ctrl + \]]', :attributes => {'experimental' => ''})
+        assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>]</kbd></kbd>}, para.sub_macros(para.buffer.join)
+      end
+  
+      test 'kbd macro with key combination, docbook backend' do
+        para = block_from_string('kbd:[Ctrl+Shift+T]', :backend => 'docbook', :attributes => {'experimental' => ''})
+        assert_equal %q{<keycombo><keycap>Ctrl</keycap><keycap>Shift</keycap><keycap>T</keycap></keycombo>}, para.sub_macros(para.buffer.join)
+      end
+    end
+
+    context 'Menu macro' do
+      test 'should process menu using macro sytnax' do
+        para = block_from_string('menu:File[]', :attributes => {'experimental' => ''})
+        assert_equal %q{<span class="menu">File</span>}, para.sub_macros(para.buffer.join)
+      end
+
+      test 'should process menu for docbook backend' do
+        para = block_from_string('menu:File[]', :backend => 'docbook', :attributes => {'experimental' => ''})
+        assert_equal %q{<guimenu>File</guimenu>}, para.sub_macros(para.buffer.join)
+      end
+
+      test 'should process menu with menu item using macro syntax' do
+        para = block_from_string('menu:File[Save As…]', :attributes => {'experimental' => ''})
+        assert_equal %q{<span class="menuseq"><span class="menu">File</span> ▸ <span class="menuitem">Save As…</span></span>}, para.sub_macros(para.buffer.join)
+      end
+
+      test 'should process menu with menu item for docbook backend' do
+        para = block_from_string('menu:File[Save As…]', :backend => 'docbook', :attributes => {'experimental' => ''})
+        assert_equal %q{<menuchoice><guimenu>File</guimenu> <guimenuitem>Save As…</guimenuitem></menuchoice>}, para.sub_macros(para.buffer.join)
+      end
+
+      test 'should process menu with menu item in submenu using macro syntax' do
+        para = block_from_string('menu:Tools[Project > Build]', :attributes => {'experimental' => ''})
+        assert_equal %q{<span class="menuseq"><span class="menu">Tools</span> ▸ <span class="submenu">Project</span> ▸ <span class="menuitem">Build</span></span>}, para.sub_macros(para.buffer.join)
+      end
+
+      test 'should process menu with menu item in submenu for docbook backend' do
+        para = block_from_string('menu:Tools[Project > Build]', :backend => 'docbook', :attributes => {'experimental' => ''})
+        assert_equal %q{<menuchoice><guimenu>Tools</guimenu> <guisubmenu>Project</guisubmenu> <guimenuitem>Build</guimenuitem></menuchoice>}, para.sub_macros(para.buffer.join)
+      end
+
+      test 'should process menu with menu item in submenu using macro syntax and comma delimiter' do
+        para = block_from_string('menu:Tools[Project, Build]', :attributes => {'experimental' => ''})
+        assert_equal %q{<span class="menuseq"><span class="menu">Tools</span> ▸ <span class="submenu">Project</span> ▸ <span class="menuitem">Build</span></span>}, para.sub_macros(para.buffer.join)
+      end
+
+      test 'should process menu with menu item using inline syntax' do
+        para = block_from_string('"File > Save As…"', :attributes => {'experimental' => ''})
+        assert_equal %q{<span class="menuseq"><span class="menu">File</span> ▸ <span class="menuitem">Save As…</span></span>}, para.sub_macros(para.buffer.join)
+      end
+
+      test 'should process menu with menu item in submenu using inline syntax' do
+        para = block_from_string('"Tools > Project > Build"', :attributes => {'experimental' => ''})
+        assert_equal %q{<span class="menuseq"><span class="menu">Tools</span> ▸ <span class="submenu">Project</span> ▸ <span class="menuitem">Build</span></span>}, para.sub_macros(para.buffer.join)
+      end
+
+      test 'inline syntax should not closing quote of XML attribute' do
+        para = block_from_string('<span class="xmltag"><node></span><span class="classname">r</span>', :attributes => {'experimental' => ''})
+        assert_equal %q{<span class="xmltag"><node></span><span class="classname">r</span>}, para.sub_macros(para.buffer.join)
+      end
+    end
   end
 
   context 'Passthroughs' do
     test 'collect inline triple plus passthroughs' do
       para = block_from_string('+++<code>inline code</code>+++')
       result = para.extract_passthroughs(para.buffer.join)
-      assert_equal "\x0" + '0' + "\x0", result
+      assert_equal "\e" + '0' + "\e", result
       assert_equal 1, para.passthroughs.size
       assert_equal '<code>inline code</code>', para.passthroughs.first[:text]
       assert para.passthroughs.first[:subs].empty?
@@ -570,7 +745,7 @@ context 'Substitutions' do
     test 'collect multi-line inline triple plus passthroughs' do
       para = block_from_string("+++<code>inline\ncode</code>+++")
       result = para.extract_passthroughs(para.buffer.join)
-      assert_equal "\x0" + '0' + "\x0", result
+      assert_equal "\e" + '0' + "\e", result
       assert_equal 1, para.passthroughs.size
       assert_equal "<code>inline\ncode</code>", para.passthroughs.first[:text]
       assert para.passthroughs.first[:subs].empty?
@@ -579,7 +754,7 @@ context 'Substitutions' do
     test 'collect inline double dollar passthroughs' do
       para = block_from_string('$$<code>{code}</code>$$')
       result = para.extract_passthroughs(para.buffer.join)
-      assert_equal "\x0" + '0' + "\x0", result
+      assert_equal "\e" + '0' + "\e", result
       assert_equal 1, para.passthroughs.size
       assert_equal '<code>{code}</code>', para.passthroughs.first[:text]
       assert_equal [:specialcharacters], para.passthroughs.first[:subs]
@@ -588,7 +763,7 @@ context 'Substitutions' do
     test 'collect multi-line inline double dollar passthroughs' do
       para = block_from_string("$$<code>\n{code}\n</code>$$")
       result = para.extract_passthroughs(para.buffer.join)
-      assert_equal "\x0" + '0' + "\x0", result
+      assert_equal "\e" + '0' + "\e", result
       assert_equal 1, para.passthroughs.size
       assert_equal "<code>\n{code}\n</code>", para.passthroughs.first[:text]
       assert_equal [:specialcharacters], para.passthroughs.first[:subs]
@@ -597,7 +772,7 @@ context 'Substitutions' do
     test 'collect passthroughs from inline pass macro' do
       para = block_from_string(%Q{pass:specialcharacters,quotes[<code>['code'\\]</code>]})
       result = para.extract_passthroughs(para.buffer.join)
-      assert_equal "\x0" + '0' + "\x0", result
+      assert_equal "\e" + '0' + "\e", result
       assert_equal 1, para.passthroughs.size
       assert_equal %q{<code>['code']</code>}, para.passthroughs.first[:text]
       assert_equal [:specialcharacters, :quotes], para.passthroughs.first[:subs]
@@ -606,7 +781,7 @@ context 'Substitutions' do
     test 'collect multi-line passthroughs from inline pass macro' do
       para = block_from_string(%Q{pass:specialcharacters,quotes[<code>['more\ncode'\\]</code>]})
       result = para.extract_passthroughs(para.buffer.join)
-      assert_equal "\x0" + '0' + "\x0", result
+      assert_equal "\e" + '0' + "\e", result
       assert_equal 1, para.passthroughs.size
       assert_equal %Q{<code>['more\ncode']</code>}, para.passthroughs.first[:text]
       assert_equal [:specialcharacters, :quotes], para.passthroughs.first[:subs]
@@ -614,19 +789,71 @@ context 'Substitutions' do
 
     # NOTE placeholder is surrounded by text to prevent reader from stripping trailing boundary char (unique to test scenario)
     test 'restore inline passthroughs without subs' do
-      para = block_from_string("some \x0" + '0' + "\x0 to study")
+      para = block_from_string("some \e" + '0' + "\e to study")
       para.passthroughs << {:text => '<code>inline code</code>', :subs => []}
       result = para.restore_passthroughs(para.buffer.join)
       assert_equal "some <code>inline code</code> to study", result
     end
 
     # NOTE placeholder is surrounded by text to prevent reader from stripping trailing boundary char (unique to test scenario)
-    # TODO add two entries to ensure index lookup is working correctly (0 indx could be ambiguous)
     test 'restore inline passthroughs with subs' do
-      para = block_from_string("some \x0" + '0' + "\x0 to study")
+      para = block_from_string("some \e" + '0' + "\e to study in the \e" + '1' + "\e programming language")
       para.passthroughs << {:text => '<code>{code}</code>', :subs => [:specialcharacters]}
+      para.passthroughs << {:text => '{language}', :subs => [:specialcharacters]}
       result = para.restore_passthroughs(para.buffer.join)
-      assert_equal 'some <code>{code}</code> to study', result
+      assert_equal 'some <code>{code}</code> to study in the {language} programming language', result
+    end
+
+    test 'complex inline passthrough macro' do
+      text_to_escape = %q{[(] <'basic form'> <'logical operator'> <'basic form'> [)]}
+      para = block_from_string %($$#{text_to_escape}$$) 
+      result = para.extract_passthroughs(para.buffer.join)
+      assert_equal 1, para.passthroughs.size
+      assert_equal text_to_escape, para.passthroughs[0][:text]
+
+      text_to_escape_escaped = %q{[(\] <'basic form'> <'logical operator'> <'basic form'> [)\]}
+      para = block_from_string %(pass:specialcharacters[#{text_to_escape_escaped}])
+      result = para.extract_passthroughs(para.buffer.join)
+      assert_equal 1, para.passthroughs.size
+      assert_equal text_to_escape, para.passthroughs[0][:text]
+    end
+  end
+
+  context 'Replacements' do
+    test 'unescapes XML entities' do
+      para = block_from_string '< " " &#x22; >'
+      assert_equal '< " " &#x22; >', para.apply_normal_subs(para.buffer)
+    end
+
+    test 'replaces arrows' do
+      para = block_from_string '<- -> <= => \<- \-> \<= \=>'
+      assert_equal '← → ⇐ ⇒ <- -> <= =>', para.apply_normal_subs(para.buffer.join)
+    end
+
+    test 'replaces dashes' do
+      para = block_from_string %(-- foo foo--bar foo\\--bar foo -- bar foo \\-- bar
+stuff in between
+-- foo
+stuff in between
+foo --
+stuff in between
+foo --)
+      expected = %( — foo foo—bar foo--bar foo — bar foo -- bar
+stuff in between — foo
+stuff in between
+foo — stuff in between
+foo — )
+      assert_equal expected, para.sub_replacements(para.buffer.join)
+    end
+
+    test 'replaces marks' do
+      para = block_from_string '(C) (R) (TM) \(C) \(R) \(TM)' 
+      assert_equal '© ® ™ (C) (R) (TM)', para.sub_replacements(para.buffer.join)
+    end
+
+    test 'replaces punctuation' do
+      para = block_from_string %(John's Hideout... foo\\'bar)
+      assert_equal "John’s Hideout… foo'bar", para.sub_replacements(para.buffer.join)
     end
   end
 
diff --git a/test/tables_test.rb b/test/tables_test.rb
index 5ccb92e..02eabfd 100644
--- a/test/tables_test.rb
+++ b/test/tables_test.rb
@@ -14,15 +14,15 @@ context 'Tables' do
       cells = [%w(A B C), %w(a b c), %w(1 2 3)]
       output = render_embedded_string input
       assert_css 'table', output, 1
-      assert_css 'table.tableblock.frame-all.grid-all[style*="width: 100%"]', output, 1
-      assert_css 'table > colgroup > col[style*="width: 33%"]', output, 3
+      assert_css 'table.tableblock.frame-all.grid-all[style*="width:100%"]', output, 1
+      assert_css 'table > colgroup > col[style*="width:33%"]', output, 3
       assert_css 'table tr', output, 3
       assert_css 'table > tbody > tr', output, 3
       assert_css 'table td', output, 9
       assert_css 'table > tbody > tr > td.tableblock.halign-left.valign-top > p.tableblock', output, 9
       cells.each_with_index {|row, rowi|
-        assert_css "table tr:nth-child(#{rowi + 1}) > td", output, row.size
-        assert_css "table tr:nth-child(#{rowi + 1}) > td > p", output, row.size
+        assert_css "table > tbody > tr:nth-child(#{rowi + 1}) > td", output, row.size
+        assert_css "table > tbody > tr:nth-child(#{rowi + 1}) > td > p", output, row.size
         row.each_with_index {|cell, celli|
           assert_xpath "(//tr)[#{rowi + 1}]/td[#{celli + 1}]/p[text()='#{cell}']", output, 1
         }
@@ -43,6 +43,46 @@ context 'Tables' do
       assert_xpath '/table/caption/following-sibling::colgroup', output, 1
     end
 
+    test 'only increments table counter for tables that have a title' do
+      input = <<-EOS
+.First numbered table
+|=======
+|1 |2 |3
+|=======
+
+|=======
+|4 |5 |6
+|=======
+
+.Second numbered table
+|=======
+|7 |8 |9
+|=======
+      EOS
+      output = render_embedded_string input
+      assert_css 'table:root', output, 3
+      assert_xpath '(/table)[1]/caption', output, 1
+      assert_xpath '(/table)[1]/caption[text()="Table 1. First numbered table"]', output, 1
+      assert_xpath '(/table)[2]/caption', output, 0
+      assert_xpath '(/table)[3]/caption', output, 1
+      assert_xpath '(/table)[3]/caption[text()="Table 2. Second numbered table"]', output, 1
+    end
+
+    test 'renders explicit caption on simple psv table' do
+      input = <<-EOS
+[caption="All the Data. "]
+.Simple psv table
+|=======
+|A |B |C
+|a |b |c
+|1 |2 |3
+|=======
+      EOS
+      output = render_embedded_string input
+      assert_xpath '/table/caption[@class="title"][text()="All the Data. Simple psv table"]', output, 1
+      assert_xpath '/table/caption/following-sibling::colgroup', output, 1
+    end
+
     test 'ignores escaped separators' do
       input = <<-EOS
 |===
@@ -233,13 +273,13 @@ I am getting in shape!
       EOS
       output = render_embedded_string input
       assert_css 'table', output, 1
-      assert_css 'table[style*="width: 80%"]', output, 1
+      assert_css 'table[style*="width:80%"]', output, 1
       assert_xpath '/table/caption[@class="title"][text()="Table 1. Horizontal and vertical source data"]', output, 1
       assert_css 'table > colgroup > col', output, 4
-      assert_css 'table > colgroup > col:nth-child(1)[@style*="width: 17%"]', output, 1
-      assert_css 'table > colgroup > col:nth-child(2)[@style*="width: 11%"]', output, 1
-      assert_css 'table > colgroup > col:nth-child(3)[@style*="width: 11%"]', output, 1
-      assert_css 'table > colgroup > col:nth-child(4)[@style*="width: 58%"]', output, 1
+      assert_css 'table > colgroup > col:nth-child(1)[@style*="width:17%"]', output, 1
+      assert_css 'table > colgroup > col:nth-child(2)[@style*="width:11%"]', output, 1
+      assert_css 'table > colgroup > col:nth-child(3)[@style*="width:11%"]', output, 1
+      assert_css 'table > colgroup > col:nth-child(4)[@style*="width:58%"]', output, 1
       assert_css 'table > thead', output, 1
       assert_css 'table > thead > tr', output, 1
       assert_css 'table > thead > tr > th', output, 4
@@ -264,7 +304,7 @@ d|9 2+>|10
       EOS
       output = render_embedded_string input
       assert_css 'table', output, 1
-      assert_css 'table > colgroup > col[style*="width: 25%"]', output, 4
+      assert_css 'table > colgroup > col[style*="width:25%"]', output, 4
       assert_css 'table > tbody > tr', output, 4
       assert_css 'table > tbody > tr > td', output, 10
       assert_css 'table > tbody > tr:nth-child(1) > td', output, 4
@@ -272,21 +312,21 @@ d|9 2+>|10
       assert_css 'table > tbody > tr:nth-child(3) > td', output, 1
       assert_css 'table > tbody > tr:nth-child(4) > td', output, 2
       
-      assert_css 'table tr:nth-child(1) > td:nth-child(1).halign-left.valign-top p em', output, 1
-      assert_css 'table tr:nth-child(1) > td:nth-child(2).halign-right.valign-top p strong', output, 1
-      assert_css 'table tr:nth-child(1) > td:nth-child(3).halign-center.valign-top p', output, 1
-      assert_css 'table tr:nth-child(1) > td:nth-child(3).halign-center.valign-top p *', output, 0
-      assert_css 'table tr:nth-child(1) > td:nth-child(4).halign-right.valign-top p strong', output, 1
+      assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(1).halign-left.valign-top p em', output, 1
+      assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(2).halign-right.valign-top p strong', output, 1
+      assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(3).halign-center.valign-top p', output, 1
+      assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(3).halign-center.valign-top p *', output, 0
+      assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(4).halign-right.valign-top p strong', output, 1
 
-      assert_css 'table tr:nth-child(2) > td:nth-child(1).halign-center.valign-top p em', output, 1
-      assert_css 'table tr:nth-child(2) > td:nth-child(2).halign-center.valign-middle[colspan="2"][rowspan="2"] p tt', output, 1
-      assert_css 'table tr:nth-child(2) > td:nth-child(3).halign-left.valign-bottom[rowspan="3"] p tt', output, 1
+      assert_css 'table > tbody > tr:nth-child(2) > td:nth-child(1).halign-center.valign-top p em', output, 1
+      assert_css 'table > tbody > tr:nth-child(2) > td:nth-child(2).halign-center.valign-middle[colspan="2"][rowspan="2"] p code', output, 1
+      assert_css 'table > tbody > tr:nth-child(2) > td:nth-child(3).halign-left.valign-bottom[rowspan="3"] p code', output, 1
 
-      assert_css 'table tr:nth-child(3) > td:nth-child(1).halign-center.valign-top p em', output, 1
+      assert_css 'table > tbody > tr:nth-child(3) > td:nth-child(1).halign-center.valign-top p em', output, 1
 
-      assert_css 'table tr:nth-child(4) > td:nth-child(1).halign-left.valign-top p', output, 1
-      assert_css 'table tr:nth-child(4) > td:nth-child(1).halign-left.valign-top p em', output, 0
-      assert_css 'table tr:nth-child(4) > td:nth-child(2).halign-right.valign-top[colspan="2"] p tt', output, 1
+      assert_css 'table > tbody > tr:nth-child(4) > td:nth-child(1).halign-left.valign-top p', output, 1
+      assert_css 'table > tbody > tr:nth-child(4) > td:nth-child(1).halign-left.valign-top p em', output, 0
+      assert_css 'table > tbody > tr:nth-child(4) > td:nth-child(2).halign-right.valign-top[colspan="2"] p code', output, 1
     end
 
     test 'supports repeating cells' do
@@ -434,6 +474,43 @@ output file name is used.
       assert_css 'table > tbody > tr > td:nth-child(2) table', output, 1
       assert_css 'table > tbody > tr > td:nth-child(2) table > tbody > tr > td', output, 2
     end
+
+    test 'nested document in AsciiDoc cell should not see doctitle of parent' do
+      input = <<-EOS
+= Document Title
+
+[cols="1a"]
+|===
+|AsciiDoc content
+|===
+      EOS
+
+      output = render_string input
+      assert_css 'table', output, 1
+      assert_css 'table > tbody > tr > td', output, 1
+      assert_css 'table > tbody > tr > td #preamble', output, 0
+      assert_css 'table > tbody > tr > td .paragraph', output, 1
+    end
+
+    test 'cell background color' do
+      input = <<-EOS
+[cols="1e,1", options="header"]
+|===
+|{set:cellbgcolor:green}green
+|{set:cellbgcolor!}
+plain
+|{set:cellbgcolor:red}red
+|{set:cellbgcolor!}
+plain
+|===
+      EOS
+
+      output = render_embedded_string input
+      assert_xpath '(/table/thead/tr/th)[1][@style="background-color:green;"]', output, 1
+      assert_xpath '(/table/thead/tr/th)[2][@style="background-color:green;"]', output, 0
+      assert_xpath '(/table/tbody/tr/td)[1][@style="background-color:red;"]', output, 1
+      assert_xpath '(/table/tbody/tr/td)[2][@style="background-color:green;"]', output, 0
+    end
   end
 
   context 'DSV' do
@@ -452,11 +529,26 @@ nobody:x:99:99:Nobody:/:/sbin/nologin
       EOS
       output = render_embedded_string input
       assert_css 'table', output, 1
-      assert_css 'table > colgroup > col[style*="width: 14%"]', output, 7
+      assert_css 'table > colgroup > col[style*="width:14%"]', output, 7
       assert_css 'table > tbody > tr', output, 6
       assert_xpath '//tr[4]/td[5]/p/text()', output, 0
       assert_xpath '//tr[3]/td[5]/p[text()="MySQL:Server"]', output, 1
     end
+
+    test 'dsv format shorthand' do
+      input = <<-EOS
+:===
+a:b:c
+1:2:3
+:===
+      EOS
+      output = render_embedded_string input
+      assert_css 'table', output, 1
+      assert_css 'table > colgroup > col', output, 3
+      assert_css 'table > tbody > tr', output, 2
+      assert_css 'table > tbody > tr:nth-child(1) > td', output, 3
+      assert_css 'table > tbody > tr:nth-child(2) > td', output, 3
+    end
   end
 
   context 'CSV' do
@@ -475,7 +567,7 @@ air, moon roof, loaded",4799.00
       EOS
       output = render_embedded_string input 
       assert_css 'table', output, 1
-      assert_css 'table > colgroup > col[style*="width: 20%"]', output, 5
+      assert_css 'table > colgroup > col[style*="width:20%"]', output, 5
       assert_css 'table > thead > tr', output, 1
       assert_css 'table > tbody > tr', output, 4
       assert_xpath '((//tbody/tr)[1]/td)[4]/p[text()="ac, abs, moon"]', output, 1
@@ -483,6 +575,21 @@ air, moon roof, loaded",4799.00
       assert_xpath '((//tbody/tr)[4]/td)[4]/p[text()="MUST SELL! air, moon roof, loaded"]', output, 1
     end
 
+    test 'csv format shorthand' do
+      input = <<-EOS
+,===
+a,b,c
+1,2,3
+,===
+      EOS
+      output = render_embedded_string input
+      assert_css 'table', output, 1
+      assert_css 'table > colgroup > col', output, 3
+      assert_css 'table > tbody > tr', output, 2
+      assert_css 'table > tbody > tr:nth-child(1) > td', output, 3
+      assert_css 'table > tbody > tr:nth-child(2) > td', output, 3
+    end
+
     test 'custom separator' do
       input = <<-EOS
 [format="csv", separator=";"]
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 9e5f63b..e670076 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -4,12 +4,6 @@ require 'test/unit'
 
 require "#{File.expand_path(File.dirname(__FILE__))}/../lib/asciidoctor.rb"
 
-begin
-  require 'mocha/setup'
-rescue LoadError
-  require 'mocha'
-end
-require 'htmlentities'
 require 'nokogiri'
 require 'pending'
 
@@ -67,8 +61,8 @@ class Test::Unit::TestCase
     xmlnodes_at_path(:css, css, content)
   end
 
-  def xmlnodes_at_xpath(css, content, count = nil)
-    xmlnodes_at_path(:xpath, css, content)
+  def xmlnodes_at_xpath(xpath, content, count = nil)
+    xmlnodes_at_path(:xpath, xpath, content)
   end
 
   def xmlnodes_at_path(type, path, content, count = nil)
@@ -82,6 +76,11 @@ class Test::Unit::TestCase
     count == 1 ? results.first : results
   end
 
+  # Generate an xpath attribute matcher that matches a name in the class attribute
+  def contains_class(name)
+    %(contains(concat(' ', normalize-space(@class), ' '), ' #{name} '))
+  end
+
   def assert_css(css, content, count = nil)
     assert_path(:css, css, content, count)
   end
@@ -100,7 +99,13 @@ class Test::Unit::TestCase
 
     results = xmlnodes_at_path type, path, content
 
-    if (count && results.length != count)
+    if (count == true || count == false)
+      if (count != results)
+        flunk "#{type_name} #{path} yielded #{results} rather than #{count} for:\n#{content}"
+      else
+        assert true
+      end
+    elsif (count && results.length != count)
       flunk "#{type_name} #{path} yielded #{results.length} elements rather than #{count} for:\n#{content}"
     elsif (count.nil? && results.empty?)
       flunk "#{type_name} #{path} not found in:\n#{content}"
@@ -145,6 +150,12 @@ class Test::Unit::TestCase
     [Asciidoctor::Lexer.parse_header_metadata(reader), reader]
   end
 
+  # Expand the character for an entity such as — so
+  # it can be used to match in an XPath expression
+  def expand_entity(number)
+    [number].pack('U*')
+  end
+
   def invoke_cli_to_buffer(argv = [], filename = 'sample.asciidoc', &block)
     invoke_cli(argv, filename, [StringIO.new, StringIO.new], &block)
   end
diff --git a/test/text_test.rb b/test/text_test.rb
index daa87ea..a77e01b 100644
--- a/test/text_test.rb
+++ b/test/text_test.rb
@@ -1,27 +1,28 @@
+# encoding: UTF-8
 require 'test_helper'
 
 context "Text" do
   test "proper encoding to handle utf8 characters in document using html backend" do
     output = example_document(:encoding).render
-    assert_xpath '//p', output, 2
+    assert_xpath '//p', output, 4
     assert_xpath '//a', output, 1
   end
 
   test "proper encoding to handle utf8 characters in embedded document using html backend" do
     output = example_document(:encoding, :header_footer => false).render
-    assert_xpath '//p', output, 2
+    assert_xpath '//p', output, 4
     assert_xpath '//a', output, 1
   end
 
   test "proper encoding to handle utf8 characters in document using docbook backend" do
     output = example_document(:encoding, :attributes => {'backend' => 'docbook'}).render
-    assert_xpath '//simpara', output, 2
+    assert_xpath '//simpara', output, 4
     assert_xpath '//ulink', output, 1
   end
 
   test "proper encoding to handle utf8 characters in embedded document using docbook backend" do
     output = example_document(:encoding, :header_footer => false, :attributes => {'backend' => 'docbook'}).render
-    assert_xpath '//simpara', output, 2
+    assert_xpath '//simpara', output, 4
     assert_xpath '//ulink', output, 1
   end
 
@@ -31,11 +32,22 @@ context "Text" do
     input << "[verse]\n"
     input.concat(File.readlines(sample_doc_path(:encoding)))
     doc = Asciidoctor::Document.new
-    reader = Asciidoctor::Reader.new input
+    reader = Asciidoctor::Reader.new(input, doc, true)
     block = Asciidoctor::Lexer.next_block(reader, doc)
     assert_xpath '//pre', block.render.gsub(/^\s*\n/, ''), 1
   end
 
+  test "proper encoding to handle utf8 characters from included file" do
+    input = <<-EOS
+include::fixtures/encoding.asciidoc[tags=romé]
+    EOS
+    doc = Asciidoctor::Document.new [], :safe => Asciidoctor::SafeMode::SAFE, :base_dir => File.expand_path(File.dirname(__FILE__))
+    reader = Asciidoctor::Reader.new(input, doc, true)
+    block = Asciidoctor::Lexer.next_block(reader, doc)
+    output = block.render
+    assert_css '.paragraph', output, 1
+  end
+
   test 'escaped text markup' do
     assert_match(/All your <em>inline<\/em> markup belongs to <strong>us<\/strong>!/,
         render_string('All your <em>inline</em> markup belongs to <strong>us</strong>!'))
@@ -89,7 +101,7 @@ context "Text" do
   end
 
   test "backtick-escaped text followed by single-quoted text" do
-    assert_match(/<tt>foo<\/tt>/, render_string(%Q(run `foo` 'dog')))
+    assert_match(/<code>foo<\/code>/, render_string(%Q(run `foo` 'dog')))
   end
 
   context "basic styling" do
@@ -106,7 +118,7 @@ context "Text" do
     end
 
     test "monospaced" do
-      assert_xpath "//tt", @rendered
+      assert_xpath "//code", @rendered
     end
 
     test "superscript" do
@@ -118,21 +130,21 @@ context "Text" do
     end
 
     test "backticks" do
-      assert_xpath "//tt", render_string("This is `totally cool`.")
+      assert_xpath "//code", render_string("This is `totally cool`.")
     end
 
     test "nested styles" do
       rendered = render_string("Winning *big _time_* in the +city *boyeeee*+.")
 
       assert_xpath "//strong/em", rendered
-      assert_xpath "//tt/strong", rendered
+      assert_xpath "//code/strong", rendered
     end
 
     test "unconstrained quotes" do
       rendered_chars = render_string("**B**__I__++M++")
       assert_xpath "//strong", rendered_chars
       assert_xpath "//em", rendered_chars
-      assert_xpath "//tt", rendered_chars
+      assert_xpath "//code", rendered_chars
     end
   end
 end

-- 
ruby-asciidoctor.git



More information about the Pkg-ruby-extras-commits mailing list