Нено обнови решението на 21.11.2011 03:30 (преди около 13 години)
+class Formatter
+ def initialize(markdown_text)
+ @markdown = markdown_text
+ @html = nil
+ end
+
+ def to_html
+ if @html == nil
+ formatted_lines = @markdown.lines.map { |line| Line.recognize_type(line) }
+ merge_adjacent_lines_of_the_same_type(formatted_lines)
+ @html = formatted_lines.map { |line| line.to_html }.inject(:+).strip
+ end
+ @html
+ end
+
+ alias :to_s :to_html
+
+ def inspect
+ @markdown
+ end
+
+ private
+
+ def merge_adjacent_lines_of_the_same_type(formatted_lines)
+ prev_line = nil
+ formatted_lines.keep_if do |line|
+ prev_line_copy, prev_line = prev_line, line
+ merged = (prev_line_copy and prev_line_copy.merge(line))
+ prev_line = prev_line_copy if merged # the current line will be removed fr,SKEPTIC,!
+ # so restore the reference to the lin,SKEPTIC,!
+ not merged
+ end
+ end
+
+ #
+ # The apply_html_formatting function in the HtmlFormatting module is responsib,SKEPTIC,!
+ # symbols and sequences with their corresponding html tags within the confines,SKEPTIC,!
+ #
+
+ module HtmlFormatting
+ def apply_html_formatting(text, *options)
+ return "" if text.empty?
+ match_data = SpecialSequence.match(text, options)
+ return match_data.pre_match +
+ match_data.matched_sequence.to_html +
+ apply_html_formatting(match_data.post_match)
+ end
+
+ class SequenceMatchData
+ def initialize(pre_match, sequence, post_match)
+ @pre_match, @matched_sequence, @post_match = pre_match, sequence, post_match
+ end
+
+ def match_offset
+ @pre_match.length
+ end
+
+ attr_reader :pre_match, :matched_sequence, :post_match
+ end
+
+ class SpecialSequence
+ include HtmlFormatting
+
+ @children = []
+
+ def self.inherited(child)
+ @children << child
+ end
+
+ # Finds the first special sequence in the text and splits the text in 3 pieces:
+ # 1. the processed text before the special sequence, which can safely be p,SKEPTIC,!
+ # 2. the special sequence itself
+ # 3. the unprocessed text after the special sequence, which must still be ,SKEPTIC,!
+ def self.match(text, options)
+ matched_sequences = @children.map { |seq| seq.match(text, options) }
+ matched_sequences << SequenceMatchData.new(text, NoSequence.new, "") # i,SKEPTIC,!
+ matched_sequences.reject { |match| match == nil }.min_by(&:match_offset)
+ end
+ end
+
+ # The safety net: this sequence always matches
+ class NoSequence < SpecialSequence
+ def self.match(text, options)
+ SequenceMatchData.new(text, self.new, "")
+ end
+
+ def to_html
+ ""
+ end
+ end
+
+ class LinkSequence < SpecialSequence
+ @@LinkPattern = /\[(?<name>[^\]]+?)\]\((?<url>[^\)]+?)\)/
+
+ def initialize(name, url)
+ @name, @url = name, url
+ end
+
+ def self.match(text, options)
+ return nil if options.include? :ignore_link
+
+ match = @@LinkPattern.match(text)
+ if match
+ SequenceMatchData.new(match.pre_match,
+ self.new(match[:name], match[:url]),
+ match.post_match)
+ else
+ nil
+ end
+ end
+
+ def to_html
+ "<a href=\"#{apply_html_formatting @url}\">#{apply_html_formatting @name}</a>"
+ end
+ end
+
+ class BoldSequence < SpecialSequence
+ def initialize(bold_text)
+ @bold_text = bold_text
+ end
+
+ def self.match(text, options)
+ return nil if options.include? :ignore_bold
+
+ match = /\*\*(?<bold>.+?)\*\*/.match(text)
+ if match
+ SequenceMatchData.new(match.pre_match, self.new(match[:bold]), match.post_match)
+ else
+ nil
+ end
+ end
+
+ def to_html
+ "<strong>#{apply_html_formatting @bold_text}</strong>"
+ end
+ end
+
+ class ItalicSequence < SpecialSequence
+ def initialize(italic_text)
+ @italic_text = italic_text
+ end
+
+ def self.match(text, options)
+ return nil if options.include? :ignore_italic
+
+ m = /_(?<italic>.+?)_/.match(text)
+ if m
+ SequenceMatchData.new(m.pre_match, self.new(m[:italic]), m.post_match)
+ else
+ nil
+ end
+ end
+
+ def to_html
+ "<em>#{apply_html_formatting @italic_text}</em>"
+ end
+ end
+
+ class AmpersandSequence < SpecialSequence
+ def self.match(text, options)
+ match = /&/.match text
+ if match
+ SequenceMatchData.new(match.pre_match, self.new, match.post_match)
+ else
+ nil
+ end
+ end
+
+ def to_html
+ "&"
+ end
+ end
+
+ class LessThanSequence < SpecialSequence
+ def self.match(text, options)
+ match = /</.match text
+ if match
+ SequenceMatchData.new(match.pre_match, self.new, match.post_match)
+ else
+ nil
+ end
+ end
+
+ def to_html
+ "<"
+ end
+ end
+
+ class GreaterThanSequence < SpecialSequence
+ def self.match(text, options)
+ match = />/.match text
+ if match
+ SequenceMatchData.new(match.pre_match, self.new, match.post_match)
+ else
+ nil
+ end
+ end
+
+ def to_html
+ ">"
+ end
+ end
+
+ class QuotesSequence < SpecialSequence
+ def self.match(text, options)
+ match = /"/.match text
+ if match
+ SequenceMatchData.new(match.pre_match, self.new, match.post_match)
+ else
+ nil
+ end
+ end
+
+ def to_html
+ """
+ end
+ end
+ end
+
+ #
+ # The Line hierarchy of classes is responsible for recognizing the types of wh,SKEPTIC,!
+ #
+
+ class Line
+ include HtmlFormatting
+
+ @children = []
+
+ def self.inherited(child)
+ @children << child
+ end
+
+ def self.recognize_type(line)
+ @children.each { |child| return child.new(line) if child.recognize?(line) }
+ raise "No line type recognizes line '#{line}'"
+ end
+ end
+
+ class HeaderLine < Line
+ @@HeaderPattern = /^\s{0,3}\#{1,4}\s+/
+
+ def initialize(line)
+ @header_text = apply_html_formatting @@HeaderPattern.match(line).post_match
+ @header_level = 4.downto(1) { |n| break n if line.lstrip.start_with?("#" * n) }
+ end
+
+ def self.recognize?(line)
+ match = @@HeaderPattern.match(line)
+ match != nil and not match.post_match.empty?
+ end
+
+ def to_html
+ "<h#{@header_level}>#{@header_text.strip}</h#{@header_level}>\n"
+ end
+
+ def merge(another_line)
+ false # adjacent header lines are not merged
+ end
+ end
+
+ class CodeBlockLine < Line
+ @@CodeBlockPattern = /^ /
+
+ def initialize(line)
+ @code_text = apply_html_formatting @@CodeBlockPattern.match(line).post_match,
+ :ignore_link, :ignore_bold, :ignore_italic
+ end
+
+ def self.recognize?(line)
+ @@CodeBlockPattern.match(line)
+ end
+
+ def to_html
+ "<pre><code>#{@code_text.chomp}</code></pre>\n"
+ end
+
+ def merge(another_line)
+ return false if another_line.class != self.class
+ @code_text << another_line.code_text
+ end
+
+ protected
+
+ attr_reader :code_text
+ end
+
+ class QuoteLine < Line
+ @@QuotePattern = /^\s{0,3}>\s/
+
+ def initialize(line)
+ @quoted_text = @@QuotePattern.match(line).post_match
+ end
+
+ def self.recognize?(line)
+ @@QuotePattern.match(line)
+ end
+
+ def to_html
+ formatted_quote = Formatter.new(@quoted_text).to_html
+ "<blockquote>#{formatted_quote.chomp}</blockquote>\n"
+ end
+
+ def merge(another_line)
+ return false if another_line.class != self.class
+ @quoted_text << another_line.quoted_text
+ end
+
+ protected
+
+ attr_reader :quoted_text
+ end
+
+ class OrderedListLine < Line
+ @@OrderedListPattern = /^\s{0,3}\d+\. /
+
+ def initialize(line)
+ @items = []
+ @items << apply_html_formatting(@@OrderedListPattern.match(line).post_match)
+ end
+
+ def self.recognize?(line)
+ match = @@OrderedListPattern.match(line)
+ match != nil and not match.post_match.empty?
+ end
+
+ def to_html
+ html = "<ol>\n"
+ @items.each { |item| html << " <li>#{item.chomp}</li>\n" }
+ html << "</ol>\n"
+ end
+
+ def merge(another_line)
+ return false if another_line.class != self.class
+ @items.concat another_line.items
+ end
+
+ protected
+
+ attr_reader :items
+ end
+
+ class UnorderedListLine < Line
+ @@UnorderedListPattern = /^\s{0,3}\* /
+
+ def initialize(line)
+ @items = []
+ @items << apply_html_formatting(@@UnorderedListPattern.match(line).post_match)
+ end
+
+ def self.recognize?(line)
+ match = @@UnorderedListPattern.match(line)
+ match != nil and not match.post_match.empty?
+ end
+
+ def to_html
+ html = "<ul>\n"
+ @items.each { |item| html << " <li>#{item.chomp}</li>\n" }
+ html << "</ul>\n"
+ end
+
+ def merge(another_line)
+ return false if another_line.class != self.class
+ @items.concat another_line.items
+ end
+
+ protected
+
+ attr_reader :items
+ end
+
+ # N.B.: EmptyLine must be defined after CodeBlock, because it would recognize ,SKEPTIC,!
+ class EmptyLine < Line
+ @@EmptyPattern = /^\s*$/
+
+ def initialize(line)
+ @empty_lines = "\n"
+ end
+
+ def self.recognize?(line)
+ @@EmptyPattern.match(line)
+ end
+
+ def to_html
+ @empty_lines
+ end
+
+ def merge(another_line)
+ return false if another_line.class != self.class
+ @empty_lines << another_line.empty_lines
+ end
+
+ protected
+
+ attr_reader :empty_lines
+ end
+
+ # N.B.: ParagraphLine must be defined last because it matches every line so it,SKEPTIC,!
+ class ParagraphLine < Line
+ def initialize(line)
+ @paragraph_text = apply_html_formatting line
+ end
+
+ def self.recognize?(line)
+ true # if the other line types didn't recognize this line, it must be a paragraph
+ end
+
+ def to_html
+ "<p>#{@paragraph_text.strip}</p>\n"
+ end
+
+ def merge(another_line)
+ return false if another_line.class != self.class
+ @paragraph_text << another_line.paragraph_text
+ end
+
+ protected
+
+ attr_reader :paragraph_text
+ end
+end