dam
This commit is contained in:
parent
468bd043ca
commit
8b63d77006
|
@ -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,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
|
|
@ -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
|
|
@ -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)
|
||||||
|

|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- I may also need to merge lists for this to work properly
|
Loading…
Reference in New Issue