diff --git a/lib/blankshell.rb b/lib/blankshell.rb
new file mode 100644
index 0000000..3bf64b1
--- /dev/null
+++ b/lib/blankshell.rb
@@ -0,0 +1,630 @@
+# frozen_string_literal: true
+
+module PointBlank
+  module Parsing
+    class LineScanner
+      def initialize(text, doc)
+        @text = text
+        @document = doc
+        @stack = [@document]
+        @depth = 0
+        @topdepth = 0
+      end
+
+      # Scan document and return scanned structure
+      def scan
+        @text.each_line do |line|
+          # Consume markers from lines to keep the levels open
+          line = consume_markers(line)
+          # DO NOT RHEDEEM line if it's empty
+          line = line&.strip&.empty? ? nil : line
+          # Open up a new block on the line out of all allowed child types
+          while line && (status, line = try_open(line)) && status; end
+        end
+        close_up(0)
+        @stack.first
+      end
+
+      private
+
+      # Try to open a new block on the line
+      def try_open(line)
+        return [false, line] unless topclass.parser && line
+
+        topclass.valid_children.each do |cand|
+          next unless cand.parser.begin?(line)
+
+          @depth += 1
+          @topdepth = @depth if @topdepth < @depth
+          @stack[@depth] = cand.new
+          @stack[@depth - 1].append_child(toplevel)
+          toplevel.parser = cand.parser.new
+          line, _implicit = toplevel.parser.consume(line, @stack[@depth - 1])
+          return [true, line]
+        end
+        [false, line]
+      end
+
+      # Attempt to consume markers for all valid stack elements
+      def consume_markers(line)
+        climb = -1
+        previous = nil
+        implicit = nil
+        @stack[..@depth].each do |element|
+          newline, impl = element.parser.consume(line, previous)
+          implicit = impl unless impl.nil?
+          line = newline if newline
+          break unless newline
+
+          climb += 1
+          previous = element
+        end
+        if climb < @depth
+          if implicit && @stack[@topdepth].is_a?(::PointBlank::DOM::Paragraph)
+            backref = @stack[@topdepth]
+            remaining, = backref.parser.consume(line, previous, lazy: true)
+            return nil if remaining
+          end
+          close_up(climb)
+        end
+        line
+      end
+
+      # Close upper levels than picked level
+      def close_up(level)
+        ((level + 1)..(@stack.length - 1)).each do |index|
+          x = @stack[index]
+          switch = x.parser.close
+          x.content = x.parser.parsed_content
+          x.parser.applyprops(x) if x.parser.respond_to? :applyprops
+          x.parser = nil
+          x = transfer(x, switch) if switch
+          x.parse_inner if x.respond_to? :parse_inner
+        end
+        @topdepth = @depth = level
+        @stack = @stack[..level]
+      end
+
+      # Transfer data from class to another class (morph class)
+      def transfer(block, switchclass)
+        newblock = switchclass.new
+        newblock.content = block.content
+        newblock.parser = nil
+        block.parent[block.position] = newblock
+        newblock
+      end
+
+      # Get top level element at the current moment
+      def toplevel
+        @stack[@depth]
+      end
+
+      # Get top level element class
+      def topclass
+        @stack[@depth].class
+      end
+
+      # Debug ifno
+      def debug(line)
+        warn "#{@depth}:#{@topdepth} #{line.inspect}"
+      end
+    end
+
+    # Null parser
+    class NullParser
+      # Check that a parser parses this line as a beginning of a block
+      # @param line [String]
+      # @return [Boolean]
+      def self.begin?(_line)
+        false
+      end
+
+      # Instantiate a new parser object
+      def initialize
+        @buffer = []
+      end
+
+      # Close parser
+      # @return [nil, Class]
+      def close; end
+
+      # Return parsed content
+      # @return [String]
+      def parsed_content
+        @buffer.join(" ")
+      end
+
+      # Consume line markers
+      # @param line [String]
+      # @return [Array(String, Boolean)]
+      def consume(line, _parent = nil, **_hargs)
+        [line, false]
+      end
+
+      private
+
+      # Push a new parsed line
+      # @param line [String]
+      # @return [void]
+      def push(line)
+        @buffer.append(line)
+      end
+    end
+
+    # Paragraph parser
+    class ParagraphParser < NullParser
+      # (see ::PointBlank::Parsing::NullParser#begin?)
+      def self.begin?(_line)
+        true
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#consume)
+      def consume(line, parent = nil, lazy: false)
+        return [nil, nil] if line.match?(/\A {0,3}\Z/)
+        return ["",  nil] if check_underlines(line, parent, lazy)
+        return [nil, nil] if check_candidates(line, parent)
+        return [nil, nil] if @closed
+
+        push(line)
+        ["", nil]
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#close)
+      def close
+        @next_class if @closed and @next_class
+      end
+
+      private
+
+      # Check if the current line is an underline (morphs class)
+      def check_underlines(line, _parent, lazy)
+        return false if lazy
+
+        ::PointBlank::DOM::Paragraph.valid_children.each do |underline|
+          next unless underline.parser.begin? line
+
+          @next_class = underline
+          @closed = true
+          return true
+        end
+        false
+      end
+
+      # Check that there are no other candidates for line beginning
+      def check_candidates(line, parent)
+        return false unless parent
+
+        other = parent.class.valid_children.filter do |x|
+          x != ::PointBlank::DOM::Paragraph
+        end
+        other.any? do |x|
+          x.parser.begin? line
+        end
+      end
+    end
+
+    # ATX heading
+    # @abstract
+    class ATXParser < NullParser
+      class << self
+        attr_accessor :level
+
+        # (see ::PointBlank::Parsing::NullParser#begin?)
+        def begin?(line)
+          line.match?(/^ {0,3}\#{#{@level}}(?: .*|)$/)
+        end
+      end
+
+      def initialize
+        super
+        @matched = false
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#consume)
+      def consume(line, _parent, **_hargs)
+        return [nil, false] if @matched
+
+        @matched = true
+        push(line
+          .gsub(/\A {0,3}\#{#{self.class.level}} */, '')
+          .gsub(/( #+|)\Z/, ''))
+        [line, false]
+      end
+    end
+
+    # ATX heading level 1
+    class ATXParserLV1 < ATXParser
+      self.level = 1
+    end
+
+    # ATX heading level 2
+    class ATXParserLV2 < ATXParser
+      self.level = 2
+    end
+
+    # ATX heading level 3
+    class ATXParserLV3 < ATXParser
+      self.level = 3
+    end
+
+    # ATX heading level 4
+    class ATXParserLV4 < ATXParser
+      self.level = 4
+    end
+
+    # ATX heading level 5
+    class ATXParserLV5 < ATXParser
+      self.level = 5
+    end
+
+    # ATX heading level 6
+    class ATXParserLV6 < ATXParser
+      self.level = 6
+    end
+
+    # Underline parser
+    # @abstract
+    class UnderlineParser < NullParser
+      # Checks whether a paragraph underline is on this line.
+      # Should match an entire underline.
+      # @param line [String]
+      # @return [boolean]
+      def self.begin?(_line)
+        false
+      end
+    end
+
+    # Setext parser level 1
+    class SetextParserLV1 < UnderlineParser
+      # (see ::PointBlank::Parsing::UnderlineParser)
+      def self.begin?(line)
+        line.match?(/\A {0,3}={3,}\s*\z/)
+      end
+    end
+
+    # Setext parser level 2
+    class SetextParserLV2 < UnderlineParser
+      # (see ::PointBlank::Parsing::UnderlineParser)
+      def self.begin?(line)
+        line.match?(/\A {0,3}-{3,}\s*\z/)
+      end
+    end
+
+    # Unordered list block (group)
+    class ULParser < NullParser
+      # (see ::PointBlank::Parsing::NullParser#begin?)
+      def self.begin?(line)
+        @marker, @offset = line.match(/\A {0,3}([-+*])(\S+)/)&.captures
+        true if @marker
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#close)
+      def applyprops(block)
+        block.each do |child|
+          child.properties["marker"] = @marker
+        end
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#consume)
+      def consume(line, _parent = nil, **_hargs)
+        return [nil, true] unless continues?(line)
+
+        [line.lstrip.delete_prefix("@marker").lstrip, true]
+      end
+
+      private
+
+      # Check if a line continues this ULParser block
+      def continues?(line)
+        line.start_with?(/\A(?: {0,3}#{@marker}| )#{@offset}/)
+      end
+    end
+
+    # Quote block
+    class QuoteParser < NullParser
+      # (see ::PointBlank::Parsing::NullParser#begin?)
+      def self.begin?(line)
+        line.start_with?(/\A {0,3}>(?: \S|)/)
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#consume)
+      def consume(line, _parent = nil, **_hargs)
+        return [nil, true] unless line.start_with?(/\A {0,3}>(?: \S|)/)
+
+        [line.lstrip.delete_prefix('>').lstrip, true]
+      end
+    end
+  end
+
+  module DOM
+    class DOMError < StandardError; end
+
+    # DOM Object
+    class DOMObject
+      class << self
+        # Make subclasses inherit scanner and valid children
+        def inherited(subclass)
+          subclass.parser ||= @parser
+          subclass.scanner ||= @scanner
+          subclass.unsorted_children ||= @unsorted_children.dup || []
+          super(subclass)
+        end
+
+        # Sort children by priority
+        # @return [void]
+        def sort_children
+          @valid_children = @unsorted_children.sort_by(&:last).map(&:first)
+        end
+
+        # Define valid child for this DOMObject class
+        # @param child [Class]
+        # @return [void]
+        def define_child(child, priority = 9999)
+          @unsorted_children ||= []
+          @unsorted_children.append([child, priority])
+        end
+
+        # Define child element scanner for this DOMObject class
+        # @param child [Class]
+        # @return [void]
+        def define_scanner(scanner)
+          @scanner = scanner
+        end
+
+        # Define self parser for this DOMObject class
+        # @param child [::PointBlank::Parsing::NullParser]
+        # @return [void]
+        def define_parser(parser)
+          @parser = parser
+        end
+
+        # Define if this DOMObject class is overflowable
+        # @return [void]
+        def enable_overflow
+          @overflow = true
+        end
+
+        # Parse a document
+        # @return [self]
+        def parse(doc)
+          newdoc = new
+          newdoc.parser = parser.new
+          scan = @scanner.new(doc, newdoc)
+          scan.scan
+        end
+
+        # Source parameters from parent (fixes recursive dependency)
+        def upsource
+          superclass&.tap do |sc|
+            @scanner = sc.scanner
+            @parser = sc.parser
+            @unsorted_children = sc.unsorted_children.dup
+          end
+          sort_children
+        end
+
+        # Get array of valid children sorted by priority
+        def valid_children
+          sort_children unless @valid_children
+          @valid_children
+        end
+
+        attr_accessor :scanner, :parser, :overflow,
+                      :unsorted_children
+      end
+
+      include ::Enumerable
+
+      def initialize
+        @children = []
+        @properties = {}
+        @content = ""
+      end
+
+      # Set element at position
+      # @param index [Integer]
+      # @param element [DOMObject]
+      # @return [DOMObject]
+      def []=(index, element)
+        unless element.is_a? ::PointBlank::DOM::DOMObject
+          raise DOMError, "invalid DOM class #{element.class}"
+        end
+
+        @children[index] = element
+      end
+
+      # Get element at position
+      # @param index [Integer]
+      # @return [DOMObject]
+      def [](index)
+        @children[index]
+      end
+
+      # Iterate over each child of DOMObject
+      # @param block [#call]
+      def each(&block)
+        @children.each(&block)
+      end
+
+      # Return an array duplicate of all children
+      # @return [Array<DOMObject>]
+      def children
+        @children.dup
+      end
+
+      # Append child
+      # @param child [DOMObject]
+      def append_child(child)
+        unless child.is_a? ::PointBlank::DOM::DOMObject
+          raise DOMError, "invalid DOM class #{child.class}"
+        end
+
+        child.parent = self
+        child.position = @children.length
+        @children.append(child)
+      end
+
+      attr_accessor :content, :parser, :parent, :position
+      attr_reader :properties
+    end
+
+    # Inline text
+    class Text < DOMObject
+    end
+
+    # Inline preformatted text
+    class InlinePre < DOMObject
+    end
+
+    # Infline formattable text
+    class InlineFormattable < DOMObject
+    end
+
+    # Bold text
+    class InlineBold < InlineFormattable
+    end
+
+    # Italics text
+    class InlineItalics < InlineFormattable
+    end
+
+    # Inline italics text (alternative)
+    class InlineAltItalics < InlineFormattable
+    end
+
+    # Underline text
+    class InlineUnder < InlineFormattable
+    end
+
+    # Strikethrough text
+    class InlineStrike < InlineFormattable
+    end
+
+    # Hyperreferenced text
+    class InlineLink < InlineFormattable
+    end
+
+    # Image
+    class InlineImage < InlinePre
+    end
+
+    # Linebreak
+    class InlineBreak < DOMObject
+    end
+
+    # Block root (virtual)
+    class Block < DOMObject
+    end
+
+    # Document root
+    class Document < Block
+    end
+
+    # Paragraph in a document (separated by 2 newlines)
+    class Paragraph < InlineFormattable
+      define_parser ::PointBlank::Parsing::ParagraphParser
+    end
+
+    # Heading level 1
+    class SetextHeading1 < InlineFormattable
+      define_parser ::PointBlank::Parsing::SetextParserLV1
+    end
+
+    # Heading level 2
+    class SetextHeading2 < SetextHeading1
+      define_parser ::PointBlank::Parsing::SetextParserLV2
+    end
+
+    # Heading level 1
+    class ATXHeading1 < InlineFormattable
+      define_parser ::PointBlank::Parsing::ATXParserLV1
+    end
+
+    # Heading level 2
+    class ATXHeading2 < ATXHeading1
+      define_parser ::PointBlank::Parsing::ATXParserLV2
+    end
+
+    # Heading level 3
+    class ATXHeading3 < ATXHeading1
+      define_parser ::PointBlank::Parsing::ATXParserLV3
+    end
+
+    # Heading level 4
+    class ATXHeading4 < ATXHeading1
+      define_parser ::PointBlank::Parsing::ATXParserLV4
+    end
+
+    # Heading level 5
+    class ATXHeading5 < ATXHeading1
+      define_parser ::PointBlank::Parsing::ATXParserLV5
+    end
+
+    # Heading level 6
+    class ATXHeading6 < ATXHeading1
+      define_parser ::PointBlank::Parsing::ATXParserLV6
+    end
+
+    # Preformatted code block
+    class CodeBlock < DOMObject
+    end
+
+    # Quote block
+    class QuoteBlock < Block
+    end
+
+    # Table
+    class TableBlock < DOMObject
+    end
+
+    # List element
+    class ListElement < Block
+    end
+
+    # Unordered list
+    class ULBlock < Block
+    end
+
+    # Ordered list block
+    class OLBlock < Block
+    end
+
+    # Indent block
+    class IndentBlock < DOMObject
+    end
+
+    # Horizontal rule
+    class HorizontalRule < DOMObject
+    end
+
+    # Block root (real)
+    Block.class_eval do
+      define_scanner ::PointBlank::Parsing::LineScanner
+      define_parser ::PointBlank::Parsing::NullParser
+      define_child ::PointBlank::DOM::Paragraph
+      define_child ::PointBlank::DOM::ATXHeading1, 600
+      define_child ::PointBlank::DOM::ATXHeading2, 600
+      define_child ::PointBlank::DOM::ATXHeading3, 600
+      define_child ::PointBlank::DOM::ATXHeading4, 600
+      define_child ::PointBlank::DOM::ATXHeading5, 600
+      define_child ::PointBlank::DOM::ATXHeading6, 600
+      define_child ::PointBlank::DOM::QuoteBlock, 600
+      define_child ::PointBlank::DOM::ULBlock, 500
+    end
+
+    Paragraph.class_eval do
+      define_child ::PointBlank::DOM::SetextHeading1, 1
+      define_child ::PointBlank::DOM::SetextHeading2, 2
+    end
+
+    Block.subclasses.each(&:upsource)
+
+    QuoteBlock.class_eval do
+      define_parser ::PointBlank::Parsing::QuoteParser
+    end
+
+    ULBlock.class_eval do
+      define_parser ::PointBlank::Parsing::ULParser
+    end
+  end
+end
diff --git a/lib/rbmark.rb b/lib/rbmark.rb
index 5895aea..1f7e6b9 100644
--- a/lib/rbmark.rb
+++ b/lib/rbmark.rb
@@ -153,8 +153,15 @@ module RBMark
     class BreakerVariant < BlockVariant
       # Check that a paragraph matches the breaker
       # @param buffer [String]
-      # @return [Class, nil]
-      def match(_buffer)
+      # @return [Boolean]
+      def match?(_buffer)
+        raise StandardError, "Abstract method called"
+      end
+
+      # Process a paragrpah
+      # @param buffer [String]
+      # @return [::RBMark::DOM::DOMObject]
+      def process(_buffer)
         raise StandardError, "Abstract method called"
       end
 
@@ -164,6 +171,16 @@ module RBMark
       #   @return [String]
     end
 
+    # Paragraph replacing variant
+    class ModifierVariant < BlockVariant
+      # Check that a buffer matches requirements of the modifier
+      # @param buffer [String]
+      # @return [Class, nil]
+      def match?(_buffer)
+        raise StandardError, "Abstract method called"
+      end
+    end
+
     # Paragraph variant
     class ParagraphVariant < BlockVariant
       # (see BlockVariant#begin?)
@@ -189,17 +206,42 @@ module RBMark
       # (see BlockVariant#flush)
       # @sg-ignore
       def flush(buffer)
-        dom_class = nil
-        breaker = parent.variants.find do |x|
-          x[0].is_a?(::RBMark::Parsing::BreakerVariant) &&
-            (dom_class = x[0].match(buffer))
-        end&.first
-        buffer = breaker.preprocess(buffer) if breaker.respond_to?(:preprocess)
-        (dom_class or ::RBMark::DOM::Paragraph).parse(buffer.strip)
+        obj = ::RBMark::DOM::Paragraph.new
+        obj.content = buffer
+        obj
+      end
+
+      # (see BlockVariant#restructure)
+      def restructure(blocks, _buffer, _mode)
+        p_buffer = blocks.last.content
+        if (block = do_breakers(p_buffer))
+          blocks[-1] = block
+        else
+          unless (blocks, _buffer, _mode = do_modifiers(blocks, p_buffer))
+            blocks[-1] = ::RBMark::DOM::Paragraph.parse(p_buffer)
+          end
+        end
+        [blocks, "", nil]
       end
 
       private
 
+      def do_modifiers(blocks, buffer)
+        breaker = parent.variants.find do |x|
+          x[0].is_a?(::RBMark::Parsing::ModifierVariant) &&
+            x[0].match?(buffer)
+        end&.first
+        breaker&.restructure(blocks, buffer, nil) || [blocks, buffer, nil]
+      end
+
+      def do_breakers(buffer)
+        breaker = parent.variants.find do |x|
+          x[0].is_a?(::RBMark::Parsing::BreakerVariant) &&
+            x[0].match?(buffer)
+        end&.first
+        breaker&.process(buffer)
+      end
+
       def check_paragraph_breakers(line)
         breakers = parent.variants.filter_map do |x|
           x[0] if x[0].is_a? ::RBMark::Parsing::BreakerVariant
@@ -266,11 +308,10 @@ module RBMark
     end
 
     # Paragraph closing variant
-    class BlankSeparator < BreakerVariant
+    class BlankSeparator < BlockVariant
       # (see BlockVariant#begin?)
-      def begin?(line, breaks_paragraph: nil, **_opts)
-        breaks_paragraph &&
-          line.match?(/^ {0,3}$/)
+      def begin?(line, **_opts)
+        line.match?(/^ {0,3}$/)
       end
 
       # (see BlockVariant#end?)
@@ -279,8 +320,14 @@ module RBMark
       end
 
       # (see BreakerVariant#match)
-      def match(_buffer)
-        nil
+      def match?(_buffer)
+        false
+      end
+
+      # (see BlockVariant#restructure)
+      def restructure(blocks, _buffer, _mode)
+        blocks.last.properties[:closed] = true if blocks.last
+        [blocks, "", nil]
       end
     end
 
@@ -298,19 +345,25 @@ module RBMark
       end
 
       # (see BreakerVariant#match)
-      def match(buffer)
+      def match?(buffer)
         return nil unless preprocess(buffer).match(/\S/)
 
-        heading(buffer.lines.last)
+        !heading(buffer.lines.last).nil?
       end
 
-      # (see BreakerVariant#preprocess)
-      def preprocess(buffer)
-        buffer.lines[..-2].join
+      # (see BreakerVariant#process)
+      def process(buffer)
+        heading = heading(buffer.lines.last)
+        buffer = preprocess(buffer)
+        heading.parse(buffer)
       end
 
       private
 
+      def preprocess(buffer)
+        buffer.lines[..-2].join
+      end
+
       def heading(buffer)
         case buffer
         when /^ {0,3}-+ *$/ then ::RBMark::DOM::Heading2
@@ -369,6 +422,28 @@ module RBMark
         block.content = buffer.lines[1..-2].join
       end
     end
+
+    # Quote block
+    class QuoteBlock < BlockVariant
+      # (see BlockVariant#begin?)
+      def begin?(line, **_opts)
+        line.match?(/^ {0,3}(?:>|> .*)$/)
+      end
+
+      # (see BlockVariant#end?)
+      def end?(_line, lookahead: nil, **_opts)
+        !(lookahead && lookahead.match?(/^ {0,3}(?:>|> .*)$/))
+      end
+
+      # (see BlockVariant#flush)
+      def flush(buffer)
+        buffer = buffer.lines.map do |line|
+          line.gsub(/^ {0,3}> ?/, '')
+        end.join
+
+        ::RBMark::DOM::QuoteBlock.parse(buffer)
+      end
+    end
   end
 
   # Module for representing abstract object hierarchy
@@ -453,7 +528,20 @@ module RBMark
           @atomic_mode = true
         end
 
-        attr_accessor :variants, :scanner_class, :alt_for, :atomic_mode
+        # Set the block continuation flag
+        # @return [void]
+        def block
+          @block_mode = true
+        end
+
+        # Allow the block to be empty
+        # @return [void]
+        def empty
+          @permit_empty = true
+        end
+
+        attr_accessor :variants, :scanner_class, :alt_for, :atomic_mode,
+                      :block_mode, :permit_empty
       end
 
       def initialize
@@ -557,18 +645,80 @@ module RBMark
     class InlineBreak < DOMObject
     end
 
-    # Document root
-    class Document < DOMObject
+    # Block root
+    class Block < DOMObject
       scanner ::RBMark::Parsing::LineScanner
-      variant ::RBMark::Parsing::ATXHeadingVariant
-      variant ::RBMark::Parsing::ThematicBreakVariant
-      variant ::RBMark::Parsing::SetextHeadingVariant
-      variant ::RBMark::Parsing::IndentedBlockVariant
-      variant ::RBMark::Parsing::FencedCodeBlock
+      variant ::RBMark::Parsing::ATXHeadingVariant, prio: 100
+      variant ::RBMark::Parsing::ThematicBreakVariant, prio: 200
+      variant ::RBMark::Parsing::SetextHeadingVariant, prio: 300
+      variant ::RBMark::Parsing::IndentedBlockVariant, prio: 400
+      variant ::RBMark::Parsing::FencedCodeBlock, prio: 500
+      variant ::RBMark::Parsing::QuoteBlock, prio: 600
       variant ::RBMark::Parsing::BlankSeparator, prio: 9998
       variant ::RBMark::Parsing::ParagraphVariant, prio: 9999
     end
 
+    # Document root
+    class Document < Block
+      class << self
+        # (see ::RBMark::DOM::DOMObject#parse)
+        def parse(text)
+          cleanup(merge(super))
+        end
+
+        private
+
+        # Clean up empty elements
+        # @param doc [::RBMark::DOM::Document]
+        # @return [::RBMark::DOM::Document]
+        def cleanup(doc)
+          _cleanup(doc)
+          doc
+        end
+
+        # Merge open paragraphs upwards
+        # @param doc [::RBMark::DOM::Document]
+        # @return [::RBMark::DOM::Document]
+        def merge(doc)
+          _merge(doc)
+          doc
+        end
+
+        # A function to merge children upward
+        def _merge_step(child, stack, depth)
+          stack
+        end
+
+        # Merge nested block constructs upwards
+        # @param doc [::RBMark::DOM::DOMObject]
+        # @return [void]
+        def _merge(doc, stack = [], depth = 0)
+          stack.append(doc) if stack.length <= depth
+          doc.children.each do |child|
+            stack = _merge_step(child, stack, depth)
+            if child.class.block_mode and child.children.length.positive?
+              _merge(child, stack, depth + 1)
+            end
+          end
+        end
+
+        # Recursively descend through hierarchy and delete empty elements
+        # @param doc [::RBMark::DOM::DOMObject]
+        # @return [Integer]
+        def _cleanup(doc)
+          size = 0
+          doc.children.delete_if do |child|
+            subsize = 0
+            subsize += _cleanup(child) if child.children.length.positive?
+            subsize += child.content&.strip&.length || 0
+            size += subsize
+            subsize.zero? && !child.class.permit_empty
+          end
+          size
+        end
+      end
+    end
+
     # Paragraph in a document (separated by 2 newlines)
     class Paragraph < InlineFormattable
       atomic
@@ -603,7 +753,8 @@ module RBMark
     end
 
     # Quote block
-    class QuoteBlock < Document
+    class QuoteBlock < Block
+      block
     end
 
     # Table
@@ -611,7 +762,7 @@ module RBMark
     end
 
     # List element
-    class ListElement < Document
+    class ListElement < Block
     end
 
     # Unordered list
@@ -629,6 +780,7 @@ module RBMark
     # Horizontal rule
     class HorizontalRule < DOMObject
       atomic
+      empty
     end
   end
 end
diff --git a/lib/test.rb b/lib/test.rb
new file mode 100644
index 0000000..846ccb7
--- /dev/null
+++ b/lib/test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative 'rbmark'
+
+structure = RBMark::DOM::Document.parse(File.read("example.md"))
+def red(string)
+  "\033[31m#{string}\033[0m"
+end
+def yellow(string)
+  "\033[33m#{string}\033[0m"
+end
+
+def prettyprint(doc, indent = 0)
+  closed = doc.properties[:closed]
+  puts "#{yellow(doc.class.name.gsub(/\w+::DOM::/,""))}#{red(closed ? "(c)" : "")}: #{doc.content.inspect}"
+  doc.children.each do |child|
+    print red("#{" " * indent}  - ")
+    prettyprint(child, indent + 4)
+  end
+end
+prettyprint(structure)
diff --git a/lib/test2.rb b/lib/test2.rb
new file mode 100644
index 0000000..bf5208e
--- /dev/null
+++ b/lib/test2.rb
@@ -0,0 +1,15 @@
+require_relative 'blankshell'
+pp PointBlank::DOM::Document.parse(<<DOC)
+Penis
+# STREEMER VIN SAUCE JORKS HIS PEANUTS ON S TREeAM
+> pee
+> > 2 pee
+> peepee
+> > 3 pee
+> > 4 pee
+bee
+> # IT'S HIP
+> BEES
+> > FUCK
+ BEES
+DOC
diff --git a/lib/test3.rb b/lib/test3.rb
new file mode 100644
index 0000000..10411a2
--- /dev/null
+++ b/lib/test3.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require_relative 'blankshell'
+
+structure = PointBlank::DOM::Document.parse(<<~DOC)
+  Penis
+  # STREEMER VIN SAUCE JORKS HIS PEANUTS ON S TREeAM
+  > pee
+  > > 2 pee
+  > peepee
+  and you cum now
+  > > 3 pee
+  > > 4 pee
+  bee
+  # IT'S HIP
+  > # IT'S HIP
+  > BEES
+  > > FUCK
+  BEES
+  PEES
+  =========
+
+  > COME ON AND SNIFF THE PAINT
+  >
+  > WITH MEEE
+  > > OH THAT IS SO CUUL
+  > OH THERE'S BLOOD IN MY STOOL
+  > AAAAA IT HURTS
+  >
+  > > WHEN I
+  > PEEEEEEE
+
+  PIIS
+  ==========
+
+  but does it end here?
+  > COCK
+  > < PENIS
+  > < > AMONGUS
+  > < CONTINUATION
+  > > BREAKER
+  COCK
+DOC
+def red(string)
+  "\033[31m#{string}\033[0m"
+end
+def yellow(string)
+  "\033[33m#{string}\033[0m"
+end
+
+def prettyprint(doc, indent = 0)
+  closed = doc.properties[:closed]
+  puts "#{yellow(doc.class.name.gsub(/\w+::DOM::/,""))}#{red(closed ? "(c)" : "")}: #{doc.content.inspect}"
+  doc.children.each do |child|
+    print red("#{" " * indent}  - ")
+    prettyprint(child, indent + 4)
+  end
+end
+prettyprint(structure)