Repository: redpen-cc/redpen
Pull request #514 (2015-11-19 14:05:10)

Replace AsciiDoctor/AsciiDoctorJ parser with simpler lightweight AsciiDoc parser
This change replaces the existing AsciiDoc parser with a simpler AsciiDoc parser with no external dependencies.

This new parser generates a model of the source document which retains each character's original position, then proceeds to 'erase' the elements of the model that are recognized as AsciiDoc markup. The remaining characters are then converted into a RedPen document model.

The pom has been updated to remove asciidoctor/asciidoctorj and jruby related dependencies.



Patch:
diff --git a/redpen-core/pom.xml b/redpen-core/pom.xml
index 5b79df8c..8cd88275 100644
--- a/redpen-core/pom.xml
+++ b/redpen-core/pom.xml
@@ -182,17 +182,6 @@
             <artifactId>pegdown</artifactId>
             <version>1.4.2</version>
         </dependency>
-        <dependency>
-            <groupId>org.asciidoctor</groupId>
-            <artifactId>asciidoctorj</artifactId>
-            <version>1.5.2</version>
-        </dependency>
-        <dependency>
-            <!--artifact is same as that used by asciidoctor-->
-            <groupId>org.jruby</groupId>
-            <artifactId>jruby-complete</artifactId>
-            <version>1.7.16.1</version>
-        </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
diff --git a/redpen-core/src/main/java/cc/redpen/parser/AsciiDocParser.java b/redpen-core/src/main/java/cc/redpen/parser/AsciiDocParser.java
deleted file mode 100644
index 8b10f51d..00000000
--- a/redpen-core/src/main/java/cc/redpen/parser/AsciiDocParser.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/**
- * redpen: a text inspection tool
- * Copyright (c) 2014-2015 Recruit Technologies Co., Ltd. and contributors
- * (see CONTRIBUTORS.md)
- * <p>
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * <p>
- * http://www.apache.org/licenses/LICENSE-2.0
- * <p>
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package cc.redpen.parser;
-
-import cc.redpen.RedPenException;
-import cc.redpen.model.Document;
-import cc.redpen.tokenizer.RedPenTokenizer;
-import org.asciidoctor.Asciidoctor;
-import org.asciidoctor.RedPenTreeProcessor;
-import org.asciidoctor.internal.IOUtils;
-import org.pegdown.ParsingTimeoutException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
-
-/**
- * Parser for AsciiDoc format, utilizing AsciiDoctorJ.<br>
- * <p>
- * AsciiDoc's syntax and grammar is documented at @see http://asciidoc.org/
- */
-public class AsciiDocParser extends BaseDocumentParser {
-    private static final Logger LOG = LoggerFactory.getLogger(AsciiDocParser.class);
-
-    private static String ASCIIDOCTOR_EOF = "\n----\nEOF\n----\n";
-
-    @Override
-    public Document parse(InputStream io, Optional<String> fileName, SentenceExtractor sentenceExtractor, RedPenTokenizer tokenizer) throws RedPenException {
-        Document.DocumentBuilder documentBuilder = new Document.DocumentBuilder(tokenizer);
-        fileName.ifPresent(documentBuilder::setFileName);
-        BufferedReader reader = createReader(io);
-        try {
-            // create an asciidoctor instance
-            Asciidoctor asciidoctor = Asciidoctor.Factory.create();
-
-            // register our 'redpen' backend
-            InputStream rubySource = new ByteArrayInputStream(AsciiDoctorRedPenRubySource.SOURCE_TEXT.getBytes("UTF-8"));
-            asciidoctor.rubyExtensionRegistry().loadClass(rubySource);
-
-            // set our 'redpen' backend as the active AsciiDoctor backend
-            Map<String, Object> options = new HashMap<>();
-            options.put("backend", "redpen");
-
-            // tell AsciiDoctor to record the source line numbers
-            options.put("sourcemap", true);
-            Map<String, Object> attributes = new HashMap<>();
-            attributes.put("sourcemap", true);
-            options.put("attributes", attributes);
-
-            // register our documentbuilding TreeProcessor
-            asciidoctor.javaExtensionRegistry().treeprocessor(new RedPenTreeProcessor(documentBuilder, sentenceExtractor, options));
-
-            // we need to add an EOF marker to asciidoctor's input text
-            // so that it correctly calculates the line number of the last line
-            String documentText = IOUtils.readFull(reader) + ASCIIDOCTOR_EOF;
-            try {
-                // trigger the tree processor, which will consequently fill the documentBuilder
-                asciidoctor.readDocumentStructure(documentText, options);
-            } catch (Exception e) {
-                LOG.info("Asciidoctor parser error: " + e.getMessage());
-            }
-
-        } catch (ParsingTimeoutException e) {
-            throw new RedPenException("Failed to parse timeout: ", e);
-        } catch (Exception e) {
-            throw new RedPenException("Exception when configuring AsciiDoctor", e);
-        }
-
-        return documentBuilder.build();
-    }
-}
-
-/**
- * The Ruby source for the AsciiDoctor RedPen backend
- */
-class AsciiDoctorRedPenRubySource {
-    public static String SOURCE_TEXT = "# encoding: UTF-8\n" +
-            "module Asciidoctor\n" +
-            "class Converter::RedPen < Converter::BuiltIn\n" +
-
-            "def initialize backend, opts = {}\n" +
-            "end\n" +
-
-            "def redpen_output node, opts = {}\n" +
-            "  content = defined?(node.content) ? node.content : (defined?(node.text) ? node.text : '')\n" +
-            "  location=''\n" +
-
-            // add the line number to the text, if known, bracketed by ^A and ^B
-            "  if defined?(node.source_location) && (node.source_location != nil)\n" +
-            "    location = \"\\001#{node.source_location.lineno}\\002\"\n" +
-            "  end\n" +
-            // return the location, and the converted content bracketed by ^C and ^D
-            "  \"#{location}\\003#{content}\\004\"\n" +
-            "end\n" +
-
-            "alias paragraph redpen_output\n" +
-            "alias document redpen_output\n" +
-            "alias embedded redpen_output\n" +
-            "alias outline redpen_output\n" +
-            "alias section redpen_output\n" +
-            "alias admonition redpen_output\n" +
-            "alias audio redpen_output\n" +
-            "alias colist redpen_output\n" +
-            "alias example redpen_output\n" +
-            "alias floating_title redpen_output\n" +
-            "alias image redpen_output\n" +
-            "alias listing redpen_output\n" +
-            "alias literal redpen_output\n" +
-            "alias stem redpen_output\n" +
-            "alias open redpen_output\n" +
-            "alias page_break redpen_output\n" +
-            "alias preamble redpen_output\n" +
-            "alias quote redpen_output\n" +
-            "alias thematic_break redpen_output\n" +
-            "alias sidebar redpen_output\n" +
-            "alias table redpen_output\n" +
-            "alias toc redpen_output\n" +
-            "alias dlist redpen_output\n" +
-            "alias olist redpen_output\n" +
-            "alias ulist redpen_output\n" +
-            "alias verse redpen_output\n" +
-            "alias video redpen_output\n" +
-            "alias inline_anchor redpen_output\n" +
-            "alias inline_break redpen_output\n" +
-            "alias inline_button redpen_output\n" +
-            "alias inline_callout redpen_output\n" +
-            "alias inline_footnote redpen_output\n" +
-            "alias inline_image redpen_output\n" +
-            "alias inline_indexterm redpen_output\n" +
-            "alias inline_kbd redpen_output\n" +
-            "alias inline_menu redpen_output\n" +
-            "alias inline_quoted redpen_output\n" +
-            "end\n" +
-
-            // monkey patch the parser to remember the header source lines
-            // this change adds each header source line, and its line number,
-            // to document-level attributes we can retreive later
-            "class AbstractNode\n" +
-            "  attr_accessor :source_text\n" +
-            "end\n" +
-
-            "class Parser\n" +
-            "  def self.parse_section_title(reader, document)\n" +
-            "    line1 = reader.read_line\n" +
-            "    line1_lineno = reader.cursor.lineno\n" +
-            "    sect_id = nil\n" +
-            "    sect_title = nil\n" +
-            "    sect_level = -1\n" +
-            "    sect_reftext = nil\n" +
-            "    single_line = true\n" +
-            "\n" +
-            "    first_char = line1.chr\n" +
-            "    if (first_char == '=' || (Compliance.markdown_syntax && first_char == '#')) &&\n" +
-            "        (match = AtxSectionRx.match(line1))\n" +
-            "      sect_level = single_line_section_level match[1]\n" +
-            "      sect_title = match[2]\n" +
-            "      if sect_title.end_with?(']]') && (anchor_match = InlineSectionAnchorRx.match(sect_title))\n" +
-            "        if anchor_match[2].nil?\n" +
-            "          sect_title = anchor_match[1]\n" +
-            "          sect_id = anchor_match[3]\n" +
-            "          sect_reftext = anchor_match[4]\n" +
-            "        end\n" +
-            "      end\n" +
-            "    elsif Compliance.underline_style_section_titles\n" +
-            "      if (line2 = reader.peek_line(true)) && SECTION_LEVELS.has_key?(line2.chr) && line2 =~ SetextSectionLineRx &&\n" +
-            "        (name_match = SetextSectionTitleRx.match(line1)) &&\n" +
-            "        # chomp so that a (non-visible) endline does not impact calculation\n" +
-            "        (line_length(line1) - line_length(line2)).abs <= 1\n" +
-            "        sect_title = name_match[1]\n" +
-            "        if sect_title.end_with?(']]') && (anchor_match = InlineSectionAnchorRx.match(sect_title))\n" +
-            "          if anchor_match[2].nil?\n" +
-            "            sect_title = anchor_match[1]\n" +
-            "            sect_id = anchor_match[3]\n" +
-            "            sect_reftext = anchor_match[4]\n" +
-            "          end\n" +
-            "        end\n" +
-            "        sect_level = section_level line2\n" +
-            "        single_line = false\n" +
-            "        reader.advance\n" +
-            "      end\n" +
-            "    end\n" +
-            "    if sect_level >= 0\n" +
-            "      sect_level += document.attr('leveloffset', 0).to_i\n" +
-            "    end\n" +
-            "    document.attributes['header_lines_source'] ||= []\n" +
-            "    document.attributes['header_lines_source'].push(line1)\n" +
-            "    document.attributes['header_lines_linenos'] ||= []\n" +
-            "    document.attributes['header_lines_linenos'].push(line1_lineno)\n" +
-            "    [sect_id, sect_reftext, sect_title, sect_level, single_line]\n" +
-            "  end\n\n" +
-            // the list item reader did not remember the source location or source text
-            " def self.next_list_item(reader, list_block, match, sibling_trait = nil)\n" +
-            "    if (list_type = list_block.context) == :dlist\n" +
-            "      list_term = ListItem.new(list_block, match[1])\n" +
-            "      list_item = ListItem.new(list_block, match[3])\n" +
-            "      has_text = !match[3].nil_or_empty?\n" +
-            "    else\n" +
-            "      # Create list item using first line as the text of the list item\n" +
-            "      text = match[2]\n" +
-            "      checkbox = false\n" +
-            "      if list_type == :ulist && text.start_with?('[')\n" +
-            "        if text.start_with?('[ ] ')\n" +
-            "          checkbox = true\n" +
-            "          checked = false\n" +
-            "          text = text[3..-1].lstrip\n" +
-            "        elsif text.start_with?('[x] ') || text.start_with?('[*] ')\n" +
-            "          checkbox = true\n" +
-            "          checked = true\n" +
-            "          text = text[3..-1].lstrip\n" +
-            "        end\n" +
-            "      end\n" +
-            "      list_item = ListItem.new(list_block, text)\n" +
-            "      if checkbox\n" +
-            "        # FIXME checklist never makes it into the options attribute\n" +
-            "        list_block.attributes['checklist-option'] = ''\n" +
-            "        list_item.attributes['checkbox'] = ''\n" +
-            "        list_item.attributes['checked'] = '' if checked\n" +
-            "      end\n" +
-            "      sibling_trait ||= resolve_list_marker(list_type, match[1], list_block.items.size, true, reader)\n" +
-            "      list_item.marker = sibling_trait\n" +
-            "      has_text = true\n" +
-            "    end\n" +
-            "    list_item.source_location = reader.cursor\n" +
-            "    list_item.source_text = match\n" +
-            "    # first skip the line with the marker / term\n" +
-            "    reader.advance\n" +
-            "    cursor = reader.cursor\n" +
-            "    list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), cursor\n" +
-            "    if list_item_reader.has_more_lines?\n" +
-            "      comment_lines = list_item_reader.skip_line_comments\n" +
-            "      subsequent_line = list_item_reader.peek_line\n" +
-            "      list_item_reader.unshift_lines comment_lines unless comment_lines.empty? \n" +
-            "      if !subsequent_line.nil?\n" +
-            "        continuation_connects_first_block = subsequent_line.empty?\n" +
-            "        # if there's no continuation connecting the first block, then\n" +
-            "        # treat the lines as paragraph text (activated when has_text = false)\n" +
-            "        if !continuation_connects_first_block && list_type != :dlist\n" +
-            "          has_text = false\n" +
-            "        end\n" +
-            "        content_adjacent = !continuation_connects_first_block && !subsequent_line.empty?\n" +
-            "      else\n" +
-            "        continuation_connects_first_block = false\n" +
-            "        content_adjacent = false\n" +
-            "      end\n" +
-            "      # only relevant for :dlist\n" +
-            "      options = {:text => !has_text}\n" +
-            "      # we can look for blocks until there are no more lines (and not worry\n" +
-            "      # about sections) since the reader is confined within the boundaries of a\n" +
-            "      # list\n" +
-            "      while list_item_reader.has_more_lines?\n" +
-            "        new_block = next_block(list_item_reader, list_block, {}, options)\n" +
-            "        list_item << new_block if new_block\n" +
-            "      end\n" +
-            "      list_item.fold_first(continuation_connects_first_block, content_adjacent)\n" +
-            "    end\n" +
-            "    if list_type == :dlist\n" +
-            "      unless list_item.text? || list_item.blocks?\n" +
-            "        list_item = nil\n" +
-            "      end\n" +
-            "      [list_term, list_item]\n" +
-            "    else\n" +
-            "      list_item\n" +
-            "    end\n" +
-            "  end\n" +
-            "end\n" +
-            "\n" +
-
-            "Asciidoctor::Converter::Factory.register Asciidoctor::Converter::RedPen, [\"redpen\"]\n" +
-
-            // need to prevent AsciiDoctor's substitutors from replacing quotes with fancy-quotes, and (c) with the &copy; etc
-            "Substitutors::SUBS[:basic]=[:specialcharacters]\n" +
-            "Substitutors::SUBS[:normal]=[:specialcharacters, :quotes, :attributes,  :macros, :post_replacements]\n" +
-            "Substitutors::SUBS[:verbatim]=[:specialcharacters, :callouts]\n" +
-            "Substitutors::SUBS[:title]=[:specialcharacters, :quotes, :macros, :attributes, :post_replacements]\n" +
-            "Substitutors::SUBS[:header]=[:specialcharacters, :attributes]\n" +
-            "end\n";
-}
\ No newline at end of file
diff --git a/redpen-core/src/main/java/cc/redpen/parser/DocumentParser.java b/redpen-core/src/main/java/cc/redpen/parser/DocumentParser.java
index 1cea19ac..3fbc90c6 100644
--- a/redpen-core/src/main/java/cc/redpen/parser/DocumentParser.java
+++ b/redpen-core/src/main/java/cc/redpen/parser/DocumentParser.java
@@ -19,6 +19,7 @@ package cc.redpen.parser;
 
 import cc.redpen.RedPenException;
 import cc.redpen.model.Document;
+import cc.redpen.parser.asciidoc.AsciiDocParser;
 import cc.redpen.tokenizer.RedPenTokenizer;
 
 import java.io.File;
diff --git a/redpen-core/src/main/java/cc/redpen/parser/asciidoc/AsciiDocParser.java b/redpen-core/src/main/java/cc/redpen/parser/asciidoc/AsciiDocParser.java
new file mode 100644
index 00000000..3aa10a23
--- /dev/null
+++ b/redpen-core/src/main/java/cc/redpen/parser/asciidoc/AsciiDocParser.java
@@ -0,0 +1,554 @@
+/**
+ * redpen: a text inspection tool
+ * Copyright (c) 2014-2015 Recruit Technologies Co., Ltd. and contributors
+ * (see CONTRIBUTORS.md)
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cc.redpen.parser.asciidoc;
+
+import cc.redpen.RedPenException;
+import cc.redpen.model.Document;
+import cc.redpen.parser.BaseDocumentParser;
+import cc.redpen.parser.SentenceExtractor;
+import cc.redpen.tokenizer.RedPenTokenizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+
+/**
+ * Erasing parser for the AsciiDoc format<br/>
+ * <p>
+ * One of the requirements for RedPen is that the line & column position (ie: offset) for
+ * each character in the parsed text be preserved throughout parsing and validation.
+ * <p>
+ * This AsciiDoc parser attempts to solve this requirement by maintaining a model of the source
+ * document's characters and their original position, and then logically 'erasing' the parts of
+ * that model - usually the markup - that should not be presented to RedPen for validation.
+ * <p>
+ * The remaining "un-erased" text is transformed into RedPen's document model.
+ * <p>
+ * AsciiDoc's syntax and grammar is documented at @see http://asciidoc.org/
+ */
+public class AsciiDocParser extends BaseDocumentParser {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsciiDocParser.class);
+
+    /**
+     * An array of AsciiDoctor macros to erase
+     */
+    private static final String[] MACROS = {
+            "ifdef::",
+            "ifndef::",
+            "ifeval::",
+            "endif::",
+    };
+
+    /**
+     * An array of AsciiDoc admonitions to erase
+     */
+    private static final String[] ADMONITIONS = {
+            "NOTE: ",
+            "TIP: ",
+            "IMPORTANT: ",
+            "CAUTION: ",
+            "WARNING: "
+    };
+
+    /**
+     * An array of AsciiDoc external link prefixes
+     */
+    private static final String[] EXTERNAL_LINK_PREFIXES = {
+            "link:",
+            "http://",
+            "https://",
+            "image:",
+            "include:"
+    };
+
+    /**
+     * current parser state
+     */
+    private class State {
+        // are we in a block
+        public boolean inBlock = false;
+        // are we in a list?
+        public boolean inList = false;
+        // should we erase lines within the current block?
+        public boolean eraseBlock = true;
+        // the sort of block we are in
+        public char blockMarker = 0;
+        // length of the lead block marker
+        public int blockMarkerLength = 0;
+    }
+
+
+    @Override
+    public Document parse(InputStream inputStream, Optional<String> fileName, SentenceExtractor sentenceExtractor, RedPenTokenizer tokenizer) throws RedPenException {
+        Document.DocumentBuilder documentBuilder = new Document.DocumentBuilder(tokenizer);
+        fileName.ifPresent(documentBuilder::setFileName);
+
+        Model model = new Model(sentenceExtractor);
+
+        // add the lines from the input to the model
+        populateModel(model, inputStream);
+
+        // convert the model to a redpen document
+        convertModel(model, documentBuilder);
+
+        return documentBuilder.build();
+    }
+
+    /**
+     * populate the erasable model with the text from the inputstream
+     */
+    protected void populateModel(Model model, InputStream io) {
+        State state = new State();
+        BufferedReader reader = createReader(io);
+
+        int lineno = 0;
+        try {
+            // add the lines to the model
+            while (true) {
+                String line = reader.readLine();
+                if (line == null) {
+                    break;
+                }
+                lineno++;
+                model.add(new Line(line, lineno));
+            }
+            reader.close();
+
+            // preocess each line of the model
+            for (model.rewind(); model.isMore(); model.getNextLine()) {
+                processLine(model.getCurrentLine(), model, state);
+            }
+
+            processHeader(model);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            LOG.error("Exception when parsing AsciiDoc file", e);
+        }
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("AsciiDoc parser model (X=erased line,[=block,section-listlevel-lineno,*=list item):\n" + model.toString());
+        }
+    }
+
+    /**
+     * Conver the parser's model to the RedPen document model
+     *
+     * @param model
+     * @param builder
+     */
+    protected void convertModel(Model model, Document.DocumentBuilder builder) {
+        model.rewind();
+
+        // add a header if there isn't one in the model
+        if ((model.getCurrentLine() != null) && (model.getCurrentLine().getSectionLevel() == 0)) {
+            builder.addSection(0);
+        }
+
+        while (model.isMore()) {
+
+            // skip blank lines
+            while (model.isMore() && model.getCurrentLine().isEmpty()) {
+                model.getNextLine();
+            }
+
+            if (model.isMore()) {
+                // check for new sections
+                if (model.getCurrentLine().getSectionLevel() > 0) {
+                    builder.addSection(
+                            model.getCurrentLine().getSectionLevel(),
+                            model.convertToSentences(model.getCurrentLine())
+                    );
+                    model.getNextLine();
+                }
+                // check for a list item
+                else if (model.getCurrentLine().isListStart()) {
+                    List<Line> listElementLines = new ArrayList<>();
+                    int listLevel = model.getCurrentLine().getListLevel();
+
+                    // add the list start line
+                    listElementLines.add(model.getCurrentLine());
+
+                    // test the following lines to see if they continue this list item
+                    model.getNextLine();
+                    while (model.isMore() &&
+                            !model.getCurrentLine().isListStart() &&
+                            (model.getCurrentLine().getListLevel() == listLevel)) {
+                        listElementLines.add(model.getCurrentLine());
+                        model.getNextLine();
+                    }
+                    builder.addListElement(listLevel, model.convertToSentences(listElementLines));
+                }
+                // process a paragraph
+                else {
+                    List<Line> paragraphLines = new ArrayList<>();
+                    // current line can't be empty, so this loop will enter at least once
+                    while (model.isMore() && !model.getCurrentLine().isEmpty()) {
+                        paragraphLines.add(model.getCurrentLine());
+                        model.getNextLine();
+                    }
+                    builder.addParagraph();
+                    model.convertToSentences(paragraphLines).forEach(builder::addSentence);
+                }
+            }
+        }
+    }
+
+    /**
+     * Does the give line start a list?
+     *
+     * @param line
+     * @param nextLine the subsequent line
+     * @return
+     */
+    private boolean isListElement(Line line, Line nextLine) {
+
+        int pos = 0;
+        while (Character.isWhitespace(line.charAt(pos))) {
+            pos++;
+        }
+
+        // is the first non-space character a suitable list marker?
+        if (".-*".indexOf(line.charAt(pos)) != -1) {
+            char listMarker = line.charAt(pos);
+            int level = 1;
+            pos++;
+            // count the list marker's size
+            while (line.charAt(pos) == listMarker) {
+                pos++;
+                level++;
+            }
+            // we need a whitespace
+            if (Character.isWhitespace(line.charAt(pos))) {
+                // remember the list level
+                line.setListLevel(level);
+                line.setListStart(true);
+                // remove the list markup
+                line.erase(0, pos);
+                // remove whitespace
+                while (Character.isWhitespace(line.charAt(pos))) {
+                    line.erase(pos, 1);
+                    pos++;
+                }
+                return true;
+            }
+        }
+
+        // test for labelled lists
+        if ((line.charAt(line.length() - 1) == ':') &&
+                (line.charAt(line.length() - 2) == ':')) {
+            int level = 1;
+            pos = line.length() - 3;
+            while ((pos > 0) && line.charAt(pos) == ':') {
+                pos--;
+                level++;
+            }
+            nextLine.setListLevel(level);
+            nextLine.setListStart(true);
+            line.erase();
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Process the header, erasing the annotations that can follow it
+     *
+     * @param model
+     */
+    private void processHeader(Model model) {
+        // look for "= Text of header" or "Text of header\n==========="
+        if (model.lineCount() > 1) {
+            boolean haveHeader = false;
+            if ((model.getLine(1).charAt(0, true) == '=') &&
+                    (model.getLine(1).charAt(1, true) == ' ')) {
+                haveHeader = true;
+            }
+            else if ((model.getLine(1).length() == model.getLine(2).length()) &&
+                    model.getLine(2).isAllSameCharacter() &&
+                    (model.getLine(2).charAt(0, true) == '=')) {
+                haveHeader = true;
+            }
+
+            if (haveHeader) {
+                model.getLine(1).setSectionLevel(1);
+                // erase lines up until an empty line
+                for (int i = 2; i <= model.lineCount(); i++) {
+                    if (!model.getLine(i).isErased() && model.getLine(i).isEmpty()) {
+                        break;
+                    }
+                    model.getLine(i).erase();
+                }
+            }
+        }
+    }
+
+    /**
+     * Process the current line, removing asciidoc tags and markup and setting the current state
+     *
+     * @param line
+     * @param state the current state
+     */
+    private void processLine(Line line, Model model, State state) {
+
+        if (!line.isErased()) {
+
+            Line previousLine = model.getLine(line.getLineNo() - 1);
+            Line nextLine = model.getLine(line.getLineNo() + 1);
+
+            if (state.inList && (line.getListLevel() == 0)) {
+                line.setListLevel(previousLine.getListLevel());
+            }
+
+            char firstChar = line.charAt(0);
+            char secondChar = line.charAt(1);
+
+            // check for block end
+            if (state.inBlock) {
+                if (line.isAllSameCharacter() &&
+                        (firstChar == state.blockMarker) &&
+                        (line.length() == state.blockMarkerLength)) {
+                    // end a regular block
+                    line.erase();
+                    line.setInBlock(true);
+                    state.inBlock = false;
+                    return;
+                }
+                else if ((line.length() >= 4) &&
+                        (firstChar == state.blockMarker) &&
+                        (firstChar == '|') &&
+                        (secondChar == '=')) {
+                    // end a table
+                    line.erase();
+                    line.setInBlock(true);
+                    state.inBlock = false;
+                    return;
+                }
+                // erase the block content
+                line.setInBlock(true);
+                if (state.eraseBlock) {
+                    line.erase();
+                    return;
+                }
+            }
+
+            // check for old style heading (line followed by single-char-line of same length)
+            if (line.isAllSameCharacter() &&
+                    (line.length() == previousLine.length()) &&
+                    ("=-~^+".indexOf(firstChar) != -1) &&
+                    (". [".indexOf(previousLine.charAt(0, true)) == -1)) {
+                previousLine.setSectionLevel(1);
+                line.erase();
+                return;
+            }
+
+            // horizontal rule
+            if (line.isAllSameCharacter() && (line.length() == 3) && (line.charAt(0) == '\'')) {
+                line.erase();
+                return;
+            }
+
+            // block markers in which we process the internal text
+            if (line.isAllSameCharacter() && (line.length() >= 4) &&
+                    ("_*".indexOf(firstChar) != -1)) {
+                line.erase();
+                return;
+            }
+
+            // test for various block starts
+            if (!state.inBlock) {
+
+                // fenced block
+                if (line.isAllSameCharacter() && (line.length() == 3) && (line.charAt(0) == '`')) {
+                    state.inBlock = true;
+                    state.eraseBlock = true;
+                    state.blockMarker = firstChar;
+                    state.blockMarkerLength = line.length();
+                    line.setInBlock(true);
+                    line.erase();
+                    return;
+                }
+
+                // see if we are starting other types of blocks
+                if (line.isAllSameCharacter() && (line.length() >= 4)) {
+                    switch (firstChar) {
+                        case '-':
+                        case '=':
+                        case '&':
+                        case '/':
+                        case '+':
+                        case '.':
+                            // blocks that have their innards erased
+                            state.inBlock = true;
+                            state.eraseBlock = true;
+                            state.blockMarker = firstChar;
+                            state.blockMarkerLength = line.length();
+                            line.setInBlock(true);
+                            line.erase();
+                            return;
+                    }
+                }
+
+                // see if this is a table marker
+                if ((line.length() >= 4) && (firstChar == '|') && (secondChar == '=')) {
+                    line.erase();
+                    state.inBlock = true;
+                    state.eraseBlock = true;
+                    state.blockMarker = '|';
+                    state.blockMarkerLength = 1;
+                    line.setInBlock(true);
+                    return;
+                }
+
+            }
+
+            // check for a single-line literal sentence
+            if (!state.inList && (firstChar == ' ')) {
+                line.erase();
+                return;
+            }
+
+            // maybe we have a comment?
+            if ((firstChar == '/') && (secondChar == '/')) {
+                line.erase();
+                return;
+            }
+
+            // 'open' block marker
+            if ((firstChar == '-') && (secondChar == '-')) {
+                line.erase();
+                return;
+            }
+
+            // attributes (at position == 0)
+            if (line.eraseEnclosure(":", ":", Line.EraseStyle.None) == 0) {
+                line.erase();
+                return;
+            }
+            if (line.eraseEnclosure("[", "]", Line.EraseStyle.None) == 0) {
+                line.erase();
+                return;
+            }
+
+            // check for a title
+            if ((firstChar == '.') && (" .".indexOf(secondChar) == -1)) {
+                line.erase(0, 1);
+            }
+
+            // erase urls and links
+            for (String prefix : EXTERNAL_LINK_PREFIXES) {
+                line.eraseEnclosure(prefix, " ,[", Line.EraseStyle.CloseMarkerContainsDelimiters);
+            }
+
+            // enclosed directives
+            line.eraseEnclosure("+++", "+++", Line.EraseStyle.All);
+            line.eraseEnclosure("[[", "]]", Line.EraseStyle.All);
+
+            line.eraseEnclosure("<<", ">>", Line.EraseStyle.PreserveLabel);
+
+            line.eraseEnclosure("{", "}", Line.EraseStyle.Markers); // NOTE: should we make substitutions?
+            line.eraseEnclosure("[", "]", Line.EraseStyle.Markers);
+
+            // headers
+            int headerIndent = 0;
+            while (line.charAt(headerIndent) == '=') {
+                headerIndent++;
+            }
+            if ((headerIndent > 0) && (line.charAt(headerIndent) == ' ')) {
+                line.erase(0, headerIndent + 1);
+                line.setSectionLevel(headerIndent);
+            }
+
+            // lists!
+            if (!state.inBlock && isListElement(line, nextLine)) {
+                state.inList = true;
+            }
+
+            // continuation markers
+            if (line.charAt(line.length() - 1) == '+') {
+                if (line.length() == 0) {
+                    line.erase();
+                }
+                else {
+                    line.erase(line.length() - 1, 1);
+                }
+            }
+
+            // a blank line will cancel any list element we are in
+            if (state.inList && (line.length() == 0)) {
+                state.inList = false;
+                line.setListLevel(0);
+            }
+
+            // macros
+            for (String macro : MACROS) {
+                if (line.startsWith(macro)) {
+                    line.erase();
+                    break;
+                }
+            }
+
+            // admonitions
+            for (String admonition : ADMONITIONS) {
+                if (line.startsWith(admonition)) {
+                    line.erase(0, admonition.length());
+                    break;
+                }
+            }
+
+            eraseInlineMarkup(line);
+        }
+    }
+
+
+    /**
+     * Erase all inline markup (bold, italics, special tokens etc)
+     *
+     * @param line
+     */
+    private void eraseInlineMarkup(Line line) {
+        // inline markup (bold, italics etc)
+        line.eraseEnclosure("__", "__", Line.EraseStyle.Markers);
+        line.eraseEnclosure("**", "**", Line.EraseStyle.Markers);
+        line.eraseEnclosure("``", "``", Line.EraseStyle.Markers);
+        line.eraseEnclosure("##", "##", Line.EraseStyle.Markers);
+        line.eraseEnclosure("^", "^", Line.EraseStyle.Markers);
+        line.eraseEnclosure("~", "~", Line.EraseStyle.Markers);
+
+        line.eraseEnclosure("_", "_", Line.EraseStyle.InlineMarkup);
+        line.eraseEnclosure("*", "*", Line.EraseStyle.InlineMarkup);
+        line.eraseEnclosure("`", "`", Line.EraseStyle.InlineMarkup);
+        line.eraseEnclosure("#", "#", Line.EraseStyle.InlineMarkup);
+
+        line.erase("'`");
+        line.erase("`'");
+        line.erase("\"`");
+        line.erase("`\"");
+        line.erase("(C)");
+        line.erase("(R)");
+        line.erase("(TM)");
+    }
+}
+
diff --git a/redpen-core/src/main/java/cc/redpen/parser/asciidoc/Line.java b/redpen-core/src/main/java/cc/redpen/parser/asciidoc/Line.java
new file mode 100644
index 00000000..fa63495d
--- /dev/null
+++ b/redpen-core/src/main/java/cc/redpen/parser/asciidoc/Line.java
@@ -0,0 +1,478 @@
+/**
+ * redpen: a text inspection tool
+ * Copyright (c) 2014-2015 Recruit Technologies Co., Ltd. and contributors
+ * (see CONTRIBUTORS.md)
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cc.redpen.parser.asciidoc;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * An 'erasing' string utility class that stores the original offset for each preserved character
+ */
+public class Line {
+    // value returned for comparison if a character is escaped
+    static final char ESCAPED_CHARACTER_VALUE = 'ø';
+    static final String INLINE_MARKUP_DELIMITERS = " _*`#^~.,";
+
+    /**
+     * The different ways embedded inline markers can be erased
+     */
+    public enum EraseStyle {
+        All,
+        None,
+        Markers,
+        InlineMarkup,
+        PreserveLabel,
+        CloseMarkerContainsDelimiters
+    }
+
+    // a list of offsets for each character
+    List<Integer> offsets = new ArrayList<>();
+    // the text for the line
+    List<Character> text = new ArrayList<>();
+    // marks erased characters as invalid
+    List<Boolean> valid = new ArrayList<>();
+    // remembers which characters were escaped in the original string
+    List<Boolean> escaped = new ArrayList<>();
+
+    private int lineNo = 0;
+    private boolean allSameCharacter = false;
+    private boolean erased = false;
+    private boolean inBlock = false;
+
+    private int sectionLevel = 0;
+    private int listLevel = 0;
+    private boolean listStart = false;
+
+    /**
+     * Construct a line using the supplied string
+     *
+     * @param str    the text of the line
+     * @param lineno the original line number
+     */
+    public Line(String str, int lineno) {
+        this.lineNo = lineno;
+        if (!str.isEmpty()) {
+            allSameCharacter = true;
+            char lastCh = 0;
+            for (int i = 0; i < str.length(); i++) {
+                char ch = str.charAt(i);
+
+                if ((i < str.length() - 1) && (ch == '\\')) {
+                    i++;
+                    ch = str.charAt(i);
+                    escaped.add(true);
+                }
+                else {
+                    escaped.add(false);
+                }
+
+                offsets.add(i);
+                text.add(ch);
+                valid.add(true);
+
+                if ((lastCh != 0) && (lastCh != ch)) {
+                    allSameCharacter = false;
+                }
+                lastCh = ch;
+            }
+        }
+
+        // trim the end
+        while (!text.isEmpty() &&
+                Character.isWhitespace(text.get(text.size() - 1))) {
+            text.remove(text.size() - 1);
+        }
+    }
+
+    /**
+     * Is this line a repeating set of the same character?
+     *
+     * @return
+     */
+    public boolean isAllSameCharacter() {
+        return allSameCharacter;
+    }
+
+    /**
+     * Has this line been completely erased?
+     *
+     * @return
+     */
+    public boolean isErased() {
+        return erased;
+    }
+
+    /**
+     * Is this line in a block?
+     *
+     * @return
+     */
+    public boolean isInBlock() {
+        return inBlock;
+    }
+
+
+    /**
+     * Is this line the start of a new list item?
+     *
+     * @return
+     */
+    public boolean isListStart() {
+        return listStart;
+    }
+
+    /**
+     * Return the list level for this line, or zero if it is not in a list.
+     *
+     * @return the list level, or zero if not in a list
+     */
+    public int getListLevel() {
+        return listLevel;
+    }
+
+    /**
+     * If the line is a section header, return the section level,
+     * or zero if the line is not a section header
+     *
+     * @return the section level or zero if not a section header
+     */
+    public int getSectionLevel() {
+        return sectionLevel;
+    }
+
+    public void setInBlock(boolean inBlock) {
+        this.inBlock = inBlock;
+    }
+
+    public void setSectionLevel(int newSectionLevel) {
+        this.sectionLevel = newSectionLevel;
+    }
+
+    public void setListLevel(int newListLevel) {
+        this.listLevel = newListLevel;
+    }
+
+    public void setListStart(boolean listStart) {
+        this.listStart = listStart;
+    }
+
+    /**
+     * Erase length characters in the line, starting at pos
+     *
+     * @param pos
+     * @param length
+     */
+    public void erase(int pos, int length) {
+        if ((pos >= 0) && (pos < valid.size())) {
+            for (int i = pos; (i < valid.size()) && (i < pos + length); i++) {
+                valid.set(i, false);
+            }
+        }
+    }
+
+    /**
+     * Erase the whole line
+     */
+    public void erase() {
+        for (int i = 0; i < valid.size(); i++) {
+            valid.set(i, false);
+        }
+        erased = true;
+    }
+
+    /**
+     * Erase all occurances of the given string
+     *
+     * @param segment
+     */
+    public void erase(String segment) {
+        for (int i = 0; i < text.size(); i++) {
+            boolean found = true;
+            for (int j = 0; j < segment.length(); j++) {
+                if (charAt(j + i) != segment.charAt(j)) {
+                    found = false;
+                    break;
+                }
+            }
+            if (found) {
+                erase(i, segment.length());
+                i += segment.length();
+            }
+        }
+    }
+
+    /**
+     * Erase the open and close markers, and optionally all the text inside them
+     * Returns the position of the first enclosure or -1 if no enclosure was found
+     *
+     * @param open
+     * @param close
+     * @param eraseStyle
+     * @return position of first enclosure
+     */
+    public int eraseEnclosure(String open,
+                              String close,
+                              EraseStyle eraseStyle) {
+        boolean inEnclosure = false;
+        int firstEnclosurePosition = -1;
+        int lastCommaPosition = -1;
+        int enclosureStart = 0;
+        for (int i = 0; i < length(); i++) {
+            if (valid.get(i)) {
+                if (!inEnclosure) {
+                    // look for the open string
+                    boolean foundOpen = true;
+                    for (int j = 0; j < open.length(); j++) {
+                        if (charAt(i + j) != open.charAt(j)) {
+                            foundOpen = false;
+                            break;
+                        }
+                    }
+                    // inline requires start of line or a space before the marker
+                    if (foundOpen && (eraseStyle == EraseStyle.InlineMarkup)) {
+                        if ((i != 0) &&
+                                (INLINE_MARKUP_DELIMITERS.indexOf(charAt(i - 1)) == -1)) {
+                            foundOpen = false;
+                        }
+                    }
+                    if (foundOpen) {
+                        enclosureStart = i;
+                        inEnclosure = true;
+                        firstEnclosurePosition = i;
+                    }
+                }
+                else {
+                    // look for the close string
+                    boolean foundClose = true;
+                    if (eraseStyle == EraseStyle.CloseMarkerContainsDelimiters) {
+                        foundClose = (i == length() - 1) || (close.indexOf(charAt(i)) != -1);
+                    }
+                    else {
+                        for (int j = 0; j < close.length(); j++) {
+                            if (charAt(i + j) != close.charAt(j)) {
+                                foundClose = false;
+                                break;
+                            }
+                        }
+                    }
+
+                    if (foundClose && (eraseStyle == EraseStyle.InlineMarkup)) {
+                        if ((i != length() - 1) &&
+                                (INLINE_MARKUP_DELIMITERS.indexOf(charAt(i + close.length())) == -1)) {
+                            foundClose = false;
+                        }
+                    }
+
+                    if (foundClose) {
+                        switch (eraseStyle) {
+                            case All:
+                                erase(enclosureStart, (i - enclosureStart) + close.length());
+                                break;
+                            case Markers:
+                            case InlineMarkup:
+                                erase(enclosureStart, open.length());
+                                erase(i, close.length());
+                                break;
+                            case PreserveLabel:
+                                if (lastCommaPosition != -1) {
+                                    erase(enclosureStart, (lastCommaPosition + 1) - enclosureStart);
+                                    erase(i, close.length());
+                                }
+                                else {
+                                    erase(enclosureStart, open.length());
+                                    erase(i, close.length());
+                                }
+                                break;
+                            case CloseMarkerContainsDelimiters:
+                                erase(enclosureStart, (i == length() - 1)
+                                        ? (length() - enclosureStart)
+                                        : (i - enclosureStart));
+                                break;
+                            case None:
+                                break;
+                        }
+                        inEnclosure = false;
+                        lastCommaPosition = -1;
+                    }
+                    else if (charAt(i) == ',') {
+                        lastCommaPosition = i;
+                    }
+                }
+            }
+        }
+        return firstEnclosurePosition;
+    }
+
+    /**
+     * Return the length of the line
+     *
+     * @return
+     */
+    public int length() {
+        return text.size();
+    }
+
+    /**
+     * Return the character at the given position. Erase characters will return 0 rather
+     * than the actual character
+     *
+     * @param i
+     * @return
+     */
+    public char charAt(int i) {
+        return charAt(i, false);
+    }
+
+    /**
+     * Return the character at the given position, optionally including erased characters
+     *
+     * @param i
+     * @param includeInvalid
+     * @return
+     */
+    public char charAt(int i, boolean includeInvalid) {
+        if ((i >= 0) && (i < text.size())) {
+            if (escaped.get(i)) {
+                return ESCAPED_CHARACTER_VALUE;
+            }
+            if (includeInvalid || valid.get(i)) {
+                return text.get(i);
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Return the offset for the character at the given position
+     *
+     * @param i
+     * @return
+     */
+    public int getOffset(int i) {
+        if (i >= 0) {
+            if (i < offsets.size()) {
+                return offsets.get(i);
+            }
+            else {
+                return offsets.size();
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Return the original line number for this line
+     *
+     * @return
+     */
+    public int getLineNo() {
+        return lineNo;
+    }
+
+    /**
+     * Return the raw character at the specified position, ignoring its validity.
+     *
+     * @param i
+     * @return
+     */
+    public char rawCharAt(int i) {
+        if ((i >= 0) && (i < text.size())) {
+            return text.get(i);
+        }
+        return ' ';
+    }
+
+    /**
+     * Is the character at the given position valid?
+     *
+     * @param i
+     * @return
+     */
+    public boolean isValid(int i) {
+        if ((i >= 0) && (i < text.size())) {
+            return valid.get(i);
+        }
+        return false;
+    }
+
+    /**
+     * Is the character at the given position empty (ie: whitespace or invalid)
+     *
+     * @return
+     */
+    public boolean isEmpty() {
+        for (int i = 0; i < text.size(); i++) {
+            if (!Character.isWhitespace(text.get(i)))
+                if (valid.get(i)) {
+                    return false;
+                }
+        }
+
+        return true;
+    }
+
+    /**
+     * Does the line start with the given string?
+     *
+     * @param s
+     * @return
+     */
+    public boolean startsWith(String s) {
+        for (int i = 0; i < s.length(); i++) {
+            if (charAt(i) != s.charAt(i)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Render the line as a string, showing what's been erased
+     * <p>
+     * X         = whole line erased
+     * [         = block
+     * nn-nnn-nn = section - listlevel - lineno
+     * *         = list item
+     * ·         = next character erased
+     *
+     * @return
+     */
+    @Override
+    public String toString() {
+        String result = "";
+        for (int i = 0; i < text.size(); i++) {
+            if (valid.get(i)) {
+                result += text.get(i);
+            }
+            else {
+                result += "·" + text.get(i);
+            }
+        }
+        return (erased ? "X" : " ") +
+                (inBlock ? "[" : " ") +
+                sectionLevel + "-" +
+                listLevel + "-" +
+                String.format("%03d", lineNo) +
+                (listStart ? "*" : ":") +
+                " " +
+                result;
+    }
+}
diff --git a/redpen-core/src/main/java/cc/redpen/parser/asciidoc/Model.java b/redpen-core/src/main/java/cc/redpen/parser/asciidoc/Model.java
new file mode 100644
index 00000000..efe8fcd4
--- /dev/null
+++ b/redpen-core/src/main/java/cc/redpen/parser/asciidoc/Model.java
@@ -0,0 +1,189 @@
+/**
+ * redpen: a text inspection tool
+ * Copyright (c) 2014-2015 Recruit Technologies Co., Ltd. and contributors
+ * (see CONTRIBUTORS.md)
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package cc.redpen.parser.asciidoc;
+
+import cc.redpen.model.Sentence;
+import cc.redpen.parser.LineOffset;
+import cc.redpen.parser.SentenceExtractor;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A model of the original document, represented as an array of lines
+ */
+public class Model {
+    private static final Line EMPTY_LINE = new Line("", 0);
+
+    private List<Line> lines = new ArrayList<>();
+
+    private int lineIndex = 0;
+    private SentenceExtractor sentenceExtractor;
+
+    /**
+     * Construct a model. The sentence extract will be used delimit sentences and
+     * join lines together when adding the model to the document builder
+     *
+     * @param sentenceExtractor
+     */
+    public Model(SentenceExtractor sentenceExtractor) {
+        this.sentenceExtractor = sentenceExtractor;
+    }
+
+    /**
+     * Return the offset string from the model at the given line number
+     *
+     * @param lineNumber
+     * @return
+     */
+    public Line getLine(int lineNumber) {
+        int index = lineNumber - 1;
+        if ((index >= 0) && (index < lines.size())) {
+            return lines.get(index);
+        }
+        return EMPTY_LINE;
+    }
+
+    /**
+     * Add a line to the model
+     *
+     * @param line
+     */
+    public void add(Line line) {
+        lines.add(line);
+    }
+
+    /**
+     * Return the number of lines in the model
+     *
+     * @return
+     */
+    public int lineCount() {
+        return lines.size();
+    }
+
+    /**
+     * Set the line pointer to the first  line
+     */
+    public void rewind() {
+        lineIndex = 0;
+    }
+
+    /**
+     * Get the next line from the model, or null if there are no more lines
+     *
+     * @return
+     */
+    public Line getNextLine() {
+        if (lineIndex < lines.size()) {
+            Line line = lines.get(lineIndex);
+            lineIndex++;
+            return line;
+        }
+        return null;
+    }
+
+    /**
+     * Return the current line from the model, or null if we've run out of lines
+     *
+     * @return
+     */
+    public Line getCurrentLine() {
+        if (lineIndex < lines.size()) {
+            Line line = lines.get(lineIndex);
+            return line;
+        }
+        return null;
+    }
+
+    /**
+     * Are there more lines to iterate through?
+     *
+     * @return
+     */
+    public boolean isMore() {
+        return lineIndex < lines.size();
+    }
+
+    /**
+     * Convert the single line into an array of sentences
+     *
+     * @param line
+     * @return
+     */
+    public List<Sentence> convertToSentences(Line line) {
+        List<Line> lines = new ArrayList<>();
+        lines.add(line);
+        return convertToSentences(lines);
+    }
+
+    /**
+     * Convert a list of Lines into sentences by breaking them up appropriately, whilst
+     * tracking their original line number and offset
+     *
+     * @param lines
+     * @return
+     */
+    public List<Sentence> convertToSentences(List<Line> lines) {
+        List<Sentence> sentences = new ArrayList<>();
+
+        String content = "";
+        List<LineOffset> offsets = new ArrayList<>();
+        for (int ln = 0; ln < lines.size(); ln++) {
+            Line line = lines.get(ln);
+
+            for (int i = 0; i < line.length(); i++) {
+                if (line.isValid(i)) {
+                    content += line.rawCharAt(i);
+                    offsets.add(new LineOffset(line.getLineNo(), line.getOffset(i)));
+                    // check for end of sentence
+                    if (sentenceExtractor.getSentenceEndPosition("" + line.rawCharAt(i)) != -1) {
+                        sentences.add(new Sentence(content, offsets, Collections.EMPTY_LIST));
+                        content = "";
+                        offsets = new ArrayList<>();
+                    }
+                }
+            }
+            // join lines
+            if ((lines.size() > 1) && (ln != lines.size() - 1)) {
+                for (char c : sentenceExtractor.getBrokenLineSeparator().toCharArray()) {
+                    content += c;
+                    offsets.add(new LineOffset(line.getLineNo(), line.getOffset(line.length())));
+                }
+            }
+        }
+        // add remaining line
+        if (!content.trim().isEmpty()) {
+            sentences.add(new Sentence(content, offsets, Collections.EMPTY_LIST));
+        }
+        return sentences;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        for (Line ostring : lines) {
+            sb.append(ostring.toString());
+            sb.append("\n");
+        }
+        return sb.toString();
+    }
+
+}
+
diff --git a/redpen-core/src/main/java/org/asciidoctor/RedPenTreeProcessor.java b/redpen-core/src/main/java/org/asciidoctor/RedPenTreeProcessor.java
deleted file mode 100644
index 375896ad..00000000
--- a/redpen-core/src/main/java/org/asciidoctor/RedPenTreeProcessor.java
+++ /dev/null
@@ -1,535 +0,0 @@
-/**
- * redpen: a text inspection tool
- * Copyright (c) 2014-2015 Recruit Technologies Co., Ltd. and contributors
- * (see CONTRIBUTORS.md)
- * <p>
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * <p>
- * http://www.apache.org/licenses/LICENSE-2.0
- * <p>
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.asciidoctor;
-
-import cc.redpen.model.Sentence;
-import cc.redpen.parser.LineOffset;
-import cc.redpen.parser.SentenceExtractor;
-import org.apache.commons.lang3.StringEscapeUtils;
-import org.asciidoctor.ast.*;
-import org.asciidoctor.extension.Treeprocessor;
-import org.jruby.RubyArray;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * AsciiDoctor tree processor, for use with the "redpen" AsciiDoctor backend, used to populate a RedPen document
- * <p>
- * This (unfortunately) has to be under an 'org' or 'com' package, rather than a 'cc' package,
- * due to the way JRuby and AsciiDoctor handle the registration of this class.
- */
-public class RedPenTreeProcessor extends Treeprocessor {
-
-    private static final Logger LOG = LoggerFactory.getLogger(RedPenTreeProcessor.class);
-
-    private static final char REDPEN_ASCIIDOCTOR_BACKEND_LINE_START = '\001';
-    private static final char REDPEN_ASCIIDOCTOR_BACKEND_LINENUMBER_DELIM = '\002';
-    private static final char REDPEN_ASCIIDOCTOR_BACKEND_SUBSTITUTION_START = '\003';
-    private static final char REDPEN_ASCIIDOCTOR_BACKEND_SUBSTITUTION_END = '\004';
-
-
-    private cc.redpen.model.Document.DocumentBuilder documentBuilder;
-    private SentenceExtractor sentenceExtractor;
-
-    private int lineNumber = 1;
-    private int headerNumber = 0;
-    private int listBlockLevel = 0;
-    /**
-     * the header source lines, and their respective line numbers,
-     * as recorded by the patched parser routing in AsciiDocParser
-     */
-    private RubyArray headerLinesSource = null;
-    private RubyArray headerLinesLineNos = null;
-
-    /**
-     * Build a tree processer that uses the supplied documentBuilder and sentenceExtractor
-     *
-     * @param documentBuilder   redpen document builder
-     * @param sentenceExtractor redpen sentence extractor
-     * @param config            asciidoctor's configuration map
-     */
-    public RedPenTreeProcessor(cc.redpen.model.Document.DocumentBuilder documentBuilder, SentenceExtractor sentenceExtractor, Map<String, Object> config) {
-        super(config);
-        this.documentBuilder = documentBuilder;
-        this.sentenceExtractor = sentenceExtractor;
-    }
-
-    /**
-     * Helper method to get the source text for a header line
-     *
-     * @param headerNumber the number of the header we're after
-     * @return
-     */
-    private String getHeaderSource(int headerNumber) {
-        if ((headerLinesSource != null) && (headerNumber < headerLinesSource.size())) {
-            return String.valueOf(headerLinesSource.get(headerNumber));
-        }
-        return "";
-    }
-
-    /**
-     * Helper method to get the header line numbers for a particular header line
-     *
-     * @param headerNumber the number of the header we're after
-     * @return
-     */
-    private int getHeaderLineNo(int headerNumber) {
-        if ((headerLinesLineNos != null) && (headerNumber < headerLinesLineNos.size())) {
-            try {
-                return ((Long) headerLinesLineNos.get(headerNumber)).intValue();
-            } catch (Exception ignored) {
-            }
-        }
-        return lineNumber;
-    }
-
-    /**
-     * Process a AsciiDoctorJ document
-     *
-     * @param document this is an AsciiDoctorJ document, not a redpen document
-     * @return processed document
-     */
-    @Override
-    @SuppressWarnings("unchecked")
-    public Document process(Document document) {
-        try {
-            List<Sentence> headers = new ArrayList<>();
-
-            // our patched Parser routine should store the header source lines and their line numbers in these attributes
-            headerLinesSource = (RubyArray) document.getAttributes().get("header_lines_source");
-            headerLinesLineNos = (RubyArray) document.getAttributes().get("header_lines_linenos");
-
-            lineNumber = getHeaderLineNo(headerNumber);
-
-            // parse the sentences/headers from the document title
-            processParagraph(document.doctitle(), getHeaderSource(headerNumber), headers);
-
-            if (headers.isEmpty()) {
-                headers.add(new Sentence(document.doctitle() != null ? document.doctitle() : "", 0));
-            }
-            documentBuilder.addSection(0, headers);
-
-            headerNumber++;
-
-            // traverse all of the blocks in the document
-            traverse(document.blocks(), 0);
-        } catch (Exception e) {
-            LOG.error("Exception when processing AsciiDoc document", e);
-        }
-        return document;
-    }
-
-    /**
-     * Traverse the list of blocks, recursively traversing any lists of blocks found within each block
-     *
-     * @param blocks
-     * @param indent
-     */
-    @SuppressWarnings("unchecked")
-    private void traverse(List<AbstractBlock> blocks, int indent) {
-        try {
-            for (int i = 0; i < blocks.size(); i++) {
-                Object item = blocks.get(i);
-                // A standard block - we convert the text and process the sentences inside
-                if (item instanceof BlockImpl) {
-                    BlockAccessor accessor = new BlockAccessor(item);
-                    // we don't want to add some blocks, such as listings
-                    boolean suppressed = accessor.getType().equals("listing");
-                    BlockImpl block = (BlockImpl) item;
-                    List<Sentence> sentences = new ArrayList<>();
-                    processParagraph(block.convert(), block.source(), sentences);
-
-                    if (!suppressed) {
-                        documentBuilder.addParagraph();
-                        for (Sentence sentence : sentences) {
-                            documentBuilder.addSentence(sentence);
-                        }
-                    }
-                }
-                // A section - this has a header, and contains other blocks
-                else if (item instanceof SectionImpl) {
-                    SectionImpl section = (SectionImpl) item;
-                    List<Sentence> headers = new ArrayList<>();
-                    lineNumber = getHeaderLineNo(headerNumber);
-                    processParagraph(section.title(), getHeaderSource(headerNumber), headers);
-                    if (headers.isEmpty()) {
-                        headers.add(new Sentence(section.title() != null ? section.title() : "", 0));
-                    }
-                    documentBuilder.addSection(section.number(), headers);
-                    headerNumber++;
-                    traverse(section.blocks(), indent + 1);
-                }
-                // catchall for all other abstract blocks
-                else if (item != null) {
-                    AbstractBlock block = (AbstractBlock) item;
-                    BlockAccessor accessor = new BlockAccessor(item);
-
-                    // process a list
-                    if (accessor.getType().equals("ulist") ||
-                            accessor.getType().equals("dlist") ||
-                            accessor.getType().equals("olist")) {
-                        // Nested lists in AsciiDoctor are returned as new blocks
-                        // RedPen prefers one list block with varied indents.
-                        if (indent <= listBlockLevel) {
-                            listBlockLevel = indent;
-                            documentBuilder.addListBlock();
-                        }
-                    } else if (accessor.getType().equals("list_item")) {
-                        // add the list item
-                        int listLevel = accessor.getIndent();
-                        List<Sentence> sentences = new ArrayList<>();
-                        lineNumber = accessor.getLineNo();
-                        processParagraph(accessor.getValue(), accessor.getSourceText(), sentences);
-                        documentBuilder.addListElement(listLevel, sentences);
-                    }
-                    traverse(block.blocks(), indent + 1);
-                } else {
-                    LOG.error("Unhandled AsciiDoctor Block class " + item.getClass().getSimpleName());
-                }
-            }
-        } catch (Exception e) {
-            LOG.error("Exception when traversing AsciiDoc blocks", e);
-        }
-    }
-
-    /**
-     * Process a paragraph of processed text.
-     * <p>
-     * This method takes the processed form of the text generated by the Ruby RedPen AsciiDoctor backend found in AsciiDocParser.java
-     * If the source form of the paragraph is also provided, it uses this to calculate the RedPen offsets for the characters in the text.
-     * <p>
-     * The AsciiDoctor parser does not return the line numbers via the AsciiDoctorJ Block interface. Therefore, the custom RedPen AsciiDoctor backend
-     * encodes the line number between ^A and ^B, if known.
-     * <p>
-     * Hence the processed text for a paragraph is typically:
-     * <p>
-     * ^Alinenumber^Bparagraph_text
-     * <p>
-     * This method first breaks the paragraph on these line markers and adjusts the running lineNumber variable,
-     * and then processes each sub-paragraph/sentence using standard RedPen sentence-end-position delimiting.
-     *
-     * @param paragraph  the AsciiDoctor processed text, already converted by the RedPen AsciiDoctor backend
-     * @param sourceText the raw source text
-     * @param sentences  A list of sentences discovered in the processed text
-     */
-    protected void processParagraph(String paragraph, String sourceText, List<Sentence> sentences) {
-        try {
-            paragraph = paragraph == null ? "" : paragraph;
-            sourceText = sourceText == null ? "" : sourceText;
-            int offset = 0;
-
-            String[] sublines = paragraph.split(String.valueOf(REDPEN_ASCIIDOCTOR_BACKEND_LINE_START));
-
-            for (String subline : sublines) {
-                int lineNumberEndPos = subline.indexOf(REDPEN_ASCIIDOCTOR_BACKEND_LINENUMBER_DELIM);
-                if (lineNumberEndPos != -1) {
-                    try {
-                        lineNumber = Integer.valueOf(subline.substring(0, lineNumberEndPos));
-                    } catch (Exception e) {
-                        LOG.error("Error when parsing line number from converted AsciiDoc", e);
-                    }
-                    subline = subline.substring(lineNumberEndPos + 1);
-                }
-                subline = StringEscapeUtils.unescapeHtml4(subline);
-
-                while (true) {
-                    int periodPosition = sentenceExtractor.getSentenceEndPosition(subline);
-                    if (periodPosition != -1) {
-                        String candidateSentence = subline.substring(0, periodPosition + 1);
-                        subline = subline.substring(periodPosition + 1);
-                        periodPosition = sentenceExtractor.getSentenceEndPosition(sourceText);
-                        String sourceSentence = "";
-                        if (periodPosition != -1) {
-                            sourceSentence = sourceText.substring(0, periodPosition + 1);
-                            sourceText = sourceText.substring(periodPosition + 1);
-                        }
-                        LineOffset lineOffset = addSentence(
-                                new LineOffset(lineNumber, offset),
-                                candidateSentence,
-                                sourceSentence,
-                                sentenceExtractor,
-                                sentences);
-                        lineNumber = lineOffset.lineNum;
-                        offset = lineOffset.offset;
-                    } else {
-                        break;
-                    }
-                }
-                if (!subline.trim().isEmpty()) {
-                    addSentence(
-                            new LineOffset(lineNumber, offset),
-                            subline,
-                            sourceText,
-                            sentenceExtractor,
-                            sentences);
-                }
-            }
-            lineNumber++;
-        } catch (Exception e) {
-            LOG.error("Exception when processing AsciiDoc paragraph", e);
-        }
-    }
-
-    /**
-     * Add a processed AsciiDoc sentence, using the raw source sentence to guide the character offsets.
-     * <p>
-     * Since AsciiDoctor does not return character offset positions for altered/formatted elements, this method
-     * attempts to calculate the offsets by comparing the source sentence with the post-processed sentence.
-     * <p>
-     * To assist with the calculation, the RedPen AsciiDoctor backend places markers in the text where
-     * AsciiDoctor has subsituted or altered the source sentence.
-     * <p>
-     * This is a normal function of AsciiDoctor. For example, AsciiDoctor's HTML backend will convert:
-     * <p>
-     * This is *bold*.
-     * <p>
-     * to:
-     * <p>
-     * This is &lt;strong&gt;bold&lt;/strong&gt;.
-     * <p>
-     * The RedPen AsciiDoctor backend uses ^C and ^D for all such substitutions, regardless of how many characters the
-     * markup originally took. For example, both _ and __ markup notations are replaced by a single ^C or ^D.
-     * So after processing using the RedPen AsciiDoctor backend, our sentence would look like:
-     * <p>
-     * This is ^Cbold^D.
-     * <p>
-     * This method uses these ^C/^D markers to calculate what has been omitted from the source sentence, and
-     * therefore what the source sentence character offset should be for the normalized sentence.
-     *
-     * @param lineOffset        the position of this line
-     * @param processed         the RedPen AsciiDoctor backend processed sentence
-     * @param source            the source sentence
-     * @param sentenceExtractor our sentence extractor
-     * @param sentences         a list of sentences to add discovered sentences to
-     * @return the new offset position, after we've processed these sentences
-     */
-    protected LineOffset addSentence(LineOffset lineOffset, String processed, String source, SentenceExtractor sentenceExtractor, List<Sentence> sentences) {
-        List<LineOffset> offsetMap = new ArrayList<>();
-        String normalizedSentence = "";
-
-        int lineNum = lineOffset.lineNum;
-        int offset = lineOffset.offset;
-        try {
-
-            int sourceOffset = 0;
-            int window = 0;
-            int matchLength = 4;
-            for (int i = 0; i < processed.length(); i++) {
-                char ch = processed.charAt(i);
-                switch (ch) {
-                    case REDPEN_ASCIIDOCTOR_BACKEND_SUBSTITUTION_START:
-                        window += 4;
-                        break;
-                    case REDPEN_ASCIIDOCTOR_BACKEND_SUBSTITUTION_END:
-                        window = Math.max(0, window - 4);
-                        break;
-                    default:
-                        // catch up with the source string using the window and match length
-                        if ((sourceOffset < source.length()) && (source.charAt(sourceOffset) != ch)) {
-                            String match = processed.substring(i, Math.min(processed.length(), i + matchLength));
-                            int pos = match.indexOf(REDPEN_ASCIIDOCTOR_BACKEND_SUBSTITUTION_START);
-                            if (pos != -1) {
-                                match = match.substring(0, pos);
-                            }
-                            pos = match.indexOf(REDPEN_ASCIIDOCTOR_BACKEND_SUBSTITUTION_END);
-                            if (pos != -1) {
-                                match = match.substring(0, pos);
-                            }
-                            for (int j = 0; (sourceOffset < source.length()); j++, sourceOffset++, offset++) {
-                                if (source.substring(sourceOffset).startsWith(match)) {
-                                    break;
-                                }
-                            }
-                        }
-
-                        if (ch == '\n') {
-                            if (!sentenceExtractor.getBrokenLineSeparator().isEmpty()) {
-                                offsetMap.add(new LineOffset(lineNum, offset));
-                                normalizedSentence += sentenceExtractor.getBrokenLineSeparator();
-                            }
-                            lineNum++;
-                            offset = 0;
-                        } else {
-                            normalizedSentence += ch;
-                            offsetMap.add(new LineOffset(lineNum, offset));
-                            offset++;
-                        }
-
-                        sourceOffset++;
-                        break;
-                }
-            }
-
-            int startOfSentenceOffset = offsetMap.isEmpty() ? lineOffset.offset : offsetMap.get(0).offset;
-            Sentence sentence = new Sentence(normalizedSentence, lineOffset.lineNum, startOfSentenceOffset);
-            sentence.setOffsetMap(offsetMap);
-            sentences.add(sentence);
-        } catch (Exception e) {
-            LOG.error("Exception when adding sentence from AsciiDoc", e);
-        }
-        return new LineOffset(lineNum, offset);
-    }
-
-    /**
-     * helper class to inspect and access important asciidoctor information
-     * obscured by JRuby (and AsciiDoctorJ)
-     */
-    protected static class BlockAccessor {
-        private static final int VAR_INDEX_TYPE_NAME = 2;
-        private static final int VAR_INDEX_CONVERTED_VALUE = 18;
-        private static final int VAR_INDEX_LIST_LEVEL = 14;
-        private static final int VAR_INDEX_SOURCE_TEXT = 20;
-        private static final int VAR_INDEX_SOURCE_POS = 17;
-        private static final int VAR_INDEX_LINE_NO_POS = 3;
-
-        private Object[] varTable = null;
-
-        /**
-         * This method takes an AsciiDoctorJ JRuby object which should be an instance of
-         * an abstract block.
-         * <p>
-         * We are attempting to get access to the underlying varTable element, which is an
-         * array of public variables stored in the JRuby object.
-         *
-         * @param aBlock an AsciiDoctorJ block
-         */
-        @SuppressWarnings("unchecked")
-        public BlockAccessor(Object aBlock) {
-            if (aBlock instanceof AbstractBlock) {
-                AbstractBlockImpl block = (AbstractBlockImpl) aBlock;
-                varTable = getVarTable(block.delegate());
-            }
-        }
-
-        /**
-         * The type element of the var table returns the type of this block, for example
-         * 'ulist' or 'list_time'
-         *
-         * @return the type of the block
-         */
-        public String getType() {
-            if ((varTable == null) || (varTable.length < VAR_INDEX_TYPE_NAME)) {
-                return "";
-            }
-            return varTable[VAR_INDEX_TYPE_NAME].toString();
-        }
-
-        /**
-         * The block's value is usually its processed or converted text form
-         *
-         * @return the block's value
-         */
-        public String getValue() {
-            if ((varTable == null) || (varTable.length < VAR_INDEX_CONVERTED_VALUE)) {
-                return "";
-            }
-            return varTable[VAR_INDEX_CONVERTED_VALUE].toString();
-        }
-
-        /**
-         * If the underlying JRuby object is a list item, then this variable is the
-         * indent of that list item.
-         *
-         * @return the indent level of the list item
-         */
-        public int getIndent() {
-            if ((varTable == null) || (varTable.length < VAR_INDEX_LIST_LEVEL)) {
-                return 0;
-            }
-            try {
-                return Integer.parseInt(varTable[VAR_INDEX_LIST_LEVEL].toString());
-            } catch (Exception ignored) {
-            }
-            return 0;
-        }
-
-        /**
-         * The source text is a new block element inserted into the JRuby object by
-         * the Ruby code in redpen's AsciiDocParser.java. Though some blocks store the source code
-         * line alongside the processed line, certain blocks, such as list blocks, do not.
-         * <p>
-         * Therefore the JRuby parser's list processor is overriden in redpen's AsciiDocParser.java
-         * to store the source text in a member variable.
-         * <p>
-         * In future, we might be able to replace the other mechanisms we use to find source lines,
-         * for example header source lines, with this new mechanism.
-         *
-         * @return the source text for the block, before processing
-         */
-        public String getSourceText() {
-            if ((varTable == null) || (varTable.length < VAR_INDEX_SOURCE_TEXT)) {
-                return "";
-            }
-            return varTable[VAR_INDEX_SOURCE_TEXT].toString();
-        }
-
-        /**
-         * This returned the line number stored inside the JRuby object. The line number
-         * is stored as a file 'cursor', which is in itself a varTable. The line number is the
-         * third element of that table.
-         *
-         * @return the line number the block starts at
-         */
-        public int getLineNo() {
-            if ((varTable == null) || (varTable.length < VAR_INDEX_SOURCE_POS)) {
-                return 0;
-            }
-            try {
-                Object[] sourcePos = getVarTable(varTable[VAR_INDEX_SOURCE_POS]);
-                if (sourcePos != null) {
-                    return Integer.parseInt(sourcePos[VAR_INDEX_LINE_NO_POS].toString());
-                }
-            } catch (Exception ignored) {
-            }
-            return 0;
-        }
-
-        /**
-         * This method uses introspection to get access to the JRuby objects variable table,
-         * which is implemented as an array of objects.
-         *
-         * @param o JRuby object instance
-         * @return the variable table inside the JRuby object instance
-         */
-        private Object[] getVarTable(Object o) {
-            try {
-                Object self;
-                try {
-                    Field dollarSelfField = o.getClass().getDeclaredField("$self");
-                    dollarSelfField.setAccessible(true);
-                    self = dollarSelfField.get(o);
-                } catch (NoSuchFieldException ee) {
-                    self = o;
-                }
-                Field varTableField = self.getClass().getField("varTable");
-                varTableField.setAccessible(true);
-                return (Object[]) varTableField.get(self);
-            } catch (Exception e) {
-                LOG.error("Error extracting varTable from " + o + ": " + e.getMessage());
-            }
-            return null;
-        }
-    }
-
-}
\ No newline at end of file


Test patch:
diff --git a/redpen-core/src/test/java/cc/redpen/AsciiDocParserTest.java b/redpen-core/src/test/java/cc/redpen/AsciiDocParserTest.java
index f438997f..739bdd83 100644
--- a/redpen-core/src/test/java/cc/redpen/AsciiDocParserTest.java
+++ b/redpen-core/src/test/java/cc/redpen/AsciiDocParserTest.java
@@ -26,7 +26,10 @@ import cc.redpen.parser.SentenceExtractor;
 import cc.redpen.validator.ValidationError;
 import org.junit.Before;
 import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.io.FileInputStream;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
@@ -35,6 +38,7 @@ import java.util.List;
 import static org.junit.Assert.*;
 
 public class AsciiDocParserTest {
+    private static final Logger LOG = LoggerFactory.getLogger(AsciiDocParserTest.class);
 
     @Before
     public void setup() {
@@ -53,7 +57,9 @@ public class AsciiDocParserTest {
         String sampleText = "Instances Overview\n==================\n" + "Author's Name <person@email.address>\nv1.2, 2015-08\n" +
                 "\nThis is the optional preamble (an untitled section body). Useful for " +
                 "writing simple sectionless documents consisting only of a preamble.\n\n" +
+
                 "NOTE: The abstract, preface, appendix, bibliography, glossary and index section titles are significant ('specialsections').\n" +
+
                 "\n\n:numbered!:\n[abstract]\n" +
                 "Instances\n" +
                 "---------\n" +
@@ -115,13 +121,13 @@ public class AsciiDocParserTest {
         }
 
         assertNotNull("doc is null", doc);
-        assertEquals(3, doc.size());
+        assertEquals(4, doc.size());
 
         final Section firstSection = doc.getSection(0);
         assertEquals(1, firstSection.getHeaderContentsListSize());
         assertEquals("Instances Overview", firstSection.getHeaderContent(0).getContent());
         assertEquals(0, firstSection.getNumberOfLists());
-        assertEquals(1, firstSection.getNumberOfParagraphs());
+        assertEquals(2, firstSection.getNumberOfParagraphs());
         assertEquals(0, firstSection.getNumberOfSubsections());
 
         Configuration configuration = new Configuration.ConfigurationBuilder()
@@ -173,7 +179,7 @@ public class AsciiDocParserTest {
 
     @Test
     public void testSectionHeader() throws UnsupportedEncodingException {
-        String sampleText = "# About _Gekioko_.\n\n" +
+        String sampleText = "= About _Gekioko_.\n\n" +
                 "Gekioko means angry.";
 
         Document doc = createFileContent(sampleText);
@@ -264,7 +270,7 @@ public class AsciiDocParserTest {
         String sampleText = "There are several railway companies in Japan as follows.\n";
         sampleText += "\n";
         sampleText += "* Tokyu\n";
-        sampleText += "**  Toyoko Line\n";
+        sampleText += "  **  Toyoko Line\n";
         sampleText += "** Denentoshi Line\n";
         sampleText += "* Keio\n";
         sampleText += "* Odakyu\n";
@@ -279,7 +285,7 @@ public class AsciiDocParserTest {
         assertEquals("Toyoko Line", doc.getSection(0).getListBlock(0).getListElement(1).getSentence(0).getContent());
         assertEquals(2, doc.getSection(0).getListBlock(0).getListElement(1).getLevel());
         assertEquals(4, doc.getSection(0).getListBlock(0).getListElement(1).getSentence(0).getLineNumber());
-        assertEquals(4, doc.getSection(0).getListBlock(0).getListElement(1).getSentence(0).getStartPositionOffset());
+        assertEquals(6, doc.getSection(0).getListBlock(0).getListElement(1).getSentence(0).getStartPositionOffset());
 
         assertEquals("Denentoshi Line", doc.getSection(0).getListBlock(0).getListElement(2).getSentence(0).getContent());
         assertEquals(2, doc.getSection(0).getListBlock(0).getListElement(2).getLevel());
@@ -295,6 +301,69 @@ public class AsciiDocParserTest {
         assertEquals(1, doc.getSection(0).getListBlock(0).getListElement(4).getLevel());
         assertEquals(7, doc.getSection(0).getListBlock(0).getListElement(4).getSentence(0).getLineNumber());
         assertEquals(2, doc.getSection(0).getListBlock(0).getListElement(4).getSentence(0).getStartPositionOffset());
+
+    }
+
+    @Test
+    public void testLabelledList() {
+        String sampleText = "= SampleDoc\n" +
+                "v0.0.2, 2015-11-17\n" +
+                ":last-update-label!:\n" +
+                "\n" +
+                "== 用語定義\n" +
+                "ユビキタス言語を定義します。\n" +
+                "\n" +
+                "Some word::\n" +
+                "なにかの意味をのせて用例をのせます。\n" +
+                "\n" +
+                "リリース::\n" +
+                "ソフトウェアを顧客に提供することです。\n" +
+                "\n" +
+                "redpen::\n" +
+                "RedPen はオープンソースの校正ツールです。RedPen は技術文書が文書規約に従って書かれているかを自動検査します。 現在の RedPen 日本語ドキュメントは十分検査されておりません。校正にはもう少々時間がかかる予定です。誤りなど見つかりましたら、https://github.com/redpen-cc/redpen-doc-ja に Issue 登録しておしらせ頂けると幸いです。";
+
+        Document doc = createFileContent(sampleText);
+        assertEquals(3, doc.getSection(1).getListBlock(0).getNumberOfListElements());
+        assertEquals("なにかの意味をのせて用例をのせます。", doc.getSection(1).getListBlock(0).getListElement(0).getSentence(0).getContent());
+        assertEquals(15, doc.getSection(1).getListBlock(0).getListElement(2).getSentence(0).getLineNumber());
+
+    }
+
+    @Test
+    public void testEscapedMarkup() {
+        String sampleText = "It is a \\*good* day.";
+        Document doc = createFileContent(sampleText);
+        Paragraph firstParagraph = doc.getSection(0).getParagraph(0);
+        assertEquals("It is a *good* day.", firstParagraph.getSentence(0).getContent());
+
+        // the offset of the 8th character (ie: *) is actually 9
+        assertEquals(9, firstParagraph.getSentence(0).getOffset(8).get().offset);
+    }
+
+    @Test
+    public void testCommentsAndTables() {
+        String sampleText = "// BLAH BLAH" +
+                "\n" +
+                "Potato" +
+                "\n" +
+                "|===\n" +
+                "|Hex |RGB |CMYK nibble\n" +
+                "\n" +
+                "|ffffff または #ffffff asd asd\n" +
+                "|[255,255,255]\n" +
+                "|[0, 0, 0, 0] または [0, 0, 0, 0%]\n" +
+                "|===\n" +
+                "\n";
+        Document doc = createFileContent(sampleText);
+
+        for (Section section : doc) {
+            for (Paragraph paragraph : section.getParagraphs()) {
+                paragraph.getSentences().forEach(sentence -> {
+                    assertEquals("Potato", sentence.getContent());
+                });
+            }
+        }
+
     }
 
     @Test
@@ -329,6 +398,55 @@ public class AsciiDocParserTest {
         }
     }
 
+    @Test
+    public void testSampleDocuments() {
+        checkForStrippedItems(createResourceContent("test_document_1.adoc", "en"));
+        checkForStrippedItems(createResourceContent("test_document_2.adoc", "en"));
+        checkForStrippedItems(createResourceContent("test_document_3.adoc", "ja"));
+    }
+
+    private void checkForStrippedItems(Document doc) {
+        // some simple illegal symbol tests
+        for (Section section : doc) {
+            for (Paragraph paragraph : section.getParagraphs()) {
+                paragraph.getSentences().forEach(sentence -> {
+                    // should be no URLS
+                    assertEquals("Possible URL: " + sentence.getContent(), -1, sentence.getContent().indexOf("http://"));
+                    assertEquals("Possible URL: " + sentence.getContent(), -1, sentence.getContent().indexOf("https://"));
+                    // should be no table markers
+                    assertEquals("Possible table symbol: " + sentence.getContent(), -1, sentence.getContent().indexOf("|="));
+                    // should be no heading symbols
+                    assertEquals("Possible header symbol: " + sentence.getContent(), -1, sentence.getContent().indexOf("----"));
+                    assertEquals("Possible header symbol: " + sentence.getContent(), -1, sentence.getContent().indexOf("****"));
+                    assertEquals("Possible header symbol: " + sentence.getContent(), -1, sentence.getContent().indexOf("==="));
+                });
+            }
+        }
+
+    }
+
+    private Document createResourceContent(String filename) {
+        return createResourceContent(filename, "en");
+    }
+
+    private Document createResourceContent(String filename, String lang) {
+        DocumentParser parser = DocumentParser.ASCIIDOC;
+
+        Document doc = null;
+        try {
+            InputStream in = new FileInputStream(this.getClass().getClassLoader().getResource("asciidoc/" + filename).getFile());
+            Configuration configuration = new Configuration.ConfigurationBuilder().setLanguage(lang).build();
+            doc = parser.parse(
+                    in,
+                    new SentenceExtractor(configuration.getSymbolTable()),
+                    configuration.getTokenizer());
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail();
+        }
+
+        return doc;
+    }
 
     private Document createFileContent(String inputDocumentString,
                                        Configuration config) {
diff --git a/redpen-core/src/test/java/cc/redpen/parser/asciidoc/AsciiDocTest.java b/redpen-core/src/test/java/cc/redpen/parser/asciidoc/AsciiDocTest.java
new file mode 100644
index 00000000..004f6f26
--- /dev/null
+++ b/redpen-core/src/test/java/cc/redpen/parser/asciidoc/AsciiDocTest.java
@@ -0,0 +1,178 @@
+/**
+ * redpen: a text inspection tool
+ * Copyright (c) 2014-2015 Recruit Technologies Co., Ltd. and contributors
+ * (see CONTRIBUTORS.md)
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package cc.redpen.parser.asciidoc;
+
+import cc.redpen.config.Configuration;
+import cc.redpen.parser.SentenceExtractor;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+
+import static org.junit.Assert.assertEquals;
+
+public class AsciiDocTest {
+
+    /**
+     * Test that ascidoc markup is erased properly
+     */
+    @Test
+    public void testModelErasure() {
+
+        Configuration configuration = new Configuration.ConfigurationBuilder().build();
+
+        String asciidocText =
+                "[[purpose]]\n" +
+                        ".Purpose\n" +
+                        "****\n" +
+                        "This contains asciidoc markup that should be {erased}.\n" +
+                        "****\n" +
+                        "\n" +
+                        "TIP: This is a tip.\n" +
+                        "\n" +
+                        "NOTE: This is a note with a http://my.domain.com/[URL in it].\n" +
+                        "\n" +
+                        "1. A list\n" +
+                        "2. More of a list\n" +
+                        "\n" +
+                        "* Bulletted\n" +
+                        "** Bulleted\n" +
+                        "\n" +
+                        "[[first,First Steps]]\n" +
+                        "== First Steps with http://asciidoc.org[AsciiDoc]\n" +
+                        "\n" +
+                        ".Inline markup\n" +
+                        "* single quotes around a phrase place 'emphasis'\n" +
+                        "* astericks around a phrase make the text *bold*\n" +
+                        "* double astericks around one or more **l**etters in a word make those letters bold\n" +
+                        "* double underscore around a __sub__string in a word emphasize that substring\n" +
+                        "* use carrots around characters to make them ^super^script\n" +
+                        "\n" +
+                        "A table\n" +
+                        "|====\n" +
+                        "table contents\n" +
+                        "|====\n" +
+                        "\n" +
+                        "Source listing\n" +
+                        "[with attributes]\n" +
+                        "----\n" +
+                        "  $ echo 'foo'\n" +
+                        "----\n";
+
+        /* the model's toString() form, showing what has been erased from the input text
+            X         = whole line erased
+            [         = block
+            nn-nnn-nn = section - listlevel - lineno
+            *         = list item
+            ·         = next character erased
+         */
+        String modelText =
+                "X 0-0-001: ·[·[·p·u·r·p·o·s·e·]·]\n" +
+                        "  0-0-002: ·.Purpose\n" +
+                        "X 0-0-003: ·*·*·*·*\n" +
+                        "  0-0-004: This contains asciidoc markup that should be ·{erased·}.\n" +
+                        "X 0-0-005: ·*·*·*·*\n" +
+                        "  0-0-006: \n" +
+                        "  0-0-007: ·T·I·P·:· This is a tip.\n" +
+                        "  0-0-008: \n" +
+                        "  0-0-009: ·N·O·T·E·:· This is a note with a ·h·t·t·p·:·/·/·m·y·.·d·o·m·a·i·n·.·c·o·m·/·[URL in it·].\n" +
+                        "  0-0-010: \n" +
+                        "  0-0-011: 1. A list\n" +
+                        "  0-0-012: 2. More of a list\n" +
+                        "  0-0-013: \n" +
+                        "  0-1-014* ·*· Bulletted\n" +
+                        "  0-2-015* ·*·*· Bulleted\n" +
+                        "  0-0-016: \n" +
+                        "X 0-0-017: ·[·[·f·i·r·s·t·,·F·i·r·s·t· ·S·t·e·p·s·]·]\n" +
+                        "  2-0-018: ·=·=· First Steps with ·h·t·t·p·:·/·/·a·s·c·i·i·d·o·c·.·o·r·g·[AsciiDoc·]\n" +
+                        "  0-0-019: \n" +
+                        "  0-0-020: ·.Inline markup\n" +
+                        "  0-1-021* ·*· single quotes around a phrase place 'emphasis'\n" +
+                        "  0-1-022* ·*· astericks around a phrase make the text ·*bold·*\n" +
+                        "  0-1-023* ·*· double astericks around one or more ·*·*l·*·*etters in a word make those letters bold\n" +
+                        "  0-1-024* ·*· double underscore around a ·_·_sub·_·_string in a word emphasize that substring\n" +
+                        "  0-1-025* ·*· use carrots around characters to make them ·^super·^script\n" +
+                        "  0-0-026: \n" +
+                        "  0-0-027: A table\n" +
+                        "X[0-0-028: ·|·=·=·=·=\n" +
+                        "X[0-0-029: ·t·a·b·l·e· ·c·o·n·t·e·n·t·s\n" +
+                        "X[0-0-030: ·|·=·=·=·=\n" +
+                        "  0-0-031: \n" +
+                        "  0-0-032: Source listing\n" +
+                        "X 0-0-033: ·[·w·i·t·h· ·a·t·t·r·i·b·u·t·e·s·]\n" +
+                        "X[0-0-034: ·-·-·-·-\n" +
+                        "X[0-0-035: · · ·$· ·e·c·h·o· ·'·f·o·o·'\n" +
+                        "X[0-0-036: ·-·-·-·-\n";
+
+        Model model = new Model(new SentenceExtractor(configuration.getSymbolTable()));
+
+        // get the parser to populate the model
+        new AsciiDocParser().populateModel(model, new ByteArrayInputStream(asciidocText.getBytes()));
+
+        assertEquals(modelText, model.toString());
+    }
+
+
+    /**
+     * Test the line class
+     */
+    @Test
+    public void testLine() {
+        String testLine = "A line *with* an [enclosure] and one with a [enclosure,with a description] " +
+                "and then an URL like http://fred.fish/";
+
+        String[] expectedResults = new String[]{
+                "  0-0-001: A line *with* an [enclosure] and one with a [enclosure,with a description] and then an URL like http://fred.fish/",
+                "  0-0-001: A line *with* an ·[·e·n·c·l·o·s·u·r·e·] and one with a ·[·e·n·c·l·o·s·u·r·e·,·w·i·t·h· ·a· ·d·e·s·c·r·i·p·t·i·o·n·] and then an URL like http://fred.fish/",
+                "  0-0-001: A line *with* an [enclosure] and one with a [enclosure,with a description] and then an URL like ·h·t·t·p·:·/·/·f·r·e·d·.·f·i·s·h·/",
+                "  0-0-001: A line ·*with·* an [enclosure] and one with a [enclosure,with a description] and then an URL like http://fred.fish/",
+                "  0-0-001: A line *with* an ·[enclosure·] and one with a ·[enclosure,with a description·] and then an URL like http://fred.fish/",
+                "  0-0-001: A line *with* an ·[enclosure·] and one with a ·[·e·n·c·l·o·s·u·r·e·,with a description·] and then an URL like http://fred.fish/"
+        };
+        Line line;
+
+        // this erases nothing since the close tag is not found
+        line = new Line(testLine, 1);
+        line.eraseEnclosure("[", "]]", Line.EraseStyle.All);
+        assertEquals(expectedResults[0], line.toString());
+
+        // すべてが消去されますか？
+        line = new Line(testLine, 1);
+        line.eraseEnclosure("[", "]", Line.EraseStyle.All);
+        assertEquals(expectedResults[1], line.toString());
+
+        // Test a string of delimiters as an ending tag
+        line = new Line(testLine, 1);
+        line.eraseEnclosure("http://", " ,[", Line.EraseStyle.CloseMarkerContainsDelimiters);
+        assertEquals(expectedResults[2], line.toString());
+
+        // Test inline markup erasure
+        line = new Line(testLine, 1);
+        line.eraseEnclosure("*", "*", Line.EraseStyle.InlineMarkup);
+        assertEquals(expectedResults[3], line.toString());
+
+        // Test just removing the markers
+        line = new Line(testLine, 1);
+        line.eraseEnclosure("[", "]", Line.EraseStyle.Markers);
+        assertEquals(expectedResults[4], line.toString());
+
+        // Test just removing the delimter tags
+        line = new Line(testLine, 1);
+        line.eraseEnclosure("[", "]", Line.EraseStyle.PreserveLabel);
+        assertEquals(expectedResults[5], line.toString());
+    }
+}
diff --git a/redpen-core/src/test/resources/asciidoc/test_document_1.adoc b/redpen-core/src/test/resources/asciidoc/test_document_1.adoc
new file mode 100644
index 00000000..7df7ba2a
--- /dev/null
+++ b/redpen-core/src/test/resources/asciidoc/test_document_1.adoc
@@ -0,0 +1,245 @@
+Asciidoctor Demo from AsciiDoctor, with some Japanese added
+===========================================================
+////
+Big ol' comment
+
+sittin' right 'tween this here title 'n header metadata
+////
+Dan Allen <thedoc@asciidoctor.org>
+:description: A demo of Asciidoctor. This document +
+              exercises numerous features of AsciiDoc +
+              to test Asciidoctor compliance.
+:library: AsciiDoctor
+:plus: &#43;
+
+[role='lead']
+This is a demonstration of {library}. And this is the preamble of this document.
+
+[[purpose]]
+.Purpose
+****
+This document exercises many of the features of AsciiDoc to test the {library} implementation.
+****
+
+TIP: If you want the output to look familiar, copy (or link) the AsciiDoc stylesheet, asciidoc.css, to the output directory.
+
+NOTE: Items marked with TODO are either not yet supported or a work in progress.
+
+[[first,First Steps]]
+== First Steps with http://asciidoc.org[AsciiDoc]
+
+.Inline markup
+* single quotes around a phrase place 'emphasis'
+* astericks around a phrase make the text *bold*
+* double astericks around one or more **l**etters in a word make those letters bold
+* double underscore around a __sub__string in a word emphasize that substring
+* use carrots around characters to make them ^super^script
+* use tildes around characters to make them ~sub~script
+ifdef::basebackend-html[]
+* to pass through +++<u>HTML</u>+++ directly, surround the text with triple plus
+endif::basebackend-html[]
+ifdef::basebackend-docbook[]
+* to pass through +++<constant>XML</constant>+++ directly, surround the text with triple plus
+endif::basebackend-docbook[]
+
+// separate two adjacent lists using a line comment (only the leading // is required)
+
+- characters can be escaped using a {backslash}
+* for instance, you can escape a quote inside emphasized text like 'Here\'s Johnny!'
+- you can safely use reserved XML characters like <, > and &, which are escaped when rendering
+- force a space{sp}between inline elements using the \{sp} attribute
+- hold text together with an intrinsic non-breaking{nbsp}space attribute, \{nbsp}
+- handle words with unicode characters like in the name Gregory Romé
+- claim your copyright (C), registered trademark (R) or trademark (TM)
+
+You can write text http://example.com[with inline links], optionally{sp}using an explicit link:http://example.com[link prefix]. In either case, the link can have a http://example.com?foo=bar&lang=en[query string].
+
+If you want to break a line +
+just end it in a {plus} sign +
+and continue typing on the next line.
+
+=== Lists Upon Lists
+
+.Adjacent lists
+* this list
+* should join
+
+* to have
+* four items
+
+[[numbered]]
+.Numbered lists
+. These items
+. will be auto-numbered
+.. and can be nested
+. A numbered list can nest
+* unordered
+* list
+* items
+
+.Statement
+I swear I left it in 'Guy\'s' car. Let\'s go look for it.
+
+[[defs]]
+term::
+  definition
+line two
+[[another_term]]another term::
+
+  another definition, which can be literal (indented) or regular paragraph
+
+This should be a standalone paragraph, not grabbed by the definition list.
+
+[[nested]]
+* first level
+written on two lines
+* first level
++
+....
+with this literal text
+....
++
+** second level
+*** third level
+- fourth level
+* back to +
+first level
+
+// this is just a comment
+
+Let's make a horizontal rule...
+
+'''
+
+then take a break.
+
+////
+We'll be right with you...
+
+after this brief interruption.
+////
+
+== We're back!
+
+Want to see a image:tiger.png[Tiger]?
+
+Do you feel safer with the tiger in a box?
+
+.Tiger in a box
+image::tiger.png[]
+
+include::include.asciidoc.txt[]
+
+.Asciidoctor usage example, should contain 3 lines
+[source, ruby]
+----
+doc = Asciidoctor::Document.new("*This* is it!", :header_footer => false)
+
+puts doc.render
+----
+
+// FIXME: use ifdef to show output according to backend
+Here's what it outputs (using the built-in templates):
+
+....
+<div class="paragraph">
+  <p><strong>This</strong> is it!</p>
+</div>
+....
+
+=== Tables
+
+
+=== Tables
+
+.An example table
+[options="header,footer"]
+|=======================
+|Col 1|Col 2      |Col 3
+|1    |Item 1     |a
+|2    |Item 2     |b
+|3    |Item 3     |c
+|6    |Three items|d
+|=======================
+
+.CSV data, 15% each column
+[format="csv",width="60%",cols="4"]
+[frame="topbot",grid="none"]
+|======
+1,2,3,4
+a,b,c,d
+A,B,C,D
+|======
+
+=== ``Quotes''
+
+____
+AsciiDoc is 'so' *powerful*!
+____
+
+This verse comes to mind.
+
+[verse]
+La la la
+
+Here's another quote:
+
+[quote, Sir Arthur Conan Doyle, The Adventures of Sherlock Holmes]
+____
+When you have eliminated all which is impossible, then whatever remains, however improbable, must be the truth.
+____
+
+Getting Literal [[literally]]
+-----------------------------
+
+ Want to get literal? Just prefix a line with a space (just one will do).
+
+....
+I'll join that party, too.
+....
+
+We forgot to mention in <<numbered>> that you can change the numbering style.
+
+.. first item (yeah!)
+.. second item, looking `so mono`
+.. third item, +mono+ it is!
+
+// This attribute line will get reattached to the next block
+// despite being followed by a trailing blank line
+[id='wrapup']
+
+== Wrap-up
+
+NOTE: AsciiDoc is quite cool, you should try it!
+
+[TIP]
+.Info
+=====
+Go to this URL to learn more about it:
+
+* http://asciidoc.org
+
+Or you could return to the xref:first[] or <<purpose,Purpose>>.
+=====
+
+= Sample from @kyonmm
+
+== 用語定義
+ユビキタス言語を定義します。
+
+Some word::
+なにかの意味をのせて用例をのせます。
+
+リリース::
+ソフトウェアを顧客に提供することです。
+
+redpen::
+RedPen はオープンソースの校正ツールです。RedPen は技術文書が文書規約に従って書かれているかを自動検査します。 現在の RedPen 日本語ドキュメントは十分検査されておりません。校正にはもう少々時間がかかる予定です。誤りなど見つかりましたら、https://github.com/redpen-cc/redpen-doc-ja に Issue 登録しておしらせ頂けると幸いです。
+
+Here's a reference to the definition of <<another_term>>, in case you forgot it.
+
+[NOTE]
+One more thing. Happy documenting!
+
+[[google]]When all else fails, head over to <http://google.com>.
+
diff --git a/redpen-core/src/test/resources/asciidoc/test_document_2.adoc b/redpen-core/src/test/resources/asciidoc/test_document_2.adoc
new file mode 100644
index 00000000..21e82b08
--- /dev/null
+++ b/redpen-core/src/test/resources/asciidoc/test_document_2.adoc
@@ -0,0 +1,106 @@
+AsciiDoc User Guide
+===================
+Stuart Rackham <srackham@gmail.com>
+:Author Initials: SJR
+:toc:
+:icons:
+:numbered:
+:website: http://www.methods.co.nz/asciidoc/
+
+AsciiDoc is a text document format for writing notes, documentation,
+articles, books, ebooks, slideshows, web pages, blogs and UNIX man
+pages.
+
+.This document
+**********************************************************************
+This is an overly large document, it probably needs to be refactored
+into a Tutorial, Quick Reference and Formal Reference.
+
+If you're new to AsciiDoc read this section and the <<X6,Getting
+Started>> section and take a look at the example AsciiDoc (`*.txt`)
+source files in the distribution `doc` directory.
+**********************************************************************
+
+
+Introduction
+------------
+AsciiDoc is a plain text human readable/writable document format that
+can be translated to DocBook or HTML using the asciidoc(1) command.
+
+asciidoc(1) comes with a set of configuration files to translate
+AsciiDoc articles, books and man pages to HTML or DocBook backend
+formats.
+
+.My AsciiDoc Itch
+**********************************************************************
+DocBook has emerged as the de facto standard Open Source documentation
+format. But DocBook is a complex language, the markup is difficult to
+read and even more difficult to write directly -- I found I was
+and fixing syntax errors, than I was writing the documentation.
+**********************************************************************
+
+
+[[X6]]
+Getting Started
+---------------
+Installing AsciiDoc
+~~~~~~~~~~~~~~~~~~~
+See the `README` and `INSTALL` files for install prerequisites and
+procedures. Packagers take a look at <<X38,Packager Notes>>.
+
+[[X11]]
+Example AsciiDoc Documents
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+The best way to quickly get a feel for AsciiDoc is to view the
+AsciiDoc web site and/or distributed examples:
+
+- Take a look at the linked examples on the AsciiDoc web site home
+  page {website}.  Press the 'Page Source' sidebar menu item to view
+  corresponding AsciiDoc source.
+- Read the `*.txt` source files in the distribution `./doc` directory
+  along with the corresponding HTML and DocBook XML files.
+
+
+AsciiDoc Document Types
+-----------------------
+There are three types of AsciiDoc documents: article, book and
+manpage.
+
+Use the asciidoc(1) `-d` (`--doctype`) option to specify the AsciiDoc
+document type -- the default document type is 'article'.
+
+article
+~~~~~~~
+Used for short documents, articles and general documentation.  See the
+AsciiDoc distribution `./doc/article.txt` example.
+
+AsciiDoc defines standard DocBook article frontmatter and backmatter
+<<X93,section markup templates>> (appendix, abstract, bibliography,
+glossary, index).
+
+book
+~~~~
+Books share the same format as articles, with the following
+differences:
+
+- The part titles in multi-part books are <<X17,top level titles>>
+  (same level as book title).
+- Some sections are book specific e.g. preface and colophon.
+
+Book documents will normally be used to produce DocBook output since
+DocBook processors can automatically generate footnotes, table of
+contents, list of tables, list of figures, list of examples and
+indexes.
+
+AsciiDoc defines standard DocBook book frontmatter and backmatter
+<<X93,section markup templates>> (appendix, dedication, preface,
+bibliography, glossary, index, colophon).
+
+.Example book documents
+Book::
+  The `./doc/book.txt` file in the AsciiDoc distribution.
+
+Multi-part book::
+  The `./doc/book-multi.txt` file in the AsciiDoc distribution.
+
+
diff --git a/redpen-core/src/test/resources/asciidoc/test_document_3.adoc b/redpen-core/src/test/resources/asciidoc/test_document_3.adoc
new file mode 100644
index 00000000..c70582cf
--- /dev/null
+++ b/redpen-core/src/test/resources/asciidoc/test_document_3.adoc
@@ -0,0 +1,897 @@
+// ドキュメントの整形
+文書には内容と体裁があります。体裁とは、書体やページレイアウトなど文書の仕上りを規定する概念です。同じ内容の文書でも、その体裁が異なれば与える印象は全く異なります。伝わる文書を書くためには、はっきりとした内容があることはもちろん、体裁も重要です。
+
+// 以下はあくまで AsciiDoctor PDF に対するスタイル調整について記述。 AciiDoctor で HTML を出力する際には、CSS を利用します。
+
+AsciiDoctorはデフォルト体裁を持っています。デフォルトの体裁は、ほとんどの場合に適当です。しかしTPOによっては不適当であったり、または単に好みと合わなかったり、などの理由で変更したいことがあります。そのような場合、AsciiDoctorに体裁の仕様を指示することで、思い通りの仕上がりにできます。
+
+体裁には多くの設定項目があります。たとえば文書のサイズ（A4、USレターなど）やフォントからはじまり、フッターの大きさなどは体裁の設定項目の一部となります。
+
+AsciiDoctorでは各体裁はスタイルと呼ばれ、文書本体とは独立したスタイルファイルに保存されます。以下、スタイルファイルを見ていきましょう。
+
+********************************
+コラム: 体裁の指定方法
+
+一般的に、体裁の設定方法はアプリケーションによって二種類に分かれます。一つは文書内で体裁を指示するものです。もう一つは文書と体裁の設定を別々に行うものです。
+
+文書内で体裁を指示するアプリケーションに、Microsoft Wordがあります。このような文書作成ツールでは、体裁を文書中に一つづつ指示する必要があります。体裁指示のたびに即時反映されることから、編集者が最終成果物の状態を把握しやすいという利点があります。一方で、体裁指定を統一しにくいという欠点が存在します。編集者が表や画像などの部品を文書に追加するたびに、体裁も設定しないといけないため、設定にバラつきや誤りが含まれやすいのです。
+
+文書と体裁の設定を別々に行うアプリケーションに、AsciiDoctorやSphinxがあります。体裁を文書本体と独立させておくことで、本文に手を入れることなく体裁だけを独立して調整できます。このようなシステムの欠点に、実際の仕上がりがすぐに把握できないという点があります。なぜなら本文と体裁が分離しているので、体裁を適用するまでどのような仕上りになるのか見ることができないのです。
+
+一方、文書と体裁の設定を別々に行うと、本文を簡潔に保てるという利点があります。本文が簡潔に保たれていることで、文書内容へ自然と注意が向くようになります。さらに、文書と体裁の設定を別々に行うシステムでは、本文の編集は環境を選ばず、使いなれたエディタやツールがそのまま使えます。製品マニュアルや書籍の執筆など、体裁の一貫性が重要視される場面では、体裁は本文と分離して管理されることが少なくありません。
+********************************
+
+==== スタイルファイル
+スタイルファイルは多くの設定項目を含みます。設定項目は、大きくいくつかの部（ブロック）に分けられます。文書のスタイリングという観点では、書体指定・体裁指定・表紙の指定・柱の指定ブロックが重要です。本節ではこれらのブロックについて解説します。
+
+スタイルファイルは複雑です。そのため解説にあたって、実例があると分かりやすいです。以下、筆者が本書のために作成したスタイルファイル（Kindle風サンプル）を例として使用します。例の完全版を https://github.com/continuous-manual-writing/style-sample[本書サンプルリポジトリ] に収録しました。AsciiDoctor PDF 1.5.0.alpha.9 以降で実際に使用できます。試してみてください。
+
+********************************
+コラム: ここにもYAML形式
+
+スタイルファイルはYAML形式です。YAML形式は、Travisの設定ファイルでも採用されています。
+********************************
+
+[id="sec-style-yaml-blocks"]
+==== 設定項目の階層構造
+
+// TBD: 階層的な名前について不明瞭。
+各節の解説に入る前に、設定項目の書式について解説します。設定項目には全て、階層的な名前がついています。階層的な名前とは、大ブロック名→小ブロック名→...→項目名が全てアンダーバー ```_``` で繋げられて記述されている名前です。階層構造を設定項目名に含めることによって、各項目が持つ役割が把握しやすくなっています。
+
+たとえば基本文字サイズを規定する設定項目に「base_font_size_large」（後述）がありますが、これは「base」→「font」→「size」→「large」という構造であることが分かります。以下基本文字サイズの設定例となります。
+
+[source, yaml]
+----
+base_font_size_small: 8
+base_font_size_large: 14
+----
+
+// 「接頭辞」とは。
+同一の階層に属す設定項目を複数記述する際、階層名＋「:」と書いて一旦行を打ち切ることで、以後字下げされている範囲において階層名を省略できます。
+
+さきほどの基本（base）文字サイズの規定例を見ましょう。baseブロックを作って字下げすることで、次のように書けます。
+
+[source, yaml]
+----
+base:
+  font_size_small: 8
+  font_size_large: 14
+----
+
+baseについてまとまりました。
+
+fontがまだ共通しています。まとめてみましょう。具体的には以下のように記述できます。
+
+[source, yaml]
+----
+base:
+  font:
+    size_small: 8
+    size_large: 14
+----
+
+場合によっては、sizeでさらにまとめたいことがあります。以下sizeでまとめた場合の設定となります。
+
+[source, yaml]
+----
+base:
+  font:
+    size:
+      small: 8
+      large: 14
+----
+
+設定項目名は常に階層構造を持つため、字下げで表現された階層はアンダーバーに書きかえられます。たとえば上記の設定は次のようにまとめられます。
+
+[source, yaml]
+----
+base:
+  font_size:
+    small: 8
+    large: 14
+----
+
+********************************
+コラム: 設定項目の全貌
+
+https://github.com/asciidoctor/asciidoctor-pdf/blob/master/docs/theming-guide.adoc[AsciiDoctor PDF Theming Guide] では、スタイルファイルの書き方から始まり全設定項目についての解説まで詳細な情報が掲載されています。実際の用例については、AsciiDoctorに同梱されているデフォルトスタイルファイルも役に立ちます。自分でスタイルファイルを書く際にはぜひ参考にしてみてください。
+********************************
+
+==== 設定値の展開・演算・組込み関数
+
+スタイルファイルの設定項目について、もう一つ触れておきたいことがあります。それは、設定値も解釈されるということです。AsciiDoctorが解釈することにより、別の設定値を引用（展開）したり、簡単な数式を演算（評価）を行なわせたりすることができます。
+
+設定値を展開するには、＄に続き展開したい設定値名を書きます。
+
+数式の記述には小カッコを含めた四則演算子の他に、組込み関数も使えます。組込み関数とは、主に端数処理を行なうためにAsciiDoctorが持っている関数群で、以下の3つがあります。
+
+[format="csv", options="header"]
+|===
+関数名, 戻り値
+floor(x), xについて小数点以下を切り捨てた値
+round(x), xについて小数点以下を四捨五入した値
+ceil(x), xについて小数点以下を切り上げた値
+|===
+
+以下は例です。第1見出しレベル文字サイズ（heading_h1_font_size）へ、基本文字サイズ（base_font_size）を2.6倍して小数点以下を切り捨てた値を入れています。
+
+[source, yaml]
+----
+heading:
+  h1_font_size: floor($base_font_size * 2.6)
+----
+
+お待たせしました。それでは各節の解説に入りましょう。
+
+********************************
+コラム: 二重定義や設定項目ミスに注意
+
+同じ設定項目について2度以上指定を重ねると、指定が上書かれます。対応する設定項目が存在しない場合は、単純に無視されます。いずれの場合もエラーや警告が出力されることはありません。ご自身でスタイルを定義される際には、設定項目名を打ち間違えたりしないよう、細心の注意を払うようにしてください。
+********************************
+
+==== 書体指定
+文書には通常、明朝体やゴシック体などの複数の書体が混在します。書体とは、ある意匠のもとに字体をまとめたものを指します。字体とは、各文字の外観を決定する字形（グリフ）の集合です。字体には標準体、ボールド体、イタリック体、ボールドイタリック体の4つがあります。
+
+標準体とは、何の修飾もされていない字形を持つ字体で、基本となる字体です。ボールド体とは、標準体の各字形を太めに表記した字体です。イタリック体は、標準体の各字形を少し傾けて表記したものです。ボールドイタリック体は、各字形にボールドおよびイタリックの各修飾を同時に施したものです。
+
+ボールド体・イタリック体・ボールドイタリック体の各体とも、見ための際立ちを生かして、重要な概念を強調したり引用したりするために使われます。本書では以下、装飾がない標準体を標準字体、装飾を伴なった各体を派生字体と呼称することにします。
+
+AsciiDoctorでは、書体はフォントファミリとして扱われます。フォントファミリの定義は、基本字体および派生3字体に対して、それぞれフォントファイルを指定することによって行なわれます。
+
+NOTE: フォントファイルには多数の形式がありますが、AsciiDoctorで使うにあたってはTrueTypeフォント（*.ttf, *.ttc）を選んでおくのが良いでしょう。
+
+書体関連の定義は全て、fontブロック以下に書かれます。フォントファミリの定義は、catalogブロックによって行なわれます。フォントファミリの定義は、fontブロックの中で最初に行なわれる設定です。
+
+[source, yaml]
+----
+font:
+  catalog:
+    IPA:
+      normal: ipaexm.ttf
+      bold: ipaexm.ttf
+      italic: ipaexm.ttf
+      bold_italic: ipaexm.ttf
+    ...
+----
+
+catalogブロック冒頭の、IPA明朝ファミリの定義です。IPA明朝系TrueType書体4つをまとめて、IPAファミリとして定義しています。ここで、シャープ ```#``` から始まる行はいわゆるコメントで、メモなど好きな内容を書いておけます。この後に、セリフ体としてのNoto Serifファミリ、及びサンセリフ体としてのM+ 1mnファミリの定義が続きます。
+
+ところで定義したフォントファミリに、使いたい文字が含まれていないような場合があります。その場合においても、文字がそのまま欠けてしまったりしては体裁上よくありません。この問題はあらかじめどんな文字も含むフォントファミリを定義しておいて、対応する文字がなかった場合に使うことで解決できます。AsciiDoctorでは、このようなフォントファミリを**フォールバックフォントファミリ**と呼んでいます。
+
+フォールバックフォントファミリは、fallbacksで定義されます。以下はフォールバックフォントファミリとして、IPA明朝ファミリを使用する例です。
+
+[source, yaml]
+----
+font:
+  catalog:
+     ...
+  fallbacks:
+    - IPA
+----
+
+先頭にハイフン ```- ``` が付いていることに注意してください。これはリスト形式と呼ばれるものです。リスト形式にすることで、好きなフォントファミリを好きな優先順位で、組み合わせて使用できるようにしています。フォールバックフォントファミリを追加したい場合は、既にあるものにならって、ハイフン ```- ``` を先頭に付けてフォントファミリ名を書き、行頭を揃えて追記してやると良いでしょう。
+
+********************************
+コラム: カスタムフォントを使用するには
+
+AsciiDoctorには、いくつかのフォントファイルが標準バンドルされています。自分で用意した書体を使用したい場合には、使いたい書体ごとにフォントファミリを定義します。その後、必要なフォントファイル全てを一箇所に集め、AsciiDoctorへそのパスを-aオプション（後述）で指定します。以下、fonts/以下のフォントファイル群を使用するように指示する例です。
+
+----
+$ asciidoctor-pdf -a pdf-style=style.yml -a pdf-fonts-dir=fonts content.adoc
+----
+********************************
+
+********************************
+コラム: フォールバックフォントフォミリで使用できる字体
+
+あるフォントファミリが、フォールバックフォントファミリとして使用される場合、通常字体のみが使用されます。ボールド体・イタリック体・ボールドイタリック体は使用されません。
+********************************
+
+==== 体裁指定: ページ設定
+AsciiDoctorのスタイルファイルには、ページの体裁を指定するpageブロックがあります。このpageブロックは、背景色・背景画像・向き・マージン・用紙サイズの5つの要素を指定するものです。pageブロックで使用できる設定項目は、以下のものがあります。
+
+ * page_background_color
+
+ページ背景色を指定します。何も指定しない場合は白になります。
+
+ * page_background_image
+
+ページ背景画像のパスを指定します。スタイルファイルからの相対パス、または絶対パスで指定します。
+
+ * page_layout
+
+文書の向きを指定します。縦向きにするにはportrait、横向きにするにはlandscapeと指定します。
+
+ * page_margin
+
+マージンを指定します。左・上・右・下の順に大カッコで括って指定します。指定には単位を付けられます。例えばインチを使用する場合には、○in（インチ）と指定します。単位を省略すると、pt（ポイント）が使用されます。
+
+ * page_size
+
+用紙サイズを指定します。A4など、用紙サイズ名で指定することが一般的です。数値でも指定できます。数値でサイズを指定する場合には、大カッコで括って横・縦と単位付きで指定します。数値に単位を忘れないように注意してください。
+
+以下、pageブロックの設定例となります。pageで字下げしているため、各設定項目ともpage_が省略されていることに注意してください。
+
+[source, yaml]
+----
+page:
+  # ページ背景は白
+  background_color: ffffff
+  # ページの向きは縦向き
+  layout: portrait
+  # マージンは左右0.75インチ、上下1インチ
+  margin: [0.75in, 1in, 0.75in, 1in]
+  # 用紙サイズはA4を使用
+  size: A4
+----
+
+
+********************************
+コラム: 色指定の方法
+
+文書の体裁にとって、色は欠かせない要素です。コンピュータにおける色の指定は、光の三原色（レッド・グリーン・ブルー）の混合比を記述していく形式が一般的です。AsciiDoctorではRGB形式およびHex形式が該当します。
+
+一方で光の三原色で記述された色は、印刷する場合に調色が難しくなる傾向があります。印刷では色の三原色（シアン・マゼンタ・イエロー）が基準になっているためです。そこで印刷工程が関係するワークフローでは、一般的にCMYK形式と呼ばれる独特な色指定方法が使われています。CMYK形式とは色を、色の三原色にブラック（キー）を加えた4色の混合比で記述する形式です。
+
+AsciiDoctorは、CMYK形式による色指定もサポートしています。
+
+本文の例の場合、以下のように指定することができます。
+
+[format="csv", options="header"]
+|===
+Hex, RGB, CMYK
+"ffffff または #ffffff", "[255,255,255]", "[0, 0, 0, 0] または [0, 0, 0, 0%]"
+|===
+********************************
+
+==== 体裁指定: 共通書式設定
+AsciiDoctorのスタイルファイルには、共通書式指定ブロック（base）があります。共通書式指定ブロックは、主に本文の体裁を指定します。中でも重要な設定項目は、文字揃え・フォント関連（色・ファミリ・サイズ）・行の高さです。共通書式設定ブロックで使える設定項目は、以下のものがあります。
+
+ * base_align
+
+alignは字揃え方法を指定する項目です。次の値のうちのいずれかを取ります。
+
+[format="csv", options="header"]
+|===
+設定値, 字揃え方法
+left, 左
+center, 中央
+right, 右
+justify, 両端
+|===
+
+justifyによる両端揃え指定は、半角スペースで語句を区切ることをしない和文ではあまりおすすめできません。
+
+欧文の例では両端揃えを指定しているケースを良く見かけますが、現状のAsciiDoctorによる両端揃えは半角スペースを単語区切りとして、空隙を調整するのみです。禁則処理や行レイアウトの評価は一切行なわれません。そのため半角スペースがあると、どのような文脈で現われたのかを問わず、単語を行の両端に吹き飛ばしてしまいます。逆に半角スペースが存在しなければ、空隙の調整は発生せず、行端がガタガタになってしまいます。
+
+つまり和文では意味をなさないどころか、レイアウトを不安定化させてしまう要因になるのです。
+
+ * base_font_color
+
+文字色を指定します。
+
+ * base_font_family
+
+使用するフォントファミリを指定します。
+
+ * base_font_size
+
+基本文字サイズを指定します。
+
+ * base_line_height_length
+
+行の高さを直接指定します。文字サイズとは独立しているので、ページに行を詰め込んだり、逆にゆったり組んだりという工夫ができるようになっています。
+
+ * base_line_height
+
+行の高さを、文字サイズに対する倍数で指定します。先のbase_line_height_lengthとセットに指定する必要がありますが、互いに食い違っていてはいけません。どちらかは必ず数式で指定しましょう。
+
+ * base_font_size_large
+
+大きめの文字が使用する、文字サイズを指定します。
+
+ * base_font_size_small
+
+小さめの文字が使用する、文字サイズを指定します。
+
+ * base_font_size_min
+
+最小文字サイズを指定します。
+
+ * base_font_style
+
+字体を指定します。
+
+以下、共通書式ブロックの設定例です。baseで字下げしているため、各設定項目ともbase_が省略されていることに注意してください。
+
+[source, yaml]
+----
+base:
+  # 左揃え
+  align: left
+  # 文字色は暗い灰色
+  font_color: 333333
+  # フォントファミリはNoto Serif
+  font_family: Noto Serif
+  # 基本文字サイズは10.5ポイント
+  font_size: 10.5
+  # 行の高さは18ポイント
+  line_height_length: 18
+  # 行高の文字サイズに対する倍数指定は数式で
+  line_height: $base_line_height_length / $base_font_size
+  # 大きめの文字サイズは基本文字サイズの1.25倍
+  font_size_large: round($base_font_size * 1.25)
+  # 小さめの文字サイズは基本文字サイズの0.85倍
+  font_size_small: round($base_font_size * 0.85)
+  # 最小文字サイズは基本文字サイズの0.75倍
+  font_size_min: $base_font_size * 0.75
+  # 標準字体を使う
+  font_style: normal
+  ...
+----
+
+==== 体裁の指定: 特殊書式設定
+
+本節では、特殊書式を指定するブロック群について解説します。特殊書式には、リンク・書式設定済みテキスト（リテラル）・見出しなどがあります。
+
+これらは全て各々の書式範囲において、共通書式指定を上書きするように作用します。順に見ていきましょう。
+
+===== リンク
+
+AsciiDocで作成された章や節、外部サイトへのリンクは、AsciiDoctor PDFによってハイパーリンクとしてPDF内に埋め込まれます。
+
+この記述のために使用される体裁が、リンク用書式です。リンク用書式の指定は、linkブロックが対応します。linkブロックが指定する項目には、以下のものがあります。
+
+ * link_font_color
+
+リンク用文字色を指定します。
+
+ * link_font_family
+
+リンク用フォントファミリを指定します。
+
+ * link_font_size
+
+リンク用文字サイズを指定します。
+
+ * link_font_style
+
+リンク用字体を指定します。
+
+以下は文字色のみを指定する、リンク用書式指定ブロックの例です。linkで字下げしているので、font_colorと表記するだけで十分であることに注意してください（<<sec-style-yaml-blocks>>を参照）。
+
+[source, yaml]
+----
+link:
+  # リンク文字色は青色
+  font_color: #002FA7
+----
+
+===== リテラル
+
+書式設定済みテキスト（リテラル）とはプログラムコードなど、「ありのまま」書き表したいテキストのことを指します。
+
+ありのまま、とはタブ・スペース・特殊文字などが入っていてもフォーマッティングを行なわずにそのまま記載するという意味です。一般的にこのようなテキストの組版には、外観を損なわないように等幅系フォントファミリを使用するなどの配慮が行なわれます。
+
+このために使用される体裁がリテラル用書式です。リテラル用書式指定ブロックはliteralが対応します。literalブロックで指定する項目には以下のものがあります。
+
+ * literal_font_color
+
+リテラル用文字色を指定します。
+
+ * literal_font_family
+
+リテラル用フォントファミリを指定します。
+
+ * literal_font_size
+
+リテラル用文字サイズを指定します。
+
+ * literal_font_style
+
+リテラル用字体を指定します。
+
+以下は文字色を指定するリテラル用書式指定ブロックの例です。literalで字下げしているため、font_colorと書くだけで良くなっていることに注意してください。
+
+[source, yaml]
+----
+literal:
+  # リテラルは赤紫色
+  font_color: b12146
+----
+
+===== 見出し
+
+見出しは文書の構成要素です。見出しを効果的に使うことで文書の構造が際立ち、必要な情報が素早く届くようになります。AsciiDocの見出しは第1〜6レベルまであり、それぞれ重要度に従って使い分けられます。
+
+見出しの体裁は、見出し書式指定ブロック（heading）で定義されます。見出し書式指定ブロックの指定項目は、見出し全体に共通した書式指定項目を含む部分（共通部分）と、見出しレベル1〜6に特化した書式指定項目を含む部分（特化部分）とで構成されます。特化部分で指定された書式は、該当する見出しレベルにおいて共通部分指定を上書きします。
+
+共通部分に指定する設定項目は、以下のものがあります。
+
+ * heading_font_color
+
+見出し共通の文字色を指定します。
+
+ * heading_font_family
+
+見出し共通のフォントファミリを指定します。
+
+ * heading_font_size
+
+見出し共通の文字サイズを指定します。
+
+ * heading_font_style
+
+見出し共通の字体を指定します。
+
+ * heading_line_height
+
+見出し共通の行高を文字サイズの倍数で指定します。
+
+ * heading_margin_top
+
+見出し共通の上部マージンを指定します。先行する本文との間を取るために使用されます。
+
+ * heading_margin_bottom
+
+見出し共通の下部マージンを指定します。後続する本文との間を取るために使用されます。
+
+また特化部分に指定する設定項目には、以下のものがあります。○には1〜6が入り、それぞれAsciiDocの見出しレベルの1〜6に対応します。
+
+ * heading_h○_font_color
+
+当該見出しレベルの文字色を指定します。
+
+ * heading_h○_font_family
+
+当該見出しレベルのフォントファミリを指定します。
+
+ * heading_h○_font_size
+
+当該見出しレベルの文字サイズを指定します。
+
+ * heading_h○_font_style
+
+当該見出しレベルの字体を指定します。
+
+以下は見出し書式指定の例です。初めの共通部分では、文字関連のスタイル指定（色・フォントファミリ・文字太さ・文字サイズ）およびマージン指定を行なっています。続く特化部分では、第1〜第6の各見出しレベルについて、文字サイズを指定しています。headingで字下げしているので、各設定項目名ともheading_が省略されていることに注意してください。
+
+[source, yaml]
+----
+heading:
+  # 共通部分
+  # 文字色は変更しない
+  font_color: $base_font_color
+  # フォントファミリも変更しない
+  font_family: $base_font_family
+  # 字体は太字
+  font_style: bold
+  # サイズは17pt
+  font_size: 17
+  # 行高はフォントサイズの1.2倍
+  line_height: 1.2
+  # 上マージンとして、行送り幅の0.2倍を確保
+  margin_top: $vertical_rhythm * 0.2
+  # 下マージンとして、行送り幅そのままを確保
+  margin_bottom: $vertical_rhythm
+
+  # 第1〜6レベルヘッダ特化部分
+  h1_font_size: floor($base_font_size * 2.6)
+  h2_font_size: floor($base_font_size * 2.6)
+  h3_font_size: round($base_font_size * 2.15)
+  h4_font_size: round($base_font_size * 1.6)
+  h5_font_size: $base_font_size_large
+  h6_font_size: $base_font_size
+----
+
+==== 表紙の指定
+// TBD
+表紙の体裁を指定するブロックは若干特殊です。表紙である以上、本文の代わりにロゴ・タイトル・サブタイトル・作者・版といった情報を分かりやすく目立つように記載する必要があるためです。分かりやすく目立つように記載するためには、自然と行ベースではなく自由に構成されてくるようになります。
+
+表紙の体裁指定は、title_pageブロックによって行なわれます。title_pageブロックが指定する設定項目は、以下のものがあります。
+
+ * title_page_align
+
+表紙における字揃え方法を指定する項目です。次の値のうちのいずれかを取ります。
+
+[format="csv", options="header"]
+|===
+設定値, 字揃え方法
+left, 左
+center, 中央
+right, 右
+justify, 両端
+|===
+
+両端揃えを指示するjustifyについては、よほど長いタイトルを付けない限り意味がないことに注意してください。
+
+ * title_page_background_color
+
+表紙の背景色を指定します。
+
+ * title_page_background_image
+
+表紙の背景に入れる画像を指定します。スタイルファイルからの相対パス、または絶対パスで指定します。
+
+===== ロゴ画像
+
+AsciiDoctorでは表紙にロゴ画像を配置できます。プロジェクトロゴなどが入った表紙は、そうでないものに比べると目を引きます。
+
+title_pageブロックが指定する設定項目のうち、ロゴ画像に関連するものには以下のものがあります。
+
+ * title_page_logo_align
+
+ロゴ画像の配置方法を指定します。次の値のいずれかを取ります。
+
+[format="csv", options="header"]
+|===
+設定値, 配置方法
+left, 左寄せ
+center, 中央寄せ
+right, 右寄せ
+|===
+
+ * title_page_logo_image
+
+ロゴ画像を指定します。スタイルファイルからの相対パス、または絶対パスで指定します。
+
+ * title_page_logo_top
+
+ロゴ画像の縦位置を、紙面上部からの絶対位置を指定します。単位として ```%``` を使用すると、紙面サイズに対しての割合で指定できます。
+
+===== タイトル文・サブタイトル文
+
+タイトル文とは文書の表題文です。サブタイトル文とは文書の副題文です。両者とも表紙にわかりやすく記載されます。
+
+AsciiDoctorでは、タイトル文は第1レベル見出しとして記述されます。サブタイトル文は、タイトル文に「: 」を付けて記述されます。以下は例になります。
+
+[source, asciidoc]
+----
+// タイトルは「武家諸法度」
+// サブタイトルは「よりよい江戸の世に向けて」
+
+= 武家諸法度: よりよい江戸の世に向けて
+----
+
+title_pageブロックが指定する設定項目のうち、タイトル文・サブタイトル文に関連するものには以下のものがあります。
+
+ * title_page_title_top
+
+タイトル文の縦位置を、紙面上部からの絶対位置で指定します。単位として ```%``` を使用すると紙面サイズに対しての割合で指定できます。
+
+ * title_page_title_font_size
+
+タイトル文の文字サイズを指定します。
+
+ * title_page_title_font_color
+
+タイトル文の文字色を指定します。
+
+ * title_page_title_line_height
+
+タイトル文の行高を指定します。
+
+ * title_page_subtitle_font_size
+
+サブタイトル文の文字サイズを指定します。
+
+ * title_page_subtitle_font_style
+
+サブタイトル文の字体を指定します。
+
+ * title_page_subtitle_line_height
+
+サブタイトル文の行高を指定します。
+
+===== 作者・版
+
+作者や版は重要な情報です。Asciidoctorではこれらの情報は表紙へ記載されます。
+
+title_pageブロックが指定する設定項目のうち、作者情報や版に関連するものには以下のものがあります。
+
+ * title_page_authors_margin_top
+
+作者ブロックの上部マージンを指定します。間を取る意図で使用されます。
+
+ * title_page_authors_font_size
+
+作者ブロックの文字サイズを指定します。
+
+ * title_page_authors_font_color
+
+作者ブロックの文字色を指定します。
+
+ * title_page_revision_margin_top
+
+版ブロックの上部マージンを指定します。間を取る意図で使用されます。
+
+以下は表紙体裁の指定例です。title_pageで字下げしていることから、各設定項目名ごとにtitle_page_が省略されていることに注意してください。logo・title・subtitle・authors・revisionについても同様です。
+
+[source, yaml]
+----
+title_page:
+  # 右揃え
+  align: right
+  # ロゴ
+  logo:
+    # ...紙面上部から10%の位置に
+    top: 10%
+  # タイトル文
+  title:
+    # ...紙面上部から55%; 中央より若干下気味に
+    top: 55%
+    # ...第1レベル見出しと同じ文字サイズに
+    font_size: $heading_h1_font_size
+    # ...灰色に
+    font_color: 999999
+    # ...行高は文字サイズの0.9倍に
+    line_height: 0.9
+  # サブタイトル文
+  subtitle:
+    # ...第3レベル見出しと同じ文字サイズに
+    font_size: $heading_h3_font_size
+    # ...ボールドイタリック体に
+    font_style: bold_italic
+    # ...行高は文字サイズと同じに
+    line_height: 1
+  # 作者ブロック
+  authors:
+    # ...1.25行分の上マージンを空けて
+    margin_top: $base_font_size * 1.25
+    # ...大きめ文字サイズに
+    font_size: $base_font_size_large
+    # ...暗めの灰色に
+    font_color: 181818
+  # 文書版ブロック
+  revision:
+    # ...1.25行分の上マージンを空けて
+    margin_top: $base_font_size * 1.25
+----
+
+// TBD 柱でいいのか？
+==== 柱の指定
+最後に柱の内容を指定するブロックが来ます。柱の内容とは、ページヘッダ・ページフッタの各内容を指します。もちろんページの偶奇で別の内容を指定することができます。
+
+ヘッダ・フッタ関連の設定項目には以下のものがあります。ヘッダ・フッタ間で設定がほぼ共通しているので、以下併記して解説します。
+
+ * header_background_color, footer_background_color
+
+背景色を指定します。
+
+ * header_border_color, footer_border_color
+
+区切り線色を指定します。
+
+ * header_border_style, footer_border_style
+
+区切り線スタイルを指定します。次の設定値のいずれかを取ります。
+
+[format="csv", options="header"]
+|===
+設定値, 区切り線スタイル
+solid, 実線
+double, 二重実線
+dashed, 破線
+dotted, 点線
+|===
+
+ * header_border_width, footer_border_width
+
+区切り線太さを指定します。
+
+ * header_font_color, footer_font_color
+
+文字色を指定します。
+
+ * header_font_family, footer_font_family
+
+フォントファミリを指定します。
+
+ * header_font_size, footer_font_size
+
+文字サイズを指定します。
+
+ * header_font_style, footer_font_style
+
+字体を指定します。
+
+ * header_height, footer_height
+
+高さを指定します。0にするとヘッダあるいはフッタ自体が出力されなくなります。
+
+ * header_line_height, footer_line_height
+
+行高を文字サイズの倍数で指定します。
+
+ * header_padding, footer_padding
+
+パディングを指定します。パディングとは内向きに計算される空隙です。大カッコで括って左・上・右・下と指定します。マージン同様、指定に単位を含められます。
+
+ * header_image_vertical_align, footer_image_vertical_align
+
+画像を含める時の、縦配置方法を指定します。以下の値のいずれかを取ります。
+
+[format="csv", options="header"]
+|===
+設定値, 配置方法
+top, 上寄せ
+middle, 中央寄せ
+bottom, 下寄せ
+|===
+
+また数値を指定すると、上端からの絶対指定と解釈されます。
+
+ * header_vertical_align, footer_vertical_align
+
+文の縦配置方法を指定します。以下の値のいずれかを取ります。
+
+[format="csv", options="header"]
+|===
+設定値, 配置方法
+top, 上寄せ
+middle, 中央寄せ
+bottom, 下寄せ
+|===
+
+===== ヘッダ・フッタ内容
+
+ヘッダ・フッタ領域はまずページの偶（recto）・奇（verso）で分けられます。そしてそれぞれ左寄せ（left）・中央寄せ（center）・右寄せ（right）の3つの記載パターンがあります。これらを指定する設定項目は以下の6つです。
+
+ * header_recto_content_left, footer_recto_content_left
+
+偶数ページ、左寄せで記載する内容を指定します。
+
+ * header_verso_content_left, footer_verso_content_left
+
+奇数ページ、左寄せで記載する内容を指定します。
+
+ * header_recto_content_center, footer_recto_content_center
+
+偶数ページ、中央寄せで記載する内容を指定します。
+
+ * header_verso_content_center, footer_verso_content_center
+
+奇数ページ、中央寄せで記載する内容を指定します。
+
+ * header_recto_content_right, footer_recto_content_right
+
+偶数ページ、右寄せで記載する内容を指定します。
+
+ * header_verso_content_right, footer_verso_content_right
+
+奇数ページ、右寄せで記載する内容を指定します。
+
+上の設定項目を適切に指定することで、好きな内容を出力させることができます。ページ番号を入れたい場合には ```{page-number}``` と入れておきましょう。 ```{...}``` という形式で記載された内容は、AsciiDoctorがアトリビュート（後述）として解釈し展開してくれます。ヘッダやフッタで良く使用されるアトリビュートには、次のものがあります。
+
+[format="csv", options="header"]
+|===
+アトリビュート名, 意味
+docdate, 執筆日
+docname, 文書名
+document-title, 文書タイトル
+document-subtitle, 文書サブタイトル
+chapter-title, 章タイトル
+page-count, ページ数
+page-number, ページ番号
+release, 版数
+revdate, 改訂日時
+revnumber, 改訂番号
+section-title, 節タイトル
+section-or-chapter-title, 節または章タイトル
+|===
+
+以上長くなりましたが、設定例を見てみましょう。
+
+[source, yaml]
+----
+header:
+  # 高さは0.75インチに
+  height: 0.75in
+  # 行高は文字サイズと同じに
+  line_height: 1
+  # 偶数ページの場合
+  recto_content:
+    # ... 中央に「v《文書版》, 《執筆日》」と入れるように
+    center: 'v{revnumber}, {docdate}'
+  # 奇数ページの場合
+  verso_content:
+    # 偶数ページと同一に
+    center: $header_recto_content
+
+footer:
+  # 文字サイズは小さめに
+  font_size: $base_font_size_small
+  # 文字色は本文そのままに
+  font_color: $base_font_color
+  # 区切り線の色は明るい灰色に
+  border_color: dddddd
+  # 区切り線の太さは0.25ポイント
+  border_width: 0.25
+  # 高さは0.75インチに
+  height: 0.75in
+  # 行高は文字サイズと同じに
+  line_height: 1
+  # 空隙を調整し (TBD)
+  padding: [$base_line_height_length / 2, 1, 0, 1]
+  # 上揃えに
+  vertical_align: top
+  # 偶数ページの場合
+  recto_content:
+    # ... 右寄せで「《節か章タイトル》 | 《ページ番号》」
+    right: '{section-or-chapter-title} | {page-number}'
+  # 奇数ページの場合
+  verso_content:
+    # ... 左寄せで「《ページ番号》 | 《章タイトル》」
+    left: '{page-number} | {chapter-title}'
+----
+
+==== アトリビュート
+
+さてこれまで見てきたように、スタイルファイルには体裁が記述されています。体裁をスタイルファイルにまとめたら、次はAsciiDoctorに使わせるために、新しく作ったスタイルファイルのパスを指定してやる必要があります。スタイルファイルのパスを指定するためには、アトリビュートを使用します。
+
+アトリビュートとはAsciiDoctor自体が持つ設定値で、いわば環境変数のようなものです。スタイルファイルのパス指定を行なう際には、pdf-styleと呼ばれるアトリビュートをAsciiDoctor起動時に設定しておく必要があります。アトリビュートを起動時に設定するには、-aオプションを使用します。-aオプションを使用するためは、アトリビュート名とアトリビュート値を、=で結んだ形で指定してやります。以下はpdf-styleアトリビュートを指定する例です。
+
+----
+$ asciidoctor-pdf -a pdf-style=style.yml content.adoc
+----
+
+pdf-style以外にも、使用可能なアトリビュートは多数あります。中でも良く使われるものを以下にまとめました。ご自身でドキュメントを書かれる際には、ぜひ参考になさってください。
+
+[format="csv", options="header"]
+|===
+アトリビュート名, 意味
+author, 作者情報
+citetitle, 引用ページタイトル
+coderay-css, CodeRayハイライタに適用するCSSファイルパス
+coderay-linenums-mode, CodeRayハイライタで行番号を出力する
+date, 日付
+description, 説明
+docdate, 執筆日
+docdatetime, 執筆日時
+docdir, 文書ディレクトリ
+docname, 文書名
+doctime, 執筆時
+doctitle, 文書タイトル
+doctype, 文書タイプ
+email, メールアドレス
+encoding, 文字コード
+filetype, ファイルタイプ
+iconsdir, アイコンディレクトリ
+imagesdir, 図表ディレクトリ
+inception_year, 着想年
+language, 言語
+lines, ページ行数
+numbered, ページ番号を出力する
+notitle, タイトルを出力しない
+outdir, 出力ディレクトリ
+outfile, 出力ファイル名
+pagewidth, ページ幅
+pdf-fontsdir, PDF出力用フォントディレクトリ
+pdf-page-size, PDFページサイズ
+pdf-style, PDF出力用体裁ファイル
+pdf-stylesdir, PDF出力用体裁ディレクトリ
+preface-title, 前書きページタイトル
+pygments-css, Pygmentsハイライタ用CSSファイルパス
+pygments-linenums-mode, Pygmentsハイライタで行番号を出力する
+source-highlighter, ソースコードハイライタの指定
+source-language, ソースコードのデフォルト言語
+toc, 目次を出力する
+toc-class, 目次用体裁クラス
+toc-placement, 目次用配置指定
+toc-position, 目次用位置指定
+toc-title, 目次ページタイトル
+|===
+
+
+********************************
+コラム: アトリビュートは流動的
+
+使用可能なアトリビュートについて、完全なリストはまだ存在しません。
+
+AsciiDoctorの開発が活発なため、アトリビュートが流動的に変化しているためです。もしかするとpdf-styleについても、名前が変更されたりするかもしれません。そのときはGithubのAsciiDoctor PDFプロジェクトのドキュメントを参照すれば、いつでも最新の情報が載っています。
+
+開発が活発な結果、仕様が流動的になってしまっていることは決して悪いことではありません。むしろ将来性があるということで大変歓迎すべきことです。筆者たちもAsciiDoctorコミュニティへ加わり、仕様の安定化に向けて働きかけやバグ報告などの協力を行なっています。いつか近い将来にアトリビュートも安定し、完全なリストが出てくることでしょう。
+
+それまでは、本書のリストがきっと役に立つことと思います。
+********************************
+