Created
March 30, 2017 04:16
-
-
Save DavidBuchanan314/4165eb203c5eab21e6a4bbad52aeee21 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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!("&", "&") | |
str.gsub!("<", "<") | |
str.gsub!(">", ">") | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example usage: