instiki/lib/maruku/attic/parse_span.rb.txt
2007-01-22 08:36:51 -06:00

463 lines
12 KiB
Plaintext

# Copyright (C) 2006 Andrea Censi <andrea (at) rubyforge.org>
#
# This file is part of Maruku.
#
# Maruku is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Maruku is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Maruku; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# NOTE: this is the old span-level regexp-based parser.
#
# The new parser is a real parser and is defined with functions in parse_span_better.rb
# The new parser is faster, handles syntax errors, but it's absolutely not readable.
#
# Also, regexp parsers simply CANNOT handle inline HTML properly.
# There are two black-magic methods `match_couple_of` and `map_match`,
# defined at the end of the file, that make the function
# `parse_lines_as_span` so elegant.
class Maruku
# Takes care of all span-level formatting, links, images, etc.
#
# Lines must not contain block-level elements.
def parse_lines_as_span(lines)
# first, get rid of linebreaks
res = resolve_linebreaks(lines)
span = MDElement.new(:dummy, res)
# encode all escapes
span.replace_each_string { |s| s.escape_md_special }
# The order of processing is significant:
# 1. inline code
# 2. immediate links
# 3. inline HTML
# 4. everything else
# search for ``code`` markers
span.match_couple_of('``') { |children, match1, match2|
e = create_md_element(:inline_code)
# this is now opaque to processing
e.meta[:raw_code] = children.join('').it_was_a_code_block
e
}
# Search for `single tick` code markers
span.match_couple_of('`') { |children, match1, match2|
e = create_md_element(:inline_code)
# this is now opaque to processing
e.meta[:raw_code] = children.join('').it_was_a_code_block
# this is now opaque to processing
e
}
# Detect any immediate link: <http://www.google.com>
# we expect an http: or something: at the beginning
span.map_match( /<(\w+:[^\>]+)>/) { |match|
url = match[1]
e = create_md_element(:immediate_link, [])
e.meta[:url] = url
e
}
# Search for inline HTML (the support is pretty basic for now)
# this searches for a matching block
inlineHTML1 = %r{
( # put everything in 1
< # open
(\w+) # opening tag in 2
> # close
.* # anything
</\2> # match closing tag
)
}x
# this searches for only one block
inlineHTML2 = %r{
( # put everything in 1
< # open
\w+ #
# close
[^<>]* # anything except
/> # closing tag
)
}x
for reg in [inlineHTML1, inlineHTML2]
span.map_match(reg) { |match|
raw_html = match[1]
convert_raw_html_in_list(raw_html)
}
end
# Detect footnotes references: [^1]
span.map_match(/\[(\^[^\]]+)\]/) { |match|
id = match[1].strip.downcase
e = create_md_element(:footnote_reference)
e.meta[:footnote_id] = id
e
}
# Detect any image like ![Alt text][url]
span.map_match(/\!\[([^\]]+)\]\s?\[([^\]]*)\]/) { |match|
alt = match[1]
id = match[2].strip.downcase
if id.size == 0
id = text.strip.downcase
end
e = create_md_element(:image)
e.meta[:ref_id] = id
e
}
# Detect any immage with immediate url: ![Alt](url "title")
# a dummy ref is created and put in the symbol table
link1 = /!\[([^\]]+)\]\s?\(([^\s\)]*)(?:\s+["'](.*)["'])?\)/
span.map_match(link1) { |match|
alt = match[1]
url = match[2]
title = match[3]
url = url.strip
# create a dummy id
id="dummy_#{@refs.size}"
@refs[id] = {:url=>url, :title=>title}
e = create_md_element(:image)
e.meta[:ref_id] = id
e
}
# an id reference: "[id]", "[ id ]"
reg_id_ref = %r{
\[ # opening bracket
([^\]]*) # 0 or more non-closing bracket (this is too permissive)
\] # closing bracket
}x
# validates a url, only $1 is set to the url
reg_url =
/((?:\w+):\/\/(?:\w+:{0,1}\w*@)?(?:\S+)(?::[0-9]+)?(?:\/|\/([\w#!:.?+=&%@!\-\/]))?)/
reg_url = %r{([^\s\]\)]+)}
# A string enclosed in quotes.
reg_title = %r{
" # opening
[^"]* # anything = 1
" # closing
}x
# [bah](http://www.google.com "Google.com"),
# [bah](http://www.google.com),
# [empty]()
reg_url_and_title = %r{
\( # opening
\s* # whitespace
#{reg_url}? # url = 1 might be empty
(?:\s+["'](.*)["'])? # optional title = 2
\s* # whitespace
\) # closing
}x
# Detect a link like ![Alt text][id]
span.map_match(/\[([^\]]+)\]\s?\[([^\]]*)\]/) { |match|
text = match[1]
id = match[2].strip.downcase
if id.size == 0
id = text.strip.downcase
end
children = parse_lines_as_span(text)
e = create_md_element(:link, children)
e.meta[:ref_id] = id
e
}
# Detect any immage with immediate url: ![Alt](url "title")
# a dummy ref is created and put in the symbol table
link1 = /!\[([^\]]+)\]\s?\(([^\s\)]*)(?:\s+["'](.*)["'])?\)/
span.map_match(link1) { |match|
text = match[1]
children = parse_lines_as_span(text)
url = match[2]
title = match[3]
url = url.strip
# create a dummy id
id="dummy_#{@refs.size}"
@refs[id] = {:url=>url, :title=>title}
@refs[id][:title] = title if title
e = create_md_element(:link, children)
e.meta[:ref_id] = id
e
}
# Detect any link like [Google engine][google]
span.match_couple_of('[', # opening bracket
%r{\] # closing bracket
[ ]? # optional whitespace
#{reg_id_ref} # ref id, with $1 being the reference
}x
) { |children, match1, match2|
id = match2[1]
id = id.strip.downcase
if id.size == 0
id = children.join.strip.downcase
end
e = create_md_element(:link, children)
e.meta[:ref_id] = id
e
}
# Detect any link with immediate url: [Google](http://www.google.com)
# XXX Note that the url can be empty: [Empty]()
# a dummy ref is created and put in the symbol table
span.match_couple_of('[', # opening bracket
%r{\] # closing bracket
[ ]? # optional whitespace
#{reg_url_and_title} # ref id, with $1 being the url and $2 being the title
}x
) { |children, match1, match2|
url = match2[1]
title = match2[3] # XXX? Is it a bug? I would use [2]
# create a dummy id
id="dummy_#{@refs.size}"
@refs[id] = {:url=>url}
@refs[id][:title] = title if title
e = create_md_element(:link, children)
e.meta[:ref_id] = id
e
}
# Detect an email address <andrea@invalid.it>
span.map_match(EMailAddress) { |match|
email = match[1]
e = create_md_element(:email_address, [])
e.meta[:email] = email
e
}
# Detect HTML entitis
span.map_match(/&([\w\d]+);/) { |match|
entity_name = match[1]
e = create_md_element(:entity, [])
e.meta[:entity_name] = entity_name
e
}
# And now the easy stuff
# search for ***strong and em***
span.match_couple_of('***') { |children,m1,m2|
create_md_element(:strong, [create_md_element(:emphasis, children)] ) }
span.match_couple_of('___') { |children,m1,m2|
create_md_element(:strong, [create_md_element(:emphasis, children)] ) }
# search for **strong**
span.match_couple_of('**') { |children,m1,m2| create_md_element(:strong, children) }
# search for __strong__
span.match_couple_of('__') { |children,m1,m2| create_md_element(:strong, children) }
# search for *emphasis*
span.match_couple_of('*') { |children,m1,m2| create_md_element(:emphasis, children) }
# search for _emphasis_
span.match_couple_of('_') { |children,m1,m2| create_md_element(:emphasis, children) }
# finally, unescape the special characters
span.replace_each_string { |s| s.unescape_md_special}
span.children
end
# returns array containing Strings or :linebreak elements
def resolve_linebreaks(lines)
res = []
s = ""
lines.each do |l|
s += (s.size>0 ? " " : "") + l.strip
if force_linebreak?(l)
res << s
res << create_md_element(:linebreak)
s = ""
end
end
res << s if s.size > 0
res
end
# raw_html is something like
# <em> A</em> dopwkk *maruk* <em>A</em>
def convert_raw_html_in_list(raw_html)
e = create_md_element(:raw_html)
e.meta[:raw_html] = raw_html
begin
e.meta[:parsed_html] = Document.new(raw_html)
rescue
$stderr.puts "convert_raw_html_in_list Malformed HTML:\n#{raw_html}"
end
e
end
end
# And now the black magic that makes the part above so elegant
class MDElement
# Try to match the regexp to each string in the hierarchy
# (using `replace_each_string`). If the regexp match, eliminate
# the matching string and substitute it with the pre_match, the
# result of the block, and the post_match
#
# ..., matched_string, ... -> ..., pre_match, block.call(match), post_match
#
# the block might return arrays.
#
def map_match(regexp, &block)
replace_each_string { |s|
processed = []
while (match = regexp.match(s))
# save the pre_match
processed << match.pre_match if match.pre_match && match.pre_match.size>0
# transform match
result = block.call(match)
# and append as processed
[*result].each do |e| processed << e end
# go on with the rest of the string
s = match.post_match
end
processed << s if s.size > 0
processed
}
end
# Finds couple of delimiters in a hierarchy of Strings and MDElements
#
# Open and close are two delimiters (like '[' and ']'), or two Regexp.
#
# If you don't pass close, it defaults to open.
#
# Each block is called with |contained children, match1, match2|
def match_couple_of(open, close=nil, &block)
close = close || open
open_regexp = open.kind_of?(Regexp) ? open : Regexp.new(Regexp.escape(open))
close_regexp = close.kind_of?(Regexp) ? close : Regexp.new(Regexp.escape(close))
# Do the same to children first
for c in @children; if c.kind_of? MDElement
c.match_couple_of(open_regexp, close_regexp, &block)
end end
processed_children = []
until @children.empty?
c = @children.shift
if c.kind_of? String
match1 = open_regexp.match(c)
if not match1
processed_children << c
else # we found opening, now search closing
# puts "Found opening (#{marker}) in #{c.inspect}"
# pre match is processed
processed_children.push match1.pre_match if
match1.pre_match && match1.pre_match.size > 0
# we will process again the post_match
@children.unshift match1.post_match if
match1.post_match && match1.post_match.size>0
contained = []; found_closing = false
until @children.empty? || found_closing
c = @children.shift
if c.kind_of? String
match2 = close_regexp.match(c)
if not match2
contained << c
else
# we found closing
found_closing = true
# pre match is contained
contained.push match2.pre_match if
match2.pre_match && match2.pre_match.size>0
# we will process again the post_match
@children.unshift match2.post_match if
match2.post_match && match2.post_match.size>0
# And now we call the block
substitute = block.call(contained, match1, match2)
processed_children << substitute
# puts "Found closing (#{marker}) in #{c.inspect}"
# puts "Children: #{contained.inspect}"
# puts "Substitute: #{substitute.inspect}"
end
else
contained << c
end
end
if not found_closing
# $stderr.puts "##### Could not find closing for #{open}, #{close} -- ignoring"
processed_children << match1.to_s
contained.reverse.each do |c|
@children.unshift c
end
end
end
else
processed_children << c
end
end
raise "BugBug" unless @children.empty?
rebuilt = []
# rebuild strings
processed_children.each do |c|
if c.kind_of?(String) && rebuilt.last && rebuilt.last.kind_of?(String)
rebuilt.last << c
else
rebuilt << c
end
end
@children = rebuilt
end
end