Browse Source

finally

master
Yessiest 3 months ago
parent
commit
38e0ee2dfb
  1. 13
      classes
  2. 756
      document.rb
  3. 217
      markdown.rb
  4. 430
      mdpp.rb
  5. 105
      test.md
  6. 118
      test.rb

13
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 []

756
document.rb

@ -0,0 +1,756 @@
# 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
text = ::RBMark::DOM::Text.new
text.content = code
element.append(text)
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
# Horizontal Rule block parser
class HRChunkParser < ChunkParser
# Check if a block matches the given parser rule
# @param text [String]
# @return [Boolean]
def match?(text)
text.match?(/\A-{3,}\Z/)
end
# Create a new element
def match(text)
element = ::RBMark::DOM::HorizontalRule.new()
element.content = ""
element
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.parse(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.parse(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::HRChunkParser
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 < InlinePre
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

217
markdown.rb

@ -1,217 +0,0 @@
## Filter-based Markdown translator.
#
module Markdown
## Superclass that defines behaviour of all translators
# @abstract Don't use directly - it only defins the ability to chain translators
class AbstractTranslator
attr_accessor :input
attr_accessor :output
def initialize()
@chain = []
end
def +(nextTranslator)
@chain.append nextTranslator
return self
end
def to_html
output = @output
@chain.each { |x|
x = x.new(output) if x.class == Class
x.to_html
output = x.output
}
return output
end
end
module_function
def html_highlighter; @html_highlighter end
def html_highlighter= v; @html_highlighter = v end
## Translator for linear tags in Markdown.
# A linear tag is any tag that starts anywhere on the line, and closes on the same exact line.
class LinearTagTranslator < AbstractTranslator
def initialize(text)
@input = text
@output = text
super()
end
def to_html
@output = @input
# Newline
.sub(/\s{2}[\n\r]/,"<br/>")
# Inline code (discord style)
.gsub(/(?<!\\)``(.*?[^\\])``/) {
code = Regexp.last_match[1]
"<code>#{code.gsub /[*`~_!\[]/,"\\\\\\0"}</code>"
}
# Inline code (Markdown style)
.gsub(/(?<!\\)`(.*?[^\\])`/) {
code = Regexp.last_match[1]
"<code>#{code.gsub /[*`~_!\[]/,"\\\\\\0"}</code>"
}
# Bold-italics
.gsub(/(?<!\\)\*\*\*(.*?[^\\])\*\*\*/,"<i><b>\\1</b></i>")
# Bold
.gsub(/(?<!\\)\*\*(.*?[^\\])\*\*/,"<b>\\1</b>")
# Italics
.gsub(/(?<!\\)\*(.*?[^\\])\*/,"<i>\\1</i>")
# Strikethrough
.gsub(/(?<!\\)~~(.*?[^\\])~~/,"<s>\\1</s>")
# Underline
.gsub(/(?<!\\)__(.*?[^\\])__/,"<span style=\"text-decoration: underline\">\\1</span>")
# Image
.gsub(/(?<!\\)!\[(.*)\]\((.*)\)/,"<img src=\"\\2\" alt=\"\\1\" />")
# Link
.gsub(/(?<!\\)\[(.*)\]\((.*)\)/,"<a href=\"\\2\">\\1</a>")
super
end
end
## Translator for linear leftmost tags.
# Leftmost linear tags open on the leftmost end of the string, and close once the line ends. These tags do not need to be explicitly closed.
class LeftmostTagTranslator < AbstractTranslator
def initialize(text)
@input = text
@output = text
super()
end
def to_html
# Headers
@output = @input.split("\n").map do |x|
x.gsub(/^(?<!\\)(\#{1,4})([^\n\r]*)/) {
level,content = Regexp.last_match[1..2]
"<h#{level.length}>"+content+"</h#{level.length}>"
}.gsub(/^\-{3,}/,"<hr>")
end.join("\n")
super
end
end
## Translator for code blocks in markdown
# Code blocks can have syntax highlighting. This class implements an attribute for providing a syntax highlighter, one handler per requested output.
class CodeBlockTranslator < AbstractTranslator
def initialize(text)
@input = text
@output = text
super()
end
def to_html
@output = @input.gsub(/(?:\n|^)```([\w_-]*)([\s\S]+?)```/) {
language,code = Regexp.last_match[1..2]
code = Markdown::html_highlighter.call(language,code) if Markdown::html_highlighter
"<pre><code>#{code.gsub /[|#*`~_!\[]/,"\\\\\\0"}</code></pre>"
}
super()
end
end
## Translator for quotes in Markdown.
# These deserve their own place in hell. As if the "yaml with triangle brackets instead of spaces" syntax wasn't horrible enough, each quote is its own markdown context.
class QuoteTranslator < AbstractTranslator
def initialize(text)
if text.is_a? Array then
@lines = text
elsif text.is_a? String then
@lines = text.split("\n")
end
@output = text
super()
end
def input= (v)
@lines = v.split("\n")
@output = v
end
def input
@lines.join("\n")
end
def to_html
stack = []
range = []
@lines.each_with_index { |x,index|
if x.match /^\s*> ?/ then
range[0] = index if not range[0]
range[1] = index
else
stack.append(range[0]..range[1]) if range[0] and range[1]
range = []
end
}
stack.append(range[0]..range[1]) if range[0] and range[1]
stack.reverse.each { |r|
@lines[r.begin] = "<blockquote>\n"+@lines[r.begin]
@lines[r.end] = @lines[r.end]+"\n</blockquote>"
@lines[r] = @lines[r].map { |line|
line.sub /^(\s*)> ?/,"\\1 "
}
@lines[r] = QuoteTranslator.new(@lines[r]).to_html
}
@output = @lines.join("\n")
super
end
end
## Table parser
# translates tables from a format in markdown to an html table
class TableTranslator < AbstractTranslator
def initialize(text)
@input = text
@output = text
super()
end
def to_html
lines = @output.split("\n")
table_testline = -1
table_start = -1
table_column_count = 0
tables = []
cur_table = []
lines.each_with_index { |line,index|
if (table_start != -1) and (line.match /^\s*\|([^\|]*\|){#{table_column_count-1}}$/) then
if (table_testline == -1) then
if (line.match /^\s*\|(\-*\|){#{table_column_count-1}}$/) then
table_testline = 1
else
table_start = -1
cur_table = []
end
else
cur_table.push (line.split("|").filter_map { |x| x.strip if x.match /\S+/ })
end
elsif (table_start != -1) then
obj = {table: cur_table, start: table_start, end: index}
tables.push(obj)
table_start = -1
cur_table = []
table_testline = -1
table_column_count = 0
end
if (table_start == -1) and (line.start_with? /\s*\|/ ) and (line.match /^\s*\|.*\|/) then
table_start = index
table_column_count = line.count "|"
cur_table.push (line.split("|").filter_map { |x| x.strip if x.match /\S+/ })
end
}
if cur_table != [] then
obj = {table: cur_table, start:table_start, end: lines.count-1}
tables.push(obj)
end
tables.reverse.each { |x|
lines[x[:start]..x[:end]] = (x[:table].map do |a2d|
(a2d.map { |x| (x.start_with? "#") ? " <th>"+x.sub(/^#\s+/,"")+"</th>" : " <td>"+x+"</td>"}).prepend(" <tr>").append(" </tr>")
end).flatten.prepend("<table>").append("</table>")
}
@output = lines.join("\n")
super()
end
end
# Backslash cleaner
# Cleans excessive backslashes after the translation
class BackslashTranslator < AbstractTranslator
def initialize(text)
@input = text
@output = text
end
def to_html
@output = @input.gsub(/\\(.)/,"\\1")
end
end
end

430
mdpp.rb

@ -0,0 +1,430 @@
#!/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)
output.join("\n")
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
# Sideline for quotes
def sideline(text)
text.lines.map do |line|
"#{line}"
end.join("")
end
# Long bracket for code blocks
def longbracket(text, properties)
puts properties.inspect
textlines = text.lines
textlines = textlines.map do |line|
"#{line}"
end
textlines.prepend("┌ (#{properties['element'][:language]})\n")
textlines.append("\n\n")
textlines.join("")
end
# Add text to bibliography
def bibliography(text, properties)
@bibliography.append([text, properties['element'][:link]])
"#{text}[#{@bibliography.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,
"indent" => true
},
"RBMark::DOM::Heading4" => {
"inline" => true,
"underline" => true,
"indent" => true
},
"RBMark::DOM::InlineImage" => {
"bibliography" => true,
"inline" => true
},
"RBMark::DOM::InlineLink" => {
"bibliography" => true,
"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::QuoteBlock" => {
"sideline" => true
},
"RBMark::DOM::CodeBlock" => {
"longbracket" => true
},
"RBMark::DOM::ULBlock" => {
"bullet" => true
},
"RBMark::DOM::OLBlock" => {
"numbered" => true
},
"RBMark::DOM::HorizontalRule" => {
"extra_newlines" => 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],
["bibliography", true],
["extra_newlines", false],
["sideline", false],
["longbracket", true],
["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)
@color_mode = options.fetch("color", true)
@ansi_mode = options.fetch("ansi", true)
@style = ::MDPP::DEFAULT_STYLE.dup
@bibliography = []
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
text = _render(@doc.children, @doc.properties)
text += _render_bibliography unless @bibliography.empty?
text
end
private
def _render_bibliography
size = IO.console.winsize[1]
text = "\n#{('─' * size)}\n"
text += @bibliography.map.with_index do |element, index|
"- [#{index + 1}] #{wordwrap(element.join(': '), size - 15)}"
end.join("\n")
text
end
def _render(children, props)
blocks = children.map do |child|
case child
when ::RBMark::DOM::Text then child.content
when ::RBMark::DOM::InlineBreak then "\n"
when ::RBMark::DOM::HorizontalRule
size = IO.console.winsize[1]
"" * size
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["element"] = obj.properties
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

105
test.md

@ -0,0 +1,105 @@
# 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
### Third level header
text
#### 4th level text
- list
- > list with blockquote
> quote
> more of the same quote
>
> - inner list
> - list2
>
> ```plaintext
> code block inside a quote
> could you imagine that shit
> eh
> ```
---
gnomo

118
test.rb

@ -1,118 +0,0 @@
require_relative "markdown"
puts Markdown::LinearTagTranslator.new(<<CODE
*Italics*
**Bold**
***Bolitalics***
__underline__
__underline plus ***bolitalics***__
___invalid underline___
~~strikethrough ~~
`code that ignores ***all*** __Markdown__ [tags](https://nevergonnagiveyouup)`
me: google en passant
them: [holy hell!](https://google.com/q?=en+passant)
CODE
).to_html
puts Markdown::LeftmostTagTranslator.new(<<CODE
# Header v1
## Header v2
### Header v3
#### Header v4
##### Invalid header
#### Not a header
*** Also #### Not a header ***
CODE
).to_html
puts Markdown::QuoteTranslator.new(<<CODE
> Quote begins
>
> yea
> # header btw
> > nextlevel quote
> > more quote
> > those are quotes
> > yes
> > > third level quote
> > > yes
> > second level again
> > > third level again
> > second level oioioi
> >
> > > third
> > >
> > >
> > >
>
>
>
> fin
CODE
).to_html
puts Markdown::CodeBlockTranslator.new(<<CODE
```markdown
shmarkshmark
# pee pee
# piss
**ass**
__cock__
cock__
piss__
`shmark shmark`
```
CODE
).to_html
test = (Markdown::CodeBlockTranslator.new(<<TEXT
# Markdown garbage gallery
## Header level 2
### Header level 3
#### Header level 4
__[Underlined Link](https://google.com)__
__**unreal shitworks**__
split
---
![Fucking image idk](https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Ftse3.explicit.bing.net%2Fth%3Fid%3DOIP.qX1HmpFNHyaTfXv-SLnAJgHaDD%26pid%3DApi&f=1&ipt=dc0e92fdd701395eda76714338060dcf91c7ff9e228f108d8af6e1ba3decd1c2&ipo=images)
> Here's a bunch of shit i guess lmao idk
```markdown
test
test
test
|1|2|3|
|-|-|-|
|a|b|c|
| uneven rows | test | yes |
|-|-|-|
| sosiska | dinozavri | suda pihaem |
| sosiska 2 | vitalya 2 | brat 2 |
*** test ***
piss
cock
__cock__
# hi
```
> ok
> here i go pissing
> ***time to take a piss***
> > pissing
> > "what the hell are you doing"
> > i'm taking a pieeees
> > "why areyou not jomping at me thats what yourshupposed to do
> > I might do it focking later
> > ok
> # bug
> __cum__
__mashup__
| # sosiska | sosiska | suda pihaem |
|-|-|-|
| # 2 | chuvak ya ukral tvayu sardelku ))0)))0))))))) | __blya ((9((9((9)__ |
| # azazaz lalka sasI | test | test |
TEXT
)+Markdown::QuoteTranslator+Markdown::LeftmostTagTranslator+Markdown::LinearTagTranslator+Markdown::TableTranslator+Markdown::BackslashTranslator)
.to_html
write = File.new("/tmp/test.html","w")
write.write(test)
write.close
Loading…
Cancel
Save