diff --git a/lib/blankshell.rb b/lib/blankshell.rb
index 3bf64b1..4bc6cdf 100644
--- a/lib/blankshell.rb
+++ b/lib/blankshell.rb
@@ -2,6 +2,168 @@
 
 module PointBlank
   module Parsing
+    module LinkSharedMethods
+      # Normalize a label
+      # @param string [String]
+      # @return [String]
+      def normalize_label(string)
+        string = string.downcase(:fold).strip.gsub(/\s+/, " ")
+        return nil if string.empty?
+
+        string
+      end
+
+      # Read link label.
+      # Returns matched label or nil, and remainder of the string
+      # @param text [String]
+      # @return [Array(<String, nil>, String)]
+      def read_return_label(text)
+        prev = text
+        label = ""
+        return nil, text unless text.start_with?('[')
+
+        bracketcount = 0
+        text.split(/(?<!\\)([\[\]])/).each do |part|
+          if part == '['
+            bracketcount += 1
+          elsif part == ']'
+            bracketcount -= 1
+            break (label += part) if bracketcount.zero?
+          end
+          label += part
+        end
+        return [nil, text] unless bracketcount.zero?
+
+        text = text.delete_prefix(label)
+        label = normalize_label(label[1..-2])
+        label ? [label, text] : [nil, prev]
+      end
+
+      # Read link label.
+      # Returns matched label or nil, and remainder of the string
+      # @param text [String]
+      # @return [Array(<String, nil>, String)]
+      def read_label(text)
+        prev = text
+        label = ""
+        return nil, text unless text.start_with?('[')
+
+        bracketcount = 0
+        text.split(/(?<!\\)([\[\]])/).each do |part|
+          if part == '['
+            bracketcount += 1
+          elsif part == ']'
+            bracketcount -= 1
+            break (label += part) if bracketcount.zero?
+          end
+          label += part
+        end
+        text = text.delete_prefix(label)
+        label = normalize_label(label[1..-2])
+        text.start_with?(':') && label ? [label, text[1..].lstrip] : [nil, prev]
+      end
+
+      # Read link destination (URI).
+      # Returns matched label or nil, and remainder of the string
+      # @param text [String]
+      # @return [Array(<String, nil>, String)]
+      def read_destination(text)
+        if (result = text.match(/\A<.*?(?<![^\\]\\)>/m)) &&
+           !result[0][1..].match?(/(?<![^\\]\\)</)
+          [result[0].gsub(/\\(?=[><])/, '')[1..-2],
+           text.delete_prefix(result[0]).lstrip]
+        elsif (result = text.match(/\A\S+/)) &&
+              !result[0].start_with?('<') &&
+              result &&
+              balanced?(result[0])
+          [result[0],
+           text.delete_prefix(result[0]).lstrip]
+        else
+          [nil, text]
+        end
+      end
+
+      # Read link title.
+      # Returns matched label or nil, and remainder of the string
+      # @param text [String]
+      # @return [Array(<String, nil>, String)]
+      def read_title(text)
+        if text.start_with?("'") &&
+           (result = text.match(/\A'.*?(?<!\\)'/m))
+          [result[0][1..-2],
+           text.delete_prefix(result[0]).lstrip]
+        elsif text.start_with?('"') &&
+              (result = text.match(/\A".*?(?<!\\)"/m))
+          [result[0][1..-2],
+           text.delete_prefix(result[0]).lstrip]
+        elsif text.start_with?('(') &&
+              (result = find_balanced_end(text))
+          [text[1..(result - 1)],
+           text.delete_prefix(text[..result]).lstrip]
+        else
+          [nil, text]
+        end
+      end
+
+      # Read link properties.
+      # Returns matched parameters as hash or nil, and remainder of the string
+      # @param text [String]
+      # @return [Array([Hash, nil], String)]
+      def read_properties(text)
+        properties = {}
+        remaining = text
+        if text.start_with? '[' # link label
+          properties[:label], remaining = read_return_label(remaining)
+        elsif text.start_with? '(' # link properties
+          destination, remaining = read_destination(remaining[1..])
+          return [nil, text] unless destination
+
+          title, remaining = read_title(remaining)
+          properties[:destination] = destination
+          properties[:title] = title
+        end
+        if properties.empty? || !remaining.start_with?(')')
+          [nil, text]
+        else
+          [properties, remaining[1..]]
+        end
+      end
+
+      # Check if brackets are balanced
+      # @param text [String]
+      # @return [Boolean]
+      def balanced?(text)
+        bracketcount = 0
+        text.split(/(?<!\\)([()])/).each do |part|
+          if part == '('
+            bracketcount += 1
+          elsif part == ')'
+            bracketcount -= 1
+            return false if bracketcount.negative?
+          end
+        end
+        bracketcount.zero?
+      end
+
+      # Find index at which balanced part of a bracket closes
+      # @param text [String]
+      # @return [Integer, nil]
+      def find_balanced_end(text)
+        bracketcount = 0
+        index = 0
+        text.split(/(?<!\\)([()])/).each do |part|
+          if part == '('
+            bracketcount += 1
+          elsif part == ')'
+            bracketcount -= 1
+            return index if bracketcount.zero?
+          end
+          index += part.length
+        end
+        nil
+      end
+    end
+
     class LineScanner
       def initialize(text, doc)
         @text = text
@@ -30,6 +192,7 @@ module PointBlank
       # Try to open a new block on the line
       def try_open(line)
         return [false, line] unless topclass.parser && line
+        return [false, line] unless [nil, self.class].include? topclass.scanner
 
         topclass.valid_children.each do |cand|
           next unless cand.parser.begin?(line)
@@ -74,9 +237,9 @@ module PointBlank
       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
+          switch = x.parser.close(x)
           x.parser = nil
           x = transfer(x, switch) if switch
           x.parse_inner if x.respond_to? :parse_inner
@@ -112,11 +275,15 @@ module PointBlank
 
     # 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
+      class << self
+        attr_accessor :parser_for
+
+        # Check that a parser parses this line as a beginning of a block
+        # @param line [String]
+        # @return [Boolean]
+        def begin?(_line)
+          false
+        end
       end
 
       # Instantiate a new parser object
@@ -125,13 +292,21 @@ module PointBlank
       end
 
       # Close parser
+      # @param block [::PointBlank::DOM::DOMObject]
       # @return [nil, Class]
-      def close; end
+      def close(block, lazy: false)
+        block.class.valid_overlays.each do |overlay_class|
+          overlay = overlay_class.new
+          output = overlay.process(block, lazy: lazy)
+          return output if output
+        end
+        nil
+      end
 
       # Return parsed content
       # @return [String]
       def parsed_content
-        @buffer.join(" ")
+        @buffer.join('')
       end
 
       # Consume line markers
@@ -154,14 +329,14 @@ module PointBlank
     # Paragraph parser
     class ParagraphParser < NullParser
       # (see ::PointBlank::Parsing::NullParser#begin?)
-      def self.begin?(_line)
-        true
+      def self.begin?(line)
+        line.match?(/\A {0,3}\S/)
       end
 
       # (see ::PointBlank::Parsing::NullParser#consume)
       def consume(line, parent = nil, lazy: false)
+        @lazy_triggered = lazy || @lazy_triggered
         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
 
@@ -170,32 +345,20 @@ module PointBlank
       end
 
       # (see ::PointBlank::Parsing::NullParser#close)
-      def close
-        @next_class if @closed and @next_class
+      def close(block, **_lazy)
+        super(block, lazy: @lazy_triggered)
       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
+        classes = parent.class.valid_children
+        once = false
+        other = classes.filter do |cls|
+          !(once ||= (cls == ::PointBlank::DOM::Paragraph))
         end
         other.any? do |x|
           x.parser.begin? line
@@ -294,29 +457,174 @@ module PointBlank
     class ULParser < NullParser
       # (see ::PointBlank::Parsing::NullParser#begin?)
       def self.begin?(line)
-        @marker, @offset = line.match(/\A {0,3}([-+*])(\S+)/)&.captures
-        true if @marker
+        line.match?(/\A {0,3}([-+*])(\s+)/)
       end
 
       # (see ::PointBlank::Parsing::NullParser#close)
       def applyprops(block)
         block.each do |child|
-          child.properties["marker"] = @marker
+          child.properties["marker"] = @marker[-1]
         end
       end
 
