diff --git a/classes b/classes
new file mode 100644
index 0000000..9153047
--- /dev/null
+++ b/classes
@@ -0,0 +1,13 @@
+Bold [x}
+Italics [x]
+Underline [x]
+Strikethrough [x]
+CodeInline [x]
+Link [x]
+Image [x]
+Headings [x]
+CodeBlock [x]
+QuoteBlock [x]
+ULBlock [x]
+OLBLock [x]
+TableBlock []
diff --git a/document.rb b/document.rb
new file mode 100644
index 0000000..07c15f6
--- /dev/null
+++ b/document.rb
@@ -0,0 +1,738 @@
+# frozen_string_literal: true
+
+module RBMark
+  # Parser units
+  # Parsers are divided into three categories:
+  # - Slicers - these parsers read the whole text of an element and slice it into chunks digestible by other parsers
+  # - ChunkParsers - these parsers transform chunks of text into a single DOM unit
+  # - InlineParsers - these parsers are called directly by the slicer to check whether a certain element matches needed criteria
+  module Parsers
+    # Abstract slicer class
+    class Slicer
+      # @param parent [::RBMark::DOM::DOMObject]
+      def initialize
+        @chunk_parsers = []
+      end
+
+      attr_accessor :chunk_parsers
+
+      private
+
+      def parse_chunk(text)
+        @chunk_parsers.each do |parser|
+          unless parser.is_a? ChunkParser
+            raise StandardError, 'not a ChunkParser'
+          end
+
+          next unless parser.match?(text)
+
+          return parser.match(text)
+        end
+        nil
+      end
+    end
+
+    # Abstract inline parser class
+    class InlineParser
+      # Test if piece matches bold syntax
+      # @param text [String]
+      # @return [Boolean]
+      def match?(text)
+        text.match?(@match_exp)
+      end
+
+      # Construct a new object from text
+      # @param text [String]
+      # @return [Object]
+      def match(text)
+        @class.parse(text)
+      end
+
+      attr_reader :class, :match_exp
+    end
+
+    # Abstract chunk parser class
+    class ChunkParser
+      # Stub for match method
+      def match(text)
+        element = ::RBMark::DOM::Text.new
+        element.content = text
+        element
+      end
+
+      # Stub for match? method
+      def match?(_text)
+        true
+      end
+    end
+
+    # Slices text into paragraphs and feeds slices to chunk parsers
+    class RootSlicer < Slicer
+      # Parse text into chunks and feed each to the chain
+      # @param text [String]
+      def parse(text)
+        output = text.split(/(?:\r\r|\n\n|\r\n\r\n|\Z)/)
+                     .reject { |x| x.match(/\A\s*\Z/) }
+                     .map do |block|
+          parse_chunk(block)
+        end
+        merge_list_indents(output)
+      end
+
+      private
+
+      def merge_list_indents(chunks)
+        last_list = nil
+        delete_deferred = []
+        chunks.each_with_index do |chunk, index|
+          if !last_list and [::RBMark::DOM::ULBlock,
+                             ::RBMark::DOM::OLBlock].include? chunk.class
+            last_list = chunk
+          elsif last_list and mergeable?(last_list, chunk)
+            merge(last_list, chunk)
+            delete_deferred.prepend(index)
+          else
+            last_list = nil
+          end
+        end
+        delete_deferred.each { |i| chunks.delete_at(i) }
+        chunks
+      end
+
+      def mergeable?(last_list, chunk)
+        if chunk.is_a? ::RBMark::DOM::IndentBlock or
+           (chunk.is_a? ::RBMark::DOM::ULBlock and
+            last_list.is_a? ::RBMark::DOM::ULBlock) or
+           (chunk.is_a? ::RBMark::DOM::OLBlock and
+            last_list.is_a? ::RBMark::DOM::OLBlock and
+            last_list.properties["num"] > chunk.properties["num"])
+          true
+        else
+          false
+        end
+      end
+
+      def merge(last_list, chunk)
+        if chunk.is_a? ::RBMark::DOM::IndentBlock
+          last_list.children.last.children.append(*chunk.children)
+        else
+          last_list.children.append(*chunk.children)
+        end
+      end
+    end
+
+    # Inline text slicer (slices based on the start and end symbols)
+    class InlineSlicer < Slicer
+      # Parse slices
+      # @param text [String]
+      def parse(text)
+        parts = []
+        index = prepare_markers
+        until text.empty?
+          before, part, text = slice(text)
+          parts.append(::RBMark::DOM::Text.parse(before)) unless before.empty?
+          next unless part
+
+          element = index.fetch(part.regexp,
+                                ::RBMark::Parsers::TextInlineParser.new)
+                         .match(part[0])
+          parts.append(element)
+        end
+        parts
+      end
+
+      private
+
+      # Prepare markers from chunk_parsers
+      # @return [Hash]
+      def prepare_markers
+        index = {}
+        @markers = @chunk_parsers.map do |parser|
+          index[parser.match_exp] = parser
+          parser.match_exp
+        end
+        index
+      end
+
+      # Get the next slice of a text based on markers
+      # @param text [String]
+      # @return [Array<(String,MatchData,String)>]
+      def slice(text)
+        first_tag = @markers.map { |x| text.match(x) }
+                            .reject(&:nil?)
+                            .min_by { |x| x.offset(0)[0] }
+        return text, nil, "" unless first_tag
+
+        [first_tag.pre_match, first_tag, first_tag.post_match]
+      end
+    end
+
+    # Slicer for unordered lists
+    class UnorderedSlicer < Slicer
+      # Parse list elements
+      def parse(text)
+        output = []
+        buffer = ""
+        text.lines.each do |line|
+          if line.start_with? "- " and !buffer.empty?
+            output.append(make_element(buffer))
+            buffer = ""
+          end
+          buffer += line[2..]
+        end
+        output.append(make_element(buffer)) unless buffer.empty?
+        output
+      end
+
+      private
+
+      def make_element(text)
+        ::RBMark::DOM::ListElement.parse(text)
+      end
+    end
+
+    # Slicer for unordered lists
+    class OrderedSlicer < Slicer
+      # rubocop:disable Metrics/AbcSize
+
+      # Parse list elements
+      def parse(text)
+        output = []
+        buffer = ""
+        indent = text.match(/\A\d+\. /)[0].length
+        num = text.match(/\A(\d+)\. /)[1]
+        text.lines.each do |line|
+          if line.start_with?(/\d+\. /) and !buffer.empty?
+            output.append(make_element(buffer, num))
+            buffer = ""
+            indent = line.match(/\A\d+\. /)[0].length
+            num = line.match(/\A(\d+)\. /)[1]
+          end
+          buffer += line[indent..]
+        end
+        output.append(make_element(buffer, num)) unless buffer.empty?
+        output
+      end
+
+      # rubocop:enable Metrics/AbcSize
+      private
+
+      def make_element(text, num)
+        element = ::RBMark::DOM::ListElement.parse(text)
+        element.property num: num.to_i
+        element
+      end
+    end
+
+    # Quote block parser
+    class QuoteChunkParser < ChunkParser
+      # Tests for chunk being a block quote
+      # @param text [String]
+      # @return [Boolean]
+      def match?(text)
+        text.lines.map do |x|
+          x.match?(/\A\s*>(?:\s[^\n\r]+|)\Z/m)
+        end.all?(true)
+      end
+
+      # Transforms text chunk into a block quote
+      # @param text
+      # @return [::RBMark::DOM::QuoteBlock]
+      def match(text)
+        text = text.lines.map do |x|
+          x.match(/\A\s*>(\s[^\n\r]+|)\Z/m)[1].to_s[1..]
+        end.join("\n")
+        ::RBMark::DOM::QuoteBlock.parse(text)
+      end
+    end
+
+    # Paragraph block
+    class ParagraphChunkParser < ChunkParser
+      # Acts as a fallback for the basic paragraph chunk
+      # @param text [String]
+      # @return [Boolean]
+      def match?(_text)
+        true
+      end
+
+      # Creates a new paragraph with the given text
+      def match(text)
+        ::RBMark::DOM::Paragraph.parse(text)
+      end
+    end
+
+    # Code block
+    class CodeChunkParser < ChunkParser
+      # Check if a block matches the given parser rule
+      # @param text [String]
+      # @return [Boolean]
+      def match?(text)
+        text.match?(/\A```\w+[\r\n]{1,2}.*[\r\n]{1,2}```\Z/m)
+      end
+
+      # Create a new element
+      def match(text)
+        lang, code = text.match(
+          /\A```(\w+)[\r\n]{1,2}(.*)[\r\n]{1,2}```\Z/m
+        )[1, 2]
+        element = ::RBMark::DOM::CodeBlock.new
+        element.property language: lang
+        element.content = code
+        element
+      end
+    end
+
+    # Heading chunk parser
+    class HeadingChunkParser < ChunkParser
+      # Check if a block matches the given parser rule
+      # @param text [String]
+      # @return [Boolean]
+      def match?(text)
+        text.match?(/\A\#{1,4}\s/)
+      end
+
+      # Create a new element
+      def match(text)
+        case text.match(/\A\#{1,4}\s/)[0]
+        when "# " then ::RBMark::DOM::Heading1.parse(text[2..])
+        when "## " then ::RBMark::DOM::Heading2.parse(text[3..])
+        when "### " then ::RBMark::DOM::Heading3.parse(text[4..])
+        when "#### " then ::RBMark::DOM::Heading4.parse(text[5..])
+        end
+      end
+    end
+
+    # Unordered list parser (chunk)
+    class UnorderedChunkParser < ChunkParser
+      # Check if a block matches the given parser rule
+      # @param text [String]
+      # @return [Boolean]
+      def match?(text)
+        return false unless text.start_with? "- "
+
+        text.lines.map do |line|
+          line.match?(/\A(?:- .*|  .*|  )\Z/)
+        end.all?(true)
+      end
+
+      # Create a new element
+      def match(text)
+        ::RBMark::DOM::ULBlock.parse(text)
+      end
+    end
+
+    # Ordered list parser (chunk)
+    class OrderedChunkParser < ChunkParser
+      # Check if a block matches the given parser rule
+      # @param text [String]
+      # @return [Boolean]
+      def match?(text)
+        return false unless text.start_with?(/\d+\. /)
+
+        indent = 0
+        text.lines.each do |line|
+          if line.start_with?(/\d+\. /)
+            indent = line.match(/\A\d+\. /)[0].length
+          elsif line.start_with?(/\s+/)
+            return false if line.match(/\A\s+/)[0].length < indent
+          else
+            return false
+          end
+        end
+        true
+      end
+
+      # Create a new element
+      def match(text)
+        ::RBMark::DOM::OLBlock.parse(text)
+      end
+    end
+
+    # Indented block parser
+    class IndentChunkParser < ChunkParser
+      # Check if a block matches the given parser rule
+      # @param text [String]
+      # @return [Boolean]
+      def match?(text)
+        text.lines.map do |x|
+          x.start_with? "    " or x.start_with? "\t"
+        end.all?(true)
+      end
+
+      # Create a new element
+      def match(text)
+        text = text.lines.map { |x| x.match(/\A(?: {4}|\t)(.*)\Z/)[1] }
+                   .join("\n")
+        ::RBMark::DOM::IndentBlock.parse(text)
+      end
+    end
+
+    # Stub text parser
+    class TextInlineParser < InlineParser
+      # Stub method for creating new Text object
+      def match(text)
+        instance = ::RBMark::DOM::Text.new
+        instance.content = text
+        instance
+      end
+    end
+
+    # Bold text
+    class BoldInlineParser < InlineParser
+      def initialize
+        super
+        @match_exp = /(?<!\\)\*\*+.+?(?<!\\)\*+\*/
+      end
+
+      # Match element
+      def match(text)
+        ::RBMark::DOM::InlineBold.parse(text[2..-3])
+      end
+    end
+
+    # Italics text
+    class ItalicsInlineParser < InlineParser
+      def initialize
+        super
+        @match_exp = /(?<!\\)\*+.+?(?<!\\)\*+/
+      end
+
+      # Match element
+      def match(text)
+        ::RBMark::DOM::InlineItalics.parse(text[1..-2])
+      end
+    end
+
+    # Underlined text
+    class UnderInlineParser < InlineParser
+      def initialize
+        super
+        @match_exp = /(?<!\\)__+.+?(?<!\\)_+_/
+      end
+
+      # Match element
+      def match(text)
+        ::RBMark::DOM::InlineUnder.parse(text[2..-3])
+      end
+    end
+
+    # Strikethrough text
+    class StrikeInlineParser < InlineParser
+      def initialize
+        super
+        @match_exp = /(?<!\\)~~+.+?(?<!\\)~+~/
+      end
+
+      # Match element
+      def match(text)
+        ::RBMark::DOM::InlineStrike.parse(text[2..-3])
+      end
+    end
+
+    # Preformatted text
+    class PreInlineParser < InlineParser
+      def initialize
+        super
+        @match_exp = /(?<!\\)``+.+?(?<!\\)`+`/
+      end
+
+      # Match element
+      def match(text)
+        ::RBMark::DOM::InlinePre.parse(text[2..-3])
+      end
+    end
+
+    # Hyperreference link
+    class LinkInlineParser < InlineParser
+      def initialize
+        super
+        @match_exp = /(?<![\\!])\[(.+?(?<!\\))\]\((.+?(?<!\\))\)/
+      end
+
+      # Match element
+      def match(text)
+        title, link = text.match(@match_exp)[1..2]
+        element = ::RBMark::DOM::InlineLink.new
+        element.content = title
+        element.property link: link
+        element
+      end
+    end
+
+    # Image
+    class ImageInlineParser < InlineParser
+      def initialize
+        super
+        @match_exp = /(?<!\\)!\[(.+?(?<!\\))\]\((.+?(?<!\\))\)/
+      end
+
+      # Match element
+      def match(text)
+        title, link = text.match(@match_exp)[1..2]
+        element = ::RBMark::DOM::InlineImage.new
+        element.content = title
+        element.property link: link
+        element
+      end
+    end
+
+    # Linebreak
+    class BreakInlineParser < InlineParser
+      def initialize
+        super
+        @match_exp = /\s{2}/
+      end
+
+      # Match element
+      def match(_text)
+        element = ::RBMark::DOM::InlineBreak.new
+        element.content = ""
+        element
+      end
+    end
+  end
+
+  # Module for representing abstract object hierarchy
+  module DOM
+    # Abstract container
+    class DOMObject
+      class << self
+        attr_accessor :parsers
+        attr_reader :slicer
+
+        # Hook for initializing variables
+        def inherited(subclass)
+          super
+          # Inheritance initialization
+          subclass.slicer = @slicer if @slicer
+          subclass.parsers = @parsers.dup if @parsers
+          subclass.parsers ||= []
+        end
+
+        # Initialize parsers for the current class
+        def initialize_parsers
+          @active_parsers = @parsers.map(&:new)
+          @active_slicer = @slicer.new if @slicer
+        end
+
+        # Add a slicer
+        # @param parser [Object]
+        def slicer=(parser)
+          unless parser < ::RBMark::Parsers::Slicer
+            raise StandardError, "#{x} is not a Slicer"
+          end
+
+          @slicer = parser
+        end
+
+        # Add a parser to the chain
+        # @param parser [Object]
+        def parser(parser)
+          unless [::RBMark::Parsers::InlineParser,
+                  ::RBMark::Parsers::ChunkParser].any? { |x| parser < x }
+            raise StandardError, "#{x} is not an InlineParser or a ChunkParser"
+          end
+
+          @parsers.append(parser)
+        end
+
+        # Parse text from the given context
+        # @param text [String]
+        # @return [self]
+        def parse(text)
+          initialize_parsers
+          container = new
+          container.content = text
+          _parse(container)
+          container.content = "" unless container.is_a? ::RBMark::DOM::Text
+          container
+        end
+
+        private
+
+        def _parse(instance)
+          return unless @active_slicer
+
+          @active_slicer.chunk_parsers = @active_parsers
+          instance.children.append(*@active_slicer.parse(instance.content))
+        end
+      end
+
+      def initialize
+        @content = nil
+        @children = []
+        @properties = {}
+      end
+
+      # Set certain property in the properties hash
+      # @param properties [Hash] proeprties to update
+      def property(**properties)
+        @properties.update(**properties)
+      end
+
+      # Add child to container
+      # @param child [DOMObject]
+      def append(*children)
+        unless children.all? { |x| x.is_a? DOMObject }
+          raise StandardError, "#{x} is not a DOMObject"
+        end
+
+        @children.append(*children)
+      end
+
+      # Insert a child into the container
+      # @param child [DOMObject]
+      # @param index [Integer]
+      def insert(index, child)
+        raise StandardError, "not a DOMObject" unless child.is_a? DOMObject
+
+        @children.insert(index, child)
+      end
+
+      # Delete a child from container
+      # @param index [Integer]
+      def delete_at(index)
+        @children.delete_at(index)
+      end
+
+      # Get a child from the container
+      # @param key [Integer]
+      def [](key)
+        @children[key]
+      end
+
+      # Set text content of a DOMObject
+      # @param text [String]
+      def content=(text)
+        raise StandardError, "not a String" unless text.is_a? String
+
+        @content = text
+      end
+
+      # Get text content of a DOMObject
+      # @return [String, nil]
+      attr_reader :content, :children, :properties
+    end
+
+    # Document root
+    class Document < DOMObject
+      self.slicer = ::RBMark::Parsers::RootSlicer
+      parser ::RBMark::Parsers::IndentChunkParser
+      parser ::RBMark::Parsers::QuoteChunkParser
+      parser ::RBMark::Parsers::HeadingChunkParser
+      parser ::RBMark::Parsers::CodeChunkParser
+      parser ::RBMark::Parsers::UnorderedChunkParser
+      parser ::RBMark::Parsers::OrderedChunkParser
+      parser ::RBMark::Parsers::ParagraphChunkParser
+    end
+
+    # Inline text
+    class Text < DOMObject
+      def self.parse(text)
+        instance = super(text)
+        instance.content = instance.content.gsub(/[\s\r\n]+/, " ")
+        instance
+      end
+    end
+
+    # Inline preformatted text
+    class InlinePre < DOMObject
+      self.slicer = ::RBMark::Parsers::InlineSlicer
+    end
+
+    # Infline formattable text
+    class InlineFormattable < DOMObject
+      self.slicer = ::RBMark::Parsers::InlineSlicer
+      parser ::RBMark::Parsers::BreakInlineParser
+      parser ::RBMark::Parsers::BoldInlineParser
+      parser ::RBMark::Parsers::ItalicsInlineParser
+      parser ::RBMark::Parsers::PreInlineParser
+      parser ::RBMark::Parsers::UnderInlineParser
+      parser ::RBMark::Parsers::StrikeInlineParser
+      parser ::RBMark::Parsers::LinkInlineParser
+      parser ::RBMark::Parsers::ImageInlineParser
+    end
+
+    # Bold text
+    class InlineBold < InlineFormattable
+    end
+
+    # Italics text
+    class InlineItalics < InlineFormattable
+    end
+
+    # Underline text
+    class InlineUnder < InlineFormattable
+    end
+
+    # Strikethrough text
+    class InlineStrike < InlineFormattable
+    end
+
+    # Hyperreferenced text
+    class InlineLink < InlineFormattable
+    end
+
+    # Image
+    class InlineImage < DOMObject
+    end
+
+    # Linebreak
+    class InlineBreak < DOMObject
+    end
+
+    # Heading level 1
+    class Heading1 < InlineFormattable
+    end
+
+    # Heading level 2
+    class Heading2 < Heading1
+    end
+
+    # Heading level 3
+    class Heading3 < Heading1
+    end
+
+    # Heading level 4
+    class Heading4 < Heading1
+    end
+
+    # Preformatted code block
+    class CodeBlock < DOMObject
+    end
+
+    # Quote block
+    class QuoteBlock < Document
+    end
+
+    # Table
+    class TableBlock < DOMObject
+    end
+
+    # Unordered list
+    class ULBlock < DOMObject
+      self.slicer = ::RBMark::Parsers::UnorderedSlicer
+    end
+
+    # Ordered list block
+    class OLBlock < DOMObject
+      self.slicer = ::RBMark::Parsers::OrderedSlicer
+    end
+
+    # Indent block
+    class IndentBlock < Document
+    end
+
+    # List element
+    class ListElement < Document
+    end
+
+    # Horizontal rule
+    class HorizontalRule < DOMObject
+    end
+
+    # Paragraph in a document (separated by 2 newlines)
+    class Paragraph < InlineFormattable
+    end
+  end
+end
diff --git a/mdpp.rb b/mdpp.rb
new file mode 100644
index 0000000..a5e23e8
--- /dev/null
+++ b/mdpp.rb
@@ -0,0 +1,377 @@
+#!/usr/bin/ruby
+# frozen_string_literal: true
+
+require_relative 'document'
+require 'io/console'
+require 'io/console/size'
+
+module MDPP
+  # Module for managing terminal output
+  module TextManager
+    # ANSI SGR escape code for bg color
+    # @param text [String]
+    # @param properties [Hash]
+    # @return [String]
+    def bg(text, properties)
+      color = properties['bg']
+      if color.is_a? Integer
+        "\e[48;5;#{color}m#{text}\e[49m"
+      elsif color.is_a? String and color.match?(/\A#[A-Fa-f0-9]{6}\Z/)
+        vector = color.scan(/[A-Fa-f0-9]{2}/).map { |x| x.to_i(16) }
+        "\e[48;2;#{vector[0]};#{vector[1]};#{vector[2]}\e[49m"
+      else
+        Kernel.warn "WARNING: Invalid color - #{color}"
+        text
+      end
+    end
+
+    # ANSI SGR escape code for fg color
+    # @param text [String]
+    # @param properties [Hash]
+    # @return [String]
+    def fg(text, properties)
+      color = properties['fg']
+      if color.is_a? Integer
+        "\e[38;5;#{color}m#{text}\e[39m"
+      elsif color.is_a? String and color.match?(/\A#[A-Fa-f0-9]{6}\Z/)
+        vector = color.scan(/[A-Fa-f0-9]{2}/).map { |x| x.to_i(16) }
+        "\e[38;2;#{vector[0]};#{vector[1]};#{vector[2]}\e[39m"
+      else
+        Kernel.warn "WARNING: Invalid color - #{color}"
+        text
+      end
+    end
+
+    # ANSI SGR escape code for bold text
+    # @param text [String]
+    # @return [String]
+    def bold(text)
+      "\e[1m#{text}\e[22m"
+    end
+
+    # ANSI SGR escape code for italics text
+    # @param text [String]
+    # @return [String]
+    def italics(text)
+      "\e[3m#{text}\e[23m"
+    end
+
+    # ANSI SGR escape code for underline text
+    # @param text [String]
+    # @return [String]
+    def underline(text)
+      "\e[4m#{text}\e[24m"
+    end
+
+    # ANSI SGR escape code for strikethrough text
+    # @param text [String]
+    # @return [String]
+    def strikethrough(text)
+      "\e[9m#{text}\e[29m"
+    end
+
+    # Word wrapping algorithm
+    # @param text [String]
+    # @param width [Integer]
+    # @return [String]
+    def wordwrap(text, width)
+      words = text.split(/ +/)
+      output = []
+      line = ""
+      until words.empty?
+        word = words.shift
+        if word.length > width
+          words.prepend(word[width..])
+          word = word[..width - 1]
+        end
+        if line.length + word.length + 1 > width
+          output.append(line.lstrip)
+          line = word
+          next
+        end
+        line = [line, word].join(' ')
+      end
+      output.append(line.lstrip)
+      newtext = output.join("\n")
+      newtext
+    end
+
+    # Draw a screen-width box around text
+    # @param text [String]
+    # @param center_margins [Integer]
+    # @return [String]
+    def box(text)
+      size = IO.console.winsize[1] - 2
+      text = wordwrap(text, (size * 0.8).floor).lines.filter_map do |line|
+        "│#{line.strip.ljust(size)}│" unless line.empty?
+      end.join("\n")
+      <<~TEXT
+        ╭#{'─' * size}╮
+        #{text}
+        ╰#{'─' * size}╯
+      TEXT
+    end
+
+    # Draw text right-justified
+    def rjust(text)
+      size = IO.console.winsize[1]
+      wordwrap(text, (size * 0.8).floor).lines.filter_map do |line|
+        line.strip.rjust(size) unless line.empty?
+      end.join("\n")
+    end
+
+    # Draw text centered
+    def center(text)
+      size = IO.console.winsize[1]
+      wordwrap(text, (size * 0.8).floor).lines.filter_map do |line|
+        line.strip.center(size) unless line.empty?
+      end.join("\n")
+    end
+
+    # Underline the last line of the text piece
+    def underline_block(text)
+      textlines = text.lines
+      last = "".match(/()()()/)
+      textlines.each do |x|
+        current = x.match(/\A(\s*)(.+?)(\s*)\Z/)
+        last = current if current[2].length > last[2].length
+      end
+      ltxt = last[1]
+      ctxt = textlines.last.slice(last.offset(2)[0]..last.offset(2)[1] - 1)
+      rtxt = last[3]
+      textlines[-1] = [ltxt, underline(ctxt), rtxt].join('')
+      textlines.join("")
+    end
+
+    # Add extra newlines around the text
+    def extra_newlines(text)
+      size = IO.console.winsize[1]
+      textlines = text.lines
+      textlines.prepend("#{' ' * size}\n")
+      textlines.append("\n#{' ' * size}\n")
+      textlines.join("")
+    end
+
+    # Underline last line edge to edge
+    def underline_full_block(text)
+      textlines = text.lines
+      textlines[-1] = underline(textlines.last)
+      textlines.join("")
+    end
+
+    # Indent all lines
+    def indent(text, properties)
+      _indent(text, level: properties['level'])
+    end
+
+    # Indent all lines (inner)
+    def _indent(text, **_useless)
+      text.lines.map do |line|
+        "    #{line}"
+      end.join("")
+    end
+
+    # Bulletpoints
+    def bullet(text, _number, properties)
+      level = properties['level']
+      "-#{_indent(text, level: level)[1..]}"
+    end
+
+    # Numbers
+    def numbered(text, number, properties)
+      level = properties['level']
+      "#{number}.#{_indent(text, level: level)[number.to_s.length + 1..]}"
+    end
+  end
+
+  DEFAULT_STYLE = {
+    "RBMark::DOM::Paragraph" => {
+      "inline" => true,
+      "indent" => true
+    },
+    "RBMark::DOM::Text" => {
+      "inline" => true
+    },
+    "RBMark::DOM::Heading1" => {
+      "inline" => true,
+      "center" => true,
+      "bold" => true,
+      "extra_newlines" => true,
+      "underline_full_block" => true
+    },
+    "RBMark::DOM::Heading2" => {
+      "inline" => true,
+      "center" => true,
+      "underline_block" => true
+    },
+    "RBMark::DOM::Heading3" => {
+      "inline" => true,
+      "underline" => true,
+      "bold" => true
+    },
+    "RBMark::DOM::Heading4" => {
+      "inline" => true,
+      "underline" => true
+    },
+    "RBMark::DOM::InlineImage" => {
+      "inline" => true
+    },
+    "RBMark::DOM::InlineLink" => {
+      "inline" => true
+    },
+    "RBMark::DOM::InlinePre" => {
+      "inline" => true
+    },
+    "RBMark::DOM::InlineStrike" => {
+      "inline" => true,
+      "strikethrough" => true
+    },
+    "RBMark::DOM::InlineUnder" => {
+      "inline" => true,
+      "underline" => true
+    },
+    "RBMark::DOM::InlineItalics" => {
+      "inline" => true,
+      "italics" => true
+    },
+    "RBMark::DOM::InlineBold" => {
+      "inline" => true,
+      "bold" => true
+    },
+    "RBMark::DOM::ULBlock" => {
+      "bullet" => true
+    },
+    "RBMark::DOM::OLBlock" => {
+      "numbered" => true
+    }
+  }.freeze
+
+  STYLE_PRIO0 = [
+    ["numbered", true],
+    ["bullet", true]
+  ].freeze
+
+  STYLE_PRIO1 = [
+    ["center", false],
+    ["rjust", false],
+    ["box", false],
+    ["indent", true],
+    ["underline", false],
+    ["bold", false],
+    ["italics", false],
+    ["strikethrough", false],
+    ["bg", true],
+    ["fg", true],
+    ["extra_newlines", false],
+    ["underline_block", false],
+    ["underline_full_block", false]
+  ].freeze
+
+  # Primary document renderer
+  class Renderer
+    include ::MDPP::TextManager
+
+    # @param input [String]
+    # @param options [Hash]
+    def initialize(input, options)
+      @doc = RBMark::DOM::Document.parse(input)
+      pp @doc
+      @color_mode = options.fetch("color", true)
+      @ansi_mode = options.fetch("ansi", true)
+      @style = ::MDPP::DEFAULT_STYLE.dup
+      return unless options['style']
+
+      @style = @style.map do |k, v|
+        v = v.merge(**options['style'][k]) if options['style'][k]
+        [k, v]
+      end.to_h
+    end
+
+    # Return rendered text
+    # @return [String]
+    def render
+      _render(@doc.children, @doc.properties)
+    end
+
+    private
+
+    def _render(children, props)
+      blocks = children.map do |child|
+        if child.is_a? ::RBMark::DOM::Text or
+           child.is_a? ::RBMark::DOM::CodeBlock
+          child.content
+        elsif child.is_a? ::RBMark::DOM::InlineBreak
+          "\n"
+        else
+          child_props = get_props(child, props)
+          calc_wordwrap(
+            _render(child.children,
+                    child_props),
+            props, child_props
+          )
+        end
+      end
+      apply_props(blocks, props)
+    end
+
+    def calc_wordwrap(obj, props, obj_props)
+      size = IO.console.winsize[1]
+      return obj if obj_props['center'] or
+                    obj_props['rjust']
+
+      if !props['inline'] and obj_props['inline']
+        wordwrap(obj, size - 2 * (props['level'].to_i + 1))
+      else
+        obj
+      end
+    end
+
+    def get_props(obj, props)
+      new_props = @style[obj.class.to_s].dup || {}
+      if props["level"]
+        new_props["level"] = props["level"]
+        new_props["level"] += 1 unless new_props["inline"]
+      else
+        new_props["level"] = 2
+      end
+      new_props
+    end
+
+    def apply_props(blockarray, properties)
+      blockarray = prio0(blockarray, properties)
+      text = blockarray.join(properties['inline'] ? "" : "\n\n")
+                       .gsub(/\n{2,}/, "\n\n")
+      prio1(text, properties)
+    end
+
+    def prio0(blocks, props)
+      ::MDPP::STYLE_PRIO0.filter { |x| props.include? x[0] }.each do |style|
+        blocks = blocks.map.with_index do |block, index|
+          if style[1]
+            method(style[0].to_s).call(block, index + 1, props)
+          else
+            method(style[0].to_s).call(block, index + 1)
+          end
+        end
+      end
+      blocks
+    end
+
+    def prio1(block, props)
+      ::MDPP::STYLE_PRIO1.filter { |x| props.include? x[0] }.each do |style|
+        block = if style[1]
+                  method(style[0].to_s).call(block, props)
+                else
+                  method(style[0].to_s).call(block)
+                end
+      end
+      block
+    end
+  end
+end
+
+if __FILE__ == $0
+  text = $stdin.read
+  renderer = MDPP::Renderer.new(text, {})
+  puts renderer.render
+end
diff --git a/test.md b/test.md
new file mode 100644
index 0000000..a7a0bf7
--- /dev/null
+++ b/test.md
@@ -0,0 +1,81 @@
+# Header level sadga kjshdkj hasdkjs hakjdhakjshd kashd kjashd kjashdk asjhdkj ashdkj ahskj hdaskd haskj hdkjash dkjashd ksajdh askjd hak   askjhdkasjhdaksjhd sakjd    1  
+
+> Block quote text
+>
+> Second block quote paragraph
+> Block quote **bold** and *italics* test
+> Block quote **bold *italics* mix** test
+
+## Header level 2
+
+[link](http://example.com)
+![image alt text](http://example.com)
+
+```plaintext
+code *block*
+eat my shit
+```
+
+paragraph with ``inline code block``
+
+- Unordered list element 1
+- Unordered list element 2
+
+1. Ordered list element 1
+2. Ordered list element 2
+
+This is not a list
+- because it continues the paragraph
+- this is how it should be, like it or not
+
+- This is also not a list
+because there is text on the next line
+
+- But this here is a list
+  because the spacing is made correctly
+  
+  more so than that, there are multiple paragraphs here!
+  
+  - AND even more lists in a list!
+  - how extra
+- And this is just the next element in the list
+
+1. same thing but with ordered lists
+   ordered lists have a little extra special property to them
+   
+   the indentations are always symmetrical to the last space of the bullet's number
+10. i.e., if you look at this here example
+    this will work
+    
+    obviously
+
+
+1. But this
+10. Won't  
+   because the indentation doesn't match the start of the line.
+
+generally speaking this kind of insane syntax trickery won't be necessary,
+but it's just better to have standards than to have none of them.
+
+an unfortunate side effect of this flexibility should also be noted, and  
+it's that markdown linters don't like this sort of stuff.
+Yet another reason not to use a markdown linter.
+
+- And this is just the lame stupid old way to do this, as described by mardkownguide
+
+    > just indent your stuff and it works
+    > really it's as simple as that.
+    > bruh
+
+    there can be as many as infinite number of elements appended to the list that way.
+
+    you can even start a sublist here if you want to
+
+    - here's a new nested list
+    - could you imagine the potential
+
+    and here's an image of nothing
+
+    ![image](https://example.com/nothing.png)
+
+- I may also need to merge lists for this to work properly