Yessiest
3 months ago
6 changed files with 1304 additions and 335 deletions
-
13classes
-
756document.rb
-
217markdown.rb
-
430mdpp.rb
-
105test.md
-
118test.rb
@ -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 [] |
@ -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 |
@ -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 |
|||
|
|||
|
@ -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 |
@ -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 |
@ -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 |
Write
Preview
Loading…
Cancel
Save
Reference in new issue