+      # (see ::PointBlank::Parsing::NullParser#consume)
+      def consume(line, _parent = nil, **_hargs)
+        self.open(line)
+        return [nil, true] unless continues?(line)
+
+        [line, true]
+      end
+
+      private
+
+      # Open block if it hasn't been opened yet
+      def open(line)
+        marker, offset = line.match(/\A {0,3}([-+*])(\s+)/)&.captures
+        return unless marker
+
+        @marker ||= ['+', '*'].include?(marker) ? "\\#{marker}" : marker
+        @offset = offset
+      end
+
+      # Check if a line continues this ULParser block
+      def continues?(line)
+        return false if ::PointBlank::Parsing::ThematicBreakParser.begin?(line)
+
+        line.start_with?(/\A(?: {0,3}#{@marker}| )#{@offset}/) ||
+          line.strip.empty?
+      end
+    end
+
+    # Unorder list block (element)
+    class ULElementParser < NullParser
+      # (see ::PointBlank::Parsing::NullParser#begin?)
+      def self.begin?(line)
+        line.match?(/\A {0,3}([-+*])(\s+)/)
+      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]
+        self.open(line)
+
+        [normalize(line), true]
       end
 
       private
 
+      # Open block if it hasn't been opened yet
+      def open(line)
+        return if @open
+
+        @marker, @offset = line.match(/\A {0,3}([-+*])(\s+)/)&.captures
+        @marker = "\\#{@marker}" if ['+', '*'].include? @marker
+        @open = true
+      end
+
       # Check if a line continues this ULParser block
       def continues?(line)
-        line.start_with?(/\A(?: {0,3}#{@marker}| )#{@offset}/)
+        return true unless @marker
+
+        line.start_with?(/\A\s#{@offset}/) ||
+          line.strip.empty?
+      end
+
+      # Normalize the line
+      def normalize(line)
+        line.gsub(/\A(?: {0,3}#{@marker}| )#{@offset}/, '')
+      end
+    end
+
+    # Ordered list block (group)
+    class OLParser < NullParser
+      # (see ::PointBlank::Parsing::NullParser#begin?)
+      def self.begin?(line)
+        line.match?(/\A {0,3}(\d+)([).])(\s+)/)
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#close)
+      def applyprops(block)
+        block.each do |child|
+          child.properties["marker"] = @mark[-1]
+        end
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#consume)
+      def consume(line, _parent = nil, **_hargs)
+        self.open(line)
+        return [nil, true] unless continues?(line)
+
+        [line, true]
+      end
+
+      private
+
+      # Open block if it hasn't been opened yet
+      def open(line)
+        num, marker, offset = line.match(/\A {0,3}(\d+)([).])(\s+)/)
+                                  &.captures
+        return unless marker
+
+        @num = " " * (num.length + 1)
+        @mark ||= "\\#{marker}"
+        @offset = offset
+      end
+
+      # Check if a line continues this ULParser block
+      def continues?(line)
+        return false if ::PointBlank::Parsing::ThematicBreakParser.begin?(line)
+
+        line.start_with?(/\A(?: {0,3}(\d+)#{@mark}|#{@num})#{@offset}/) ||
+          line.strip.empty?
+      end
+    end
+
+    # Unorder list block (element)
+    class OLElementParser < NullParser
+      # (see ::PointBlank::Parsing::NullParser#begin?)
+      def self.begin?(line)
+        line.match?(/\A {0,3}(\d+)([).])(\s+)/)
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#consume)
+      def consume(line, _parent = nil, **_hargs)
+        return [nil, true] unless continues?(line)
+
+        self.open(line)
+
+        [normalize(line), true]
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#applyprops)
+      def applyprops(block)
+        block.properties["number"] = @num.to_i
+      end
+
+      private
+
+      # Open block if it hasn't been opened yet
+      def open(line)
+        return if @open
+
+        @num, @marker, @offset = line.match(/\A {0,3}(\d+)([).])(\s+)/)
+                                     &.captures
+        @numoffset = " " * (@num.length + 1)
+        @marker = "\\#{@marker}"
+        @open = true
+      end
+
+      # Check if a line continues this ULParser block
+      def continues?(line)
+        return true unless @marker
+
+        line.start_with?(/\A#{@numoffset}#{@offset}/) ||
+          line.strip.empty?
+      end
+
+      # Normalize the line
+      def normalize(line)
+        line.gsub(/\A(?: {0,3}(\d+)#{@marker}|#{@numoffset})#{@offset}/, '')
       end
     end
 
@@ -331,11 +639,697 @@ module PointBlank
       def consume(line, _parent = nil, **_hargs)
         return [nil, true] unless line.start_with?(/\A {0,3}>(?: \S|)/)
 
-        [line.lstrip.delete_prefix('>').lstrip, true]
+        [normalize(line), true]
+      end
+
+      private
+
+      # Normalize line in quoteblock
+      def normalize(line)
+        line.gsub(/\A {0,3}> ?/, '')
+      end
+    end
+
+    # Fenced code block
+    # (TODO: This needs ~~~ as alternative to ticks,
+    # and proper relative indentation)
+    class FencedCodeBlock < NullParser
+      # (see ::PointBlank::Parsing::NullParser#begin?)
+      def self.begin?(line)
+        line.start_with?(/\A {0,3}```[^`]+$/)
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#applyprops)
+      def applyprops(block)
+        block.properties["infoline"] = @infoline
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#consume)
+      def consume(line, _parent = nil, **_hargs)
+        return [nil, false] if @closed
+
+        try_close(line)
+        push(line) if @open && !@closed
+        self.open(line)
+        ["", false]
+      end
+
+      private
+
+      def try_close(line)
+        @closed = true if @open && line.match?(/\A {0,3}```/)
+      end
+
+      def open(line)
+        return if @open
+
+        @infoline = line.match(/\A {0,3}```(.*)/)[1]
+        @open = true
+      end
+    end
+
+    # Indented code block
+    class IndentedBlock < NullParser
+      # (see ::PointBlank::Parsing::NullParser#begin?)
+      def self.begin?(line)
+        line.start_with?(/\A {4}/)
+      end
+
+      # (see ::PointBlank::Parsing::NullParser#consume)
+      def consume(line, _parent = nil, **_hargs)
+        return [nil, nil] unless self.class.begin?(line) ||
+                                 line.strip.empty?
+
+        push(normalize(line))
+        ["", false]
+      end
+
+      private
+
+      def normalize(line)
+        line.gsub("\A(?:    |\t)", '')
+      end
+    end
+
+    # Thematic break parser
+    class ThematicBreakParser < NullParser
+      # (see PointBlank::Parsing::NullParser#begin?)
+      def self.begin?(line)
+        line.match?(/\A {0,3}(?:[- ]+|[* ]+|[_ ]+)\n/)
+      end
+
+      # (see PointBlank::Parsing::NullParser#consume)
+      def consume(_line, _parent = nil, **_hargs)
+        return [nil, nil] if @closed
+
+        @closed = true
+        ["", nil]
+      end
+    end
+
+    # Class of parsers that process the paragraph after it finished collection
+    class NullOverlay < NullParser
+      # Stub
+      def self.begin?(_line)
+        false
+      end
+
+      # Process block after it closed
+      # @param block [::PointBlank::DOM::DOMObject]
+      # @param lazy [Boolean]
+      # @return [nil, Class]
+      def process(_block, lazy: false); end
+    end
+
+    # Overlay for processing underline classes of paragraph
+    class ParagraphUnderlineOverlay < NullOverlay
+      # (see ::PointBlank::Parsing::NullOverlay#process)
+      def process(block, lazy: false)
+        output = check_underlines(block.content.lines.last, lazy)
+        block.content = block.content.lines[0..-2].join("") if output
+        output
+      end
+
+      private
+
+      # Check if the current line is an underline (morphs class)
+      def check_underlines(line, lazy)
+        return nil if lazy
+
+        ::PointBlank::DOM::Paragraph.valid_children.each do |underline|
+          parser = underline.parser
+          next unless parser < ::PointBlank::Parsing::UnderlineParser
+          next unless parser.begin? line
+
+          return underline
+        end
+        nil
+      end
+    end
+
+    # Overlay for link reference definitions
+    class LinkReferenceOverlay < NullOverlay
+      include LinkSharedMethods
+
+      def initialize
+        super
+        @definitions = {}
+      end
+
+      # (see ::PointBlank::Parsing::NullOverlay#process)
+      def process(block, **_lazy)
+        text = block.content
+        loop do
+          prev = text
+          label, text = read_label(text)
+          break prev unless label
+
+          destination, text = read_destination(text)
+          break prev unless destination
+
+          title, text = read_title(text)
+          push_definition(label, destination, title)
+        end
+        modify(block, text)
+        nil
+      end
+
+      private
+
+      def root(block)
+        current_root = block
+        current_root = current_root.parent while current_root.parent
+        current_root
+      end
+
+      def modify(block, text)
+        rootblock = root(block)
+        rootblock.properties[:linkdefs] =
+          if rootblock.properties[:linkdefs]
+            @definitions.merge(rootblock.properties[:linkdefs])
+          else
+            @definitions.dup
+          end
+        block.content = text
+      end
+
+      def push_definition(label, uri, title = nil)
+        labelname = label.strip.downcase.gsub(/\s+/, ' ')
+        return if @definitions[labelname]
+
+        @definitions[labelname] = {
+          uri: uri,
+          title: title
+        }
+      end
+    end
+
+    # Inline scanner
+    class StackScanner
+      def initialize(doc, init_tokens: nil)
+        @doc = doc
+        @init_tokens = init_tokens
+      end
+
+      # Scan document
+      def scan
+        rounds = quantize(@doc.class.unsorted_children)
+        tokens = @init_tokens || [@doc.content]
+        rounds.each do |valid_parsers|
+          @valid_parsers = valid_parsers
+          tokens = tokenize(tokens)
+          tokens = forward_walk(tokens)
+          tokens = reverse_walk(tokens)
+        end
+        structure = finalize(tokens)
+        structure.each { |child| @doc.append_child(child) }
+      end
+
+      private
+
+      # Finalize structure, concatenate adjacent text parts,
+      # transform into Text objects
+      # @param parts [Array<String, ::PointBlank::DOM::DOMObject>]
+      # @return [Array<::PointBlank::DOM::DOMObject>]
+      def finalize(structure)
+        structnew = []
+        buffer = ""
+        structure.each do |block|
+          block = block.first if block.is_a? Array
+          buffer += block if block.is_a? String
+          next if block.is_a? String
+
+          structnew.append(construct_text(buffer)) unless buffer.empty?
+          buffer = ""
+          structnew.append(block)
+        end
+        structnew.append(construct_text(buffer)) unless buffer.empty?
+        structnew
+      end
+
+      # Construct text object for a string
+      # @param string [String]
+      # @return [::PointBlank::DOM::Text]
+      def construct_text(string)
+        obj = ::PointBlank::DOM::Text.new
+        obj.content = string
+        obj
+      end
+
+      # Transform text into a list of tokens
+      def tokenize(tokens)
+        parts = tokens
+        @valid_parsers.each do |parser|
+          newparts = []
+          parts.each do |x|
+            if x.is_a? String
+              newparts.append(*parser.tokenize(x))
+            else
+              newparts.append(x)
+            end
+          end
+          parts = newparts
+        end
+        parts
+      end
+
+      # Process parsed tokens (callback on open, forward search direction)
+      def forward_walk(parts)
+        parts = parts.dup
+        newparts = []
+        while (part = parts.shift)
+          next newparts.append(part) if part.is_a? String
+
+          if part[1].respond_to?(:forward_walk) && part.last == :open
+            part, parts = part[1].forward_walk([part] + parts)
+          end
+          newparts.append(part)
+        end
+        newparts
+      end
+
+      # Process parsed tokens (callback on close, inverse search direction)
+      def reverse_walk(parts)
+        backlog = []
+        parts.each do |part|
+          backlog.append(part)
+          next unless part.is_a? Array
+          next unless part.last == :close
+          next unless part[1].respond_to?(:reverse_walk)
+
+          backlog = part[1].reverse_walk(backlog)
+        end
+        backlog
+      end
+
+      # Quantize valid children
+      def quantize(children)
+        children.group_by(&:last).map { |_, v| v.map(&:first).map(&:parser) }
+      end
+    end
+
+    # Null inline scanner element
+    # @abstract
+    class NullInline
+      class << self
+        attr_accessor :parser_for
+      end
+
+      # Tokenize a string
+      # @param string [String]
+      # @return [Array<Array(String, Class, Symbol), String>]
+      def self.tokenize(string)
+        [string]
+      end
+
+      # @!method self.reverse_walk(backlog)
+      #   Reverse-walk the backlog and construct a valid element from it
+      #   @param backlog [Array<Array(String, Class, Symbol), String>]
+      #   @return [Array<Array(String, Class, Symbol), String>]
+
+      # @!method self.forward_walk(backlog)
+      #   Forward-walk the backlog starting from the current valid element
+      #   @param backlog [Array<Array(String, Class, Symbol), String>]
+      #   @return [Array<Array(String, Class, Symbol), String>]
+
+      # Check that the symbol at this index is not escaped
+      # @param index [Integer]
+      # @param string [String]
+      # @return [nil, Integer]
+      def self.check_unescaped(index, string)
+        return index if index.zero?
+
+        count = 0
+        index -= 1
+        while index >= 0 && string[index] == "\\"
+          count += 1
+          index -= 1
+        end
+        (count % 2).zero?
+      end
+
+      # Find the first occurence of an unescaped pattern
+      # @param string [String]
+      # @param pattern [Regexp, String]
+      # @return [Integer, nil]
+      def self.find_unescaped(string, pattern)
+        initial = 0
+        while (index = string.index(pattern, initial))
+          return index if check_unescaped(index, string)
+
+          initial = index + 1
+        end
+        nil
+      end
+
+      # Iterate over every string/unescaped token part
+      # @param string [String]
+      # @param pattern [Regexp]
+      # @param callback [#call]
+      # @return [Array<String, Array(String, Class, Symbol)>]
+      def self.iterate_tokens(string, pattern, &filter)
+        tokens = []
+        initial = 0
+        while (index = string.index(pattern, initial))
+          prefix = (index.zero? ? nil : string[initial..(index - 1)])
+          tokens.append(prefix) if prefix
+          unescaped = check_unescaped(index, string)
+          match = filter.call(index.positive? ? string[..(index - 1)] : "",
+                              string[index..],
+                              unescaped)
+          tokens.append(match)
+          match = match.first if match.is_a? Array
+          initial = index + match.length
+        end
+        remaining = string[initial..] || ""
+        tokens.append(remaining) unless remaining.empty?
+        tokens
+      end
+
+      # Build child
+      # @param children [Array]
+      # @return [::PointBlank::DOM::DOMObject]
+      def self.build(children)
+        obj = parser_for.new
+        if parser_for.valid_children.empty?
+          children.each do |child|
+            child = child.first if child.is_a? Array
+            child = construct_text(child) if child.is_a? String
+            obj.append_child(child)
+          end
+        else
+          tokens = children.map do |child|
+            child.is_a?(Array) ? child.first : child
+          end
+          scanner = StackScanner.new(obj, init_tokens: tokens)
+          scanner.scan
+        end
+        obj
+      end
+
+      # Construct text object for a string
+      # @param string [String]
+      # @return [::PointBlank::DOM::Text]
+      def self.construct_text(string)
+        obj = ::PointBlank::DOM::Text.new
+        obj.content = string
+        obj
+      end
+
+      # Check that contents can be contained within this element
+      # @param elements [Array<String, Array(String, Class, Symbol)>]
+      # @return [Boolean]
+      def self.check_contents(elements)
+        elements.each do |element|
+          next unless element.is_a? ::PointBlank::DOM::DOMObject
+          next if parser_for.valid_children.include? element.class
+
+          return false
+        end
+        true
+      end
+    end
+
+    # Code inline parser
+    class CodeInline < NullInline
+      # (see ::PointBlank::Parsing::NullInline#tokenize)
+      def self.tokenize(string)
+        open = {}
+        iterate_tokens(string, "`") do |_before, current_text, matched|
+          if matched
+            match = current_text.match(/^`+/)[0]
+            if open[match]
+              open[match] = nil
+              [match, self, :close]
+            else
+              open[match] = true
+              [match, self, :open]
+            end
+          else
+            current_text[0]
+          end
+        end
+      end
+
+      # TODO: optimize, buffer only after walking
+      # (see ::PointBlank::Parsing::NullInline#forward_walk)
+      def self.forward_walk(parts)
+        buffer = ""
+        opening = parts.first.first
+        cutoff = 0
+        parts.each_with_index do |part, idx|
+          text = (part.is_a?(Array) ? part.first : part)
+          buffer += text
+          next unless part.is_a? Array
+
+          break (cutoff = idx) if part.first == opening &&
+                                  part.last == :close
+        end
+        buffer = buffer[opening.length..(-1 - opening.length)]
+        [cutoff.positive? ? build([buffer]) : opening, parts[(cutoff + 1)..]]
+      end
+    end
+
+    # Autolink inline parser
+    class AutolinkInline < NullInline
+      # (see ::PointBlank::Parsing::NullInline#tokenize)
+      def self.tokenize(string)
+        iterate_tokens(string, /[<>]/) do |_before, current_text, matched|
+          if matched
+            if current_text.start_with?("<")
+              ["<", self, :open]
+            else
+              [">", self, :close]
+            end
+          else
+            current_text[0]
+          end
+        end
+      end
+
+      # TODO: optimize, buffer only after walking
+      # (see ::PointBlank::Parsing::NullInline#forward_walk
+      def self.forward_walk(parts)
+        buffer = ""
+        cutoff = 0
+        parts.each_with_index do |part, idx|
+          text = (part.is_a?(Array) ? part.first : part)
+          buffer += text
+          next unless part.is_a? Array
+
+          break (cutoff = idx) if part.first == ">" && part.last == :close
+        end
+        return '<', parts[1..] unless buffer.match?(/^<[\w\-_+]+:[^<>\s]+>$/)
+
+        [build([buffer[1..-2]]), parts[(cutoff + 1)..]]
+      end
+    end
+
+    # Hyperreference inline superclass
+    # @abstract
+    class HyperlinkInline < NullInline
+      # Parse link properties according to given link suffix
+      # @param input [String]
+      # @return [Array(<Hash, String, nil>, String)]
+      def self.parse_linkinfo(input)
+        props, remainder = read_properties(input)
+        return nil, "" unless props
+
+        capture = input[..(input.length - remainder.length - 1)]
+        [props, capture]
+      end
+
+      # Build object and apply link info to it
+      # @param capture [Array<String, Array(String, Class, Symbol)>]
+      # @return [::PointBlank::DOM::DOMObject]
+      def self.build_w_linkinfo(capture)
+        linkinfo = capture[-1][2]
+        obj = build(capture[1..-2])
+        obj.properties = linkinfo
+        obj
+      end
+
+      # TODO: optimize, increase index instead of building buffers
+      # (see ::PointBlank::Parsing::NullInline#reverse_walk)
+      def self.reverse_walk(backlog)
+        before = []
+        capture = []
+        open = true
+        cls = nil
+        backlog.reverse_each do |block|
+          (open ? capture : before).prepend(block)
+          next unless block.is_a?(Array) && block[1] < self
+
+          open = false
+          cls = block[1]
+          return backlog unless block[1].check_contents(capture)
+        end
+        return backlog if open
+
+        before + [cls.build_w_linkinfo(capture)]
+      end
+    end
+
+    # Image inline parser
+    class ImageInline < HyperlinkInline
+      class << self
+        include ::PointBlank::Parsing::LinkSharedMethods
+      end
+
+      # (see ::PointBlank::Parsing::NullInline#tokenize)
+      def self.tokenize(string)
+        iterate_tokens(string, /(?:!\[|\]\()/) do |_before, text, matched|
+          next text[0] unless matched
+          next ["![", self, :open] if text.start_with? "!["
+          next text[0] unless text.start_with? "]"
+
+          info, capture = parse_linkinfo(text[1..])
+          info ? ["]#{capture}", HyperlinkInline, info, :close] : text[0]
+        end
+      end
+    end
+
+    # Link inline parser
+    class LinkInline < HyperlinkInline
+      class << self
+        include ::PointBlank::Parsing::LinkSharedMethods
+      end
+
+      # (see ::PointBlank::Parsing::NullInline#tokenize)
+      def self.tokenize(string)
+        iterate_tokens(string, /(?:\[|\][(\[])/) do |_before, text, matched|
+          next text[0] unless matched
+          next ["[", self, :open] if text.start_with? "["
+          next text[0] unless text.start_with? "]"
+
+          info, capture = parse_linkinfo(text[1..])
+          info ? ["]#{capture}", HyperlinkInline, info, :close] : text[0]
+        end
+      end
+    end
+
+    # Emphasis and strong emphasis inline parser
+    class EmphInline < NullInline
+      INFIX_TOKENS = /^[^\p{S}\p{P}\p{Zs}_]_++[^\p{S}\p{P}\p{Zs}_]$/
+      # (see ::PointBlank::Parsing::NullInline#tokenize)
+      def self.tokenize(string)
+        iterate_tokens(string, /(?:_++|\*++)/) do |bfr, text, matched|
+          token, afr = text.match(/^(_++|\*++)(.?)/)[1..2]
+          left = left_token?(bfr[-1] || "", token, afr)
+          right = right_token?(bfr[-1] || "", token, afr)
+          break_into_elements(token, [bfr[-1] || "", token, afr].join(''),
+                              left, right, matched)
+        end
+      end
+
+      # Is this token, given these surrounding characters, left-flanking?
+      # @param bfr [String]
+      # @param token [String]
+      # @param afr [String]
+      def self.left_token?(bfr, _token, afr)
+        bfr_white = bfr.match?(/[\p{Zs}\n\r]/) || bfr.empty?
+        afr_white = afr.match?(/[\p{Zs}\n\r]/) || afr.empty?
+        bfr_symbol = bfr.match?(/[\p{P}\p{S}]/)
+        afr_symbol = afr.match?(/[\p{P}\p{S}]/)
+        !afr_white && (!afr_symbol || (afr_symbol && (bfr_symbol || bfr_white)))
+      end
+
+      # Is this token, given these surrounding characters, reft-flanking?
+      # @param bfr [String]
+      # @param token [String]
+      # @param afr [String]
+      def self.right_token?(bfr, _token, afr)
+        bfr_white = bfr.match?(/[\p{Z}\n\r]/) || bfr.empty?
+        afr_white = afr.match?(/[\p{Z}\n\r]/) || afr.empty?
+        bfr_symbol = bfr.match?(/[\p{P}\p{S}]/)
+        afr_symbol = afr.match?(/[\p{P}\p{S}]/)
+        !bfr_white && (!bfr_symbol || (bfr_symbol && (afr_symbol || afr_white)))
+      end
+
+      # Break token string into elements
+      # @param token_inner [String]
+      # @param token [String]
+      # @param left [Boolean]
+      # @param right [Boolean]
+      # @param matched [Boolean]
+      def self.break_into_elements(token_inner, token, left, right, matched)
+        return token_inner[0] unless matched
+
+        star_token = token_inner.match?(/^\*+$/)
+        infix_token = token.match(INFIX_TOKENS)
+        return token_inner if !star_token && infix_token
+
+        if left && right
+          [token_inner, self, :open, :close]
+        elsif left
+          [token_inner, self, :open]
+        elsif right
+          [token_inner, self, :close]
+        else
+          token_inner
+        end
+      end
+
+      # (see ::PointBlank::Parsing::NullInline#reverse_walk)
+      def self.reverse_walk(backlog)
+        until backlog.last.first.empty?
+          capture = []
+          before = []
+          closer = backlog.last
+          star = closer.first.match?(/^\*+$/)
+          open = true
+          backlog[..-2].reverse_each do |blk|
+            open = false if blk.is_a?(Array) && blk[2] == :open &&
+                            blk.first.match?(/^\*+$/) == star &&
+                            blk[1] == self &&
+                            ((blk.first.length + closer.first.length) % 3 != 0 ||
+                               ((blk.first.length % 3).zero? &&
+                                (closer.first.length % 3).zero?))
+            (open ? capture : before).prepend(blk)
+            next unless blk.is_a?(Array)
+            return backlog unless blk[1].check_contents(capture)
+          end
+          return backlog if open
+
+          opener = before[-1]
+          strong = if closer.first.length > 1 && opener.first.length > 1
+                     # Strong emphasis
+                     closer[0] = closer.first[2..]
+                     opener[0] = opener.first[2..]
+                     true
+                   else
+                     # Emphasis
+                     closer[0] = closer.first[1..]
+                     opener[0] = opener.first[1..]
+                     false
+                   end
+          before = before[..-2] if opener.first.empty?
+          backlog = before + [build_emph(capture, strong)] + [closer]
+        end
+        backlog
+      end
+
+      # Build strong or normal emphasis depending on the boolean flag
+      # @param children [Array<String, ::PointBlank::DOM::DOMObject>]
+      # @param strong [Boolean]
+      # @return [::PointBlank::DOM::DOMObject]
+      def self.build_emph(children, strong)
+        obj = if strong
+                ::PointBlank::DOM::InlineStrong
+              else
+                ::PointBlank::DOM::InlineEmphasis
+              end.new
+        tokens = children.map do |child|
+          child.is_a?(Array) ? child.first : child
+        end
+        scanner = StackScanner.new(obj, init_tokens: tokens)
+        scanner.scan
+        obj
       end
     end
   end
 
+  # Domain object model elements
   module DOM
     class DOMError < StandardError; end
 
@@ -353,7 +1347,8 @@ module PointBlank
         # Sort children by priority
         # @return [void]
         def sort_children
-          @valid_children = @unsorted_children.sort_by(&:last).map(&:first)
+          @valid_children = @unsorted_children&.sort_by(&:last)&.map(&:first) ||
+                            []
         end
 
         # Define valid child for this DOMObject class
@@ -375,13 +1370,25 @@ module PointBlank
         # @param child [::PointBlank::Parsing::NullParser]
         # @return [void]
         def define_parser(parser)
+          parser.parser_for = self
           @parser = parser
         end
 
-        # Define if this DOMObject class is overflowable
+        # Define an overlay - a parsing strategy that occurs once a block is closed.
+        # May transform block if #process method of the overlay class returns
+        # a class.
+        # @param overlay [::PointBlank::Parsing::NullOverlay]
         # @return [void]
-        def enable_overflow
-          @overflow = true
+        def define_overlay(overlay, priority = 9999)
+          @unsorted_overlays ||= []
+          @unsorted_overlays.append([overlay, priority])
+        end
+
+        # Sort overlays by priority
+        # @return [void]
+        def sort_overlays
+          @valid_overlays = @unsorted_overlays&.sort_by(&:last)&.map(&:first) ||
+                            []
         end
 
         # Parse a document
@@ -399,24 +1406,35 @@ module PointBlank
             @scanner = sc.scanner
             @parser = sc.parser
             @unsorted_children = sc.unsorted_children.dup
+            @unsorted_overlays = sc.unsorted_overlays.dup
           end
           sort_children
         end
 
+        # Get array of valid overlays sorted by priority
+        # @return [Array<::PointBlank::Parsing::NullOverlay>]
+        def valid_overlays
+          sort_overlays unless @valid_overlays
+          @valid_overlays
+        end
+
         # Get array of valid children sorted by priority
+        # @return [Array<Class>]
         def valid_children
           sort_children unless @valid_children
           @valid_children
         end
 
-        attr_accessor :scanner, :parser, :overflow,
-                      :unsorted_children
+        attr_accessor :scanner, :parser,
+                      :unsorted_children,
+                      :unsorted_overlays
       end
 
       include ::Enumerable
 
       def initialize
         @children = []
+        @temp_children = []
         @properties = {}
         @content = ""
       end
@@ -464,8 +1482,23 @@ module PointBlank
         @children.append(child)
       end
 
-      attr_accessor :content, :parser, :parent, :position
-      attr_reader :properties
+      # Append temp. child
+      # @param child [DOMObject, String]
+      def append_temp(child)
+        unless child.is_a?(::PointBlank::DOM::DOMObject) ||
+               child.is_a?(String)
+          raise DOMError, "invlaid temp class #{child.class}"
+        end
+
+        @temp_children.append(child)
+      end
+
+      attr_accessor :content, :parser, :parent, :position, :properties
+      attr_reader :temp_children
+    end
+
+    # Temp. text class
+    class TempText < DOMObject
     end
 
     # Inline text
@@ -474,59 +1507,121 @@ module PointBlank
 
     # Inline preformatted text
     class InlinePre < DOMObject
+      define_parser ::PointBlank::Parsing::CodeInline
     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
+    # Image
+    class InlineImage < InlineFormattable
+      define_parser ::PointBlank::Parsing::ImageInline
+      define_child ::PointBlank::DOM::InlinePre, 4000
+      ## that would be really funny lmao
+      # define_child ::PointBlank::DOM::InlineImage
     end
 
     # Hyperreferenced text
     class InlineLink < InlineFormattable
-    end
-
-    # Image
-    class InlineImage < InlinePre
+      define_parser ::PointBlank::Parsing::LinkInline
+      define_child ::PointBlank::DOM::InlinePre, 4000
+      define_child ::PointBlank::DOM::InlineImage, 5000
+      ## idk if this makes sense honestly
+      # define_child ::PointBlank::DOM::InlineAutolink
     end
 
     # Linebreak
     class InlineBreak < DOMObject
     end
 
+    # Autolink
+    class InlineAutolink < DOMObject
+      define_parser ::PointBlank::Parsing::AutolinkInline
+    end
+
+    # Inline root
+    class InlineRoot < DOMObject
+      define_scanner ::PointBlank::Parsing::StackScanner
+      define_child ::PointBlank::DOM::InlinePre, 4000
+      define_child ::PointBlank::DOM::InlineAutolink, 4000
+      define_child ::PointBlank::DOM::InlineImage, 5000
+      define_child ::PointBlank::DOM::InlineLink, 6000
+    end
+
+    # Strong emphasis
+    class InlineStrong < InlineRoot
+    end
+
+    # Emphasis
+    class InlineEmphasis < InlineRoot
+    end
+
+    InlineRoot.class_eval do
+      define_child ::PointBlank::DOM::InlineStrong, 8000
+      define_child ::PointBlank::DOM::InlineEmphasis, 8000
+    end
+
+    InlineRoot.subclasses.each(&:upsource)
+
+    InlineStrong.class_eval do
+      define_parser ::PointBlank::Parsing::EmphInline
+    end
+
+    InlineEmphasis.class_eval do
+      define_parser ::PointBlank::Parsing::EmphInline
+    end
+
+    InlineImage.class_eval do
+      define_child ::PointBlank::DOM::InlineStrong, 8000
+      define_child ::PointBlank::DOM::InlineEmphasis, 8000
+    end
+
+    InlineLink.class_eval do
+      define_child ::PointBlank::DOM::InlineStrong, 8000
+      define_child ::PointBlank::DOM::InlineEmphasis, 8000
+    end
     # Block root (virtual)
     class Block < DOMObject
     end
 
+    # Leaf block (virtual)
+    class LeafBlock < DOMObject
+      # Virtual hook to push inlines in place of leaf blocks
+      def parse_inner
+        child = ::PointBlank::DOM::Text.new
+        child.content = content
+        append_child(child)
+      end
+    end
+
     # Document root
     class Document < Block
     end
 
     # Paragraph in a document (separated by 2 newlines)
-    class Paragraph < InlineFormattable
+    class Paragraph < DOMObject
+      class << self
+        # Define an overlay
+      end
+
       define_parser ::PointBlank::Parsing::ParagraphParser
+      define_overlay ::PointBlank::Parsing::ParagraphUnderlineOverlay, 0
+      define_overlay ::PointBlank::Parsing::LinkReferenceOverlay
+
+      # Virtual hook to parse inline contents of a finished paragraph
+      def parse_inner
+        child = ::PointBlank::DOM::InlineRoot.new
+        child.content = content
+        scanner = ::PointBlank::Parsing::StackScanner.new(child)
+        scanner.scan
+        self.content = ""
+        child.each { |c| append_child(c) }
+      end
     end
 
     # Heading level 1
-    class SetextHeading1 < InlineFormattable
+    class SetextHeading1 < LeafBlock
       define_parser ::PointBlank::Parsing::SetextParserLV1
     end
 
@@ -536,7 +1631,7 @@ module PointBlank
     end
 
     # Heading level 1
-    class ATXHeading1 < InlineFormattable
+    class ATXHeading1 < LeafBlock
       define_parser ::PointBlank::Parsing::ATXParserLV1
     end
 
@@ -565,8 +1660,9 @@ module PointBlank
       define_parser ::PointBlank::Parsing::ATXParserLV6
     end
 
-    # Preformatted code block
-    class CodeBlock < DOMObject
+    # Preformatted fenced code block
+    class CodeBlock < LeafBlock
+      define_parser ::PointBlank::Parsing::FencedCodeBlock
     end
 
     # Quote block
@@ -577,31 +1673,44 @@ module PointBlank
     class TableBlock < DOMObject
     end
 
-    # List element
-    class ListElement < Block
+    # Unordered list element
+    class ULListElement < Block
+    end
+
+    # Ordered list element
+    class OLListElement < Block
     end
 
     # Unordered list
-    class ULBlock < Block
+    class ULBlock < DOMObject
+      define_scanner ::PointBlank::Parsing::LineScanner
+      define_parser ::PointBlank::Parsing::ULParser
+      define_child ::PointBlank::DOM::ULListElement
     end
 
     # Ordered list block
-    class OLBlock < Block
+    class OLBlock < DOMObject
+      define_scanner ::PointBlank::Parsing::LineScanner
+      define_parser ::PointBlank::Parsing::ULParser
+      define_child ::PointBlank::DOM::OLListElement
     end
 
     # Indent block
-    class IndentBlock < DOMObject
+    class IndentBlock < LeafBlock
+      define_parser ::PointBlank::Parsing::IndentedBlock
     end
 
     # Horizontal rule
     class HorizontalRule < DOMObject
+      define_parser ::PointBlank::Parsing::ThematicBreakParser
     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::IndentBlock, 9999
+      define_child ::PointBlank::DOM::Paragraph, 9998
       define_child ::PointBlank::DOM::ATXHeading1, 600
       define_child ::PointBlank::DOM::ATXHeading2, 600
       define_child ::PointBlank::DOM::ATXHeading3, 600
@@ -609,7 +1718,11 @@ module PointBlank
       define_child ::PointBlank::DOM::ATXHeading5, 600
       define_child ::PointBlank::DOM::ATXHeading6, 600
       define_child ::PointBlank::DOM::QuoteBlock, 600
-      define_child ::PointBlank::DOM::ULBlock, 500
+      define_child ::PointBlank::DOM::ULBlock, 700
+      define_child ::PointBlank::DOM::OLBlock, 700
+      define_child ::PointBlank::DOM::CodeBlock, 600
+      define_child ::PointBlank::DOM::HorizontalRule, 300
+      sort_children
     end
 
     Paragraph.class_eval do
@@ -626,5 +1739,17 @@ module PointBlank
     ULBlock.class_eval do
       define_parser ::PointBlank::Parsing::ULParser
     end
+
+    ULListElement.class_eval do
+      define_parser ::PointBlank::Parsing::ULElementParser
+    end
+
+    OLBlock.class_eval do
+      define_parser ::PointBlank::Parsing::OLParser
+    end
+
+    OLListElement.class_eval do
+      define_parser ::PointBlank::Parsing::OLElementParser
+    end
   end
 end
diff --git a/lib/test3.rb b/lib/test3.rb
index 10411a2..900f8de 100644
--- a/lib/test3.rb
+++ b/lib/test3.rb
@@ -2,7 +2,7 @@
 
 require_relative 'blankshell'
 
-structure = PointBlank::DOM::Document.parse(<<~DOC)
+doc = <<~DOC
   Penis
   # STREEMER VIN SAUCE JORKS HIS PEANUTS ON S TREeAM
   > pee
@@ -20,6 +20,13 @@ structure = PointBlank::DOM::Document.parse(<<~DOC)
   PEES
   =========
 
+  [definition]: /url 'title'
+  [definition
+  2
+  ]:
+  /long_url_with_varying_stuff
+  (title)
+
   > COME ON AND SNIFF THE PAINT
   >
   > WITH MEEE
@@ -40,7 +47,120 @@ structure = PointBlank::DOM::Document.parse(<<~DOC)
   > < CONTINUATION
   > > BREAKER
   COCK
+
+  + Plus block opens
+    and continues.
+
+    This is the next paragraph of a plus block,
+    and this is a continuation line in the block
+  + This thing continues the outer block and has a plus sign still.
+    next part
+  - SIMPS LMAO
+    continuation
+
+    This by the way should continue the
+    block but should be a separate
+    paragraph
+  - Next shit
+
+    > INCLUDING INNER QUOTES BY THE WAY
+    WITH INNER PARAGRAPH FALL OFF!!!
+
+    also a paragraph inside this thing
+
+    - BUT CAN WE GET EVEN STUPIDER?????
+
+      > YES WE CAN!!!!
+
+  - Another element
+
+   NOW it breaks
+  1. FREDDY FAZBER???
+     HARHAR HAR HAR HAR
+     HAR HAR HARHAR
+
+     HOLY SHITTO FREDDY FASTBER???
+     AR AR HARHAR HAR
+     HURHURHURHUR
+
+  2. fast
+      ber
+  10. BIG
+      still the same OLblock
+  11) OK NOW THIS IS EBIN
+      different block
+  12930192) THIS still continues because idk why really
+            lmao
+
+            > QUONT PARGRAP
+            WHAT THEF UCK BASSBOOSTED
+
+  >```fencedcode block infoline (up to interpretation)
+  > #THIS should have a very specific structure, not modified by anything
+  >
+  > int main() {
+  >   int i = 1;
+  >   if (i > 0) {
+  >     printf("anus\\n");
+  >   }
+  >   return 0;
+  > }
+  >```
+
+      Also code block
+
+      Hello mario
+
+      also these should continue so that's a thing
+
+  - Thematic break test
+  - - - - - - - - - - - - - - - - - - - - -
+  - Above should be a thematic break, not a list containing a thematic break
+
+  but what if
+  --------------
+  WRONG????
+
+  aaa
+                bbb
+                                ccc
+
+  now it's time to CUHHHMMMMMMM
+  - <amongus:thisis_an_autolink>
+  - <amongus:but this isn't>
+  - <peeee:nis> peepee <peee:peeeeeeinis> Pe
+  - `cum on <` hogogwagarts ><cum:on`>hogogwagarts`
+  - ``` test `should work tho `` and this should be continued` ````
+  - \\<amongus:bumpalumpa>
+  - `` \\<cum:amongus> ```
+  - \\```amongus``
+  - ``amongus``\\`
+  - ![image](/test.jpg 'title')
+  - moretests![image](/test.jpg (title))after
+  - more tests ![image](/invalid(link 'valid') after
+  - more tests ![image](/valid(link) 'valid') after
+  - next test
+    ![image `inner block` etc](/should_be_valid "should be valid")
+    amongus
+  - ![image `this shouldn't be allowed to be an image](/shouldn't be valid `technicallynotatitle`)
+  - [outer![inner](/AAAAAA 'peepee')](/poopoo 'AAAAAA')
+  - [amongus][definition]
+  - *emphasis on multiple words*
+  - **strong emphasis on multiple words**
+  - infix**emphasis**block
+  - no_infix_empahsis
+  - _emphasis_
+  - __strong emphasis__
+  - __nested __strong__ emphasis__
+  - __(__this__)__
+  - *among us*** ***vr*
+  - *among **us*vr****
+  - *among **us *vr****
+  - *among**us*
+  - [*outer*![****inner****](/AAAAAA 'peepee')](/poopoo 'AAAAAA')
 DOC
+
+structure = PointBlank::DOM::Document.parse(doc)
 def red(string)
   "\033[31m#{string}\033[0m"
 end
@@ -49,10 +169,11 @@ def yellow(string)
 end
 
 def prettyprint(doc, indent = 0)
-  closed = doc.properties[:closed]
-  puts "#{yellow(doc.class.name.gsub(/\w+::DOM::/,""))}#{red(closed ? "(c)" : "")}: #{doc.content.inspect}"
+  puts "#{yellow(doc.class.name.gsub(/\w+::DOM::/, ''))}: "\
+       "#{doc.content.inspect} "\
+       "#{doc.properties.empty? ? '' : red(doc.properties.inspect)}"
   doc.children.each do |child|
-    print red("#{" " * indent}  - ")
+    print red("#{' ' * indent}  - ")
     prettyprint(child, indent + 4)
   end
 end