Skip to content

Instantly share code, notes, and snippets.

@DavidBuchanan314
Created March 30, 2017 04:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DavidBuchanan314/4165eb203c5eab21e6a4bbad52aeee21 to your computer and use it in GitHub Desktop.
Save DavidBuchanan314/4165eb203c5eab21e6a4bbad52aeee21 to your computer and use it in GitHub Desktop.
# This class renders an undefined subset of Markdown to HTML
class Markdown
def initialize(fileName)
@input = preprocess(File.read(fileName))
@line_count = @input.lines.count
@line_state = []
@line_flags = {
isCodeBlock: false
}
@line_text = []
@tokenMap = { # This should be put somewhere else
"#" => :h1,
"##" => :h2,
"###" => :h3,
"*" => :bullet
}
@tagOpen = { # This should be put somewhere else
:h1 => "<h1>",
:h2 => "<h2>",
:h3 => "<h3>",
:codeBlock => "<pre><code>",
:lineBreak => "</p>\n",
:quote => "<blockquote><p>",
:rule => "<hr>\n",
:bullet => "<ul>"
}
@tagClose = { # This should be put somewhere else
:h1 => "</h1>",
:h2 => "</h2>",
:h3 => "</h3>",
:codeBlock => "</code></pre>",
:lineBreak => "<p>",
:quote => "</p></blockquote>",
:rule => "",
:bullet => "</ul>"
}
@escapees = "\\*[]()`"
end
def render() # Returns an HTML string of the rendered Markdown.
parse()
return renderHTML()
end
private
def preprocess(str)
return str.gsub(/^###include \".+\"$/) do |s| ###include so doesn't conflict with code that may exist
filename = s[/(?<=\").+(?=\")/] #" not the best way of doing this?
replacement = File.read(filename)
preprocess(replacement) if filename[/\.md$/] # recursive preprocessing TODO: test this...
replacement
end
end
def parse
index = -1
@input.each_line do |line|
index += 1
@line_state[index] = []
line.chomp!
if (line == "") then
@line_state[index] = [:lineBreak]
next
elsif (line == "```") then
@line_flags[:isCodeBlock] = !@line_flags[:isCodeBlock]
next
elsif (line == "---") then
@line_state[index] = [:rule]
next
end
if (@line_flags[:isCodeBlock]) then
@line_state[index].push(:codeBlock)
@line_text[index] = escapeInline(line)
next
end
prefixTokens = line[/^\W*/].delete(" ") # extract tokens that occur at the start of a line ie >, #, * etc.
prefixTokens = prefixTokens.scan(/((.)\2*)/).map(&:first) # split into runs of the same token i.e "###>>" => ["###", ">>"]
@line_text[index] = line[/\w.*$/] # content of the line, not including initial tokens
prefixTokens.each do |token|
if (token[0] == ">")
token.length.times { @line_state[index].push(:quote) }
else
@line_state[index].push(@tokenMap[token])
end
end
@line_state[index].push(:lineBreak) if (@line_text[index][-3..-1] == " ")
end
end
def parseLine(line) # TODO decouple parsing and rendering
line = escapeHTML(line)
# my syntax highlighter really hates these regexps
line.gsub!(/(?<!\\)`.+?[^\\]`/){|s| "<code>#{escapeInline(s[1..-2])}</code>"} #" inline code
line.gsub!(/\[.+?\]\(.+?\)/){|s| "<a href=\"#{s.split("](")[1][0..-2]}\">#{s.split("](")[0][1..-1]}</a>"} # hyperlink
line.gsub!(/(?<!\\)\*\*.+?[^\\]\*\*/){|s| "<strong>#{s[2..-3]}</strong>"} # bold
line.gsub!(/(?<!\\)\*.+?[^\\]\*/){|s| "<em>#{s[1..-2]}</em>"} # italics
return unescapeMarkdown(line)
end
def escapeHTML(str)
str.gsub!("&", "&amp;")
str.gsub!("<", "&lt;")
str.gsub!(">", "&gt;")
return str
end
def escapeInline(str) # escape inline markdown
@escapees.each_char do |c|
str.gsub!(c, "\\\\" + c) # I'm not sure why I need four backslashes here...
end
return str
end
def unescapeMarkdown(str)
@escapees.each_char do |c|
str.gsub!("\\" + c, c)
end
return str
end
def renderHTML
output = "<p>"
@line_count.times do |index|
prevState = @line_state[index-1] || []
diffIndex = 0
while (prevState[diffIndex] == @line_state[index][diffIndex] && prevState[diffIndex] != nil) do
diffIndex += 1
end
closedTags = prevState[diffIndex..-1].reverse # close tags in reverse order
openedTags = @line_state[index][diffIndex..-1]
closedTags.each do |tag|
output += @tagClose[tag]
end
output += "\n"
openedTags.each do |tag|
output += @tagOpen[tag]
end
lineOut = parseLine(@line_text[index] || "")
if @line_state[index].include? :bullet then
lineOut = "<li>#{lineOut}</li>"
end
output += lineOut
end
output += "</p>"
return output
end
end
@DavidBuchanan314
Copy link
Author

Example usage:

require_relative "../markdown"
md = Markdown.new("test.md")
puts md.render()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment