[Hiki-cvs 1400] [947] use recent hikidoc.rb (rev.110).

Back to archive index

svnno****@sourc***** svnno****@sourc*****
2009年 8月 1日 (土) 00:56:08 JST


Revision: 947
          http://sourceforge.jp/projects/hiki/svn/view?view=rev&revision=947
Author:   fdiary
Date:     2009-08-01 00:56:07 +0900 (Sat, 01 Aug 2009)

Log Message:
-----------
use recent hikidoc.rb (rev.110).

Modified Paths:
--------------
    hiki/trunk/ChangeLog
    hiki/trunk/style/default/hikidoc.rb

Modified: hiki/trunk/ChangeLog
===================================================================
--- hiki/trunk/ChangeLog	2009-07-28 09:39:07 UTC (rev 946)
+++ hiki/trunk/ChangeLog	2009-07-31 15:56:07 UTC (rev 947)
@@ -1,3 +1,7 @@
+2009-07-31  Kazuhiko  <kazuh****@fdiar*****>
+
+	* style/default/hikidoc.rb: use recent hikidoc.rb (rev.110).
+
 2009-07-28  okkez  <okkez****@gmail*****>
 
 	* misc/plugin/history.rb (Hiki::History#history): uncomment. (see rev.942)

Modified: hiki/trunk/style/default/hikidoc.rb
===================================================================
--- hiki/trunk/style/default/hikidoc.rb	2009-07-28 09:39:07 UTC (rev 946)
+++ hiki/trunk/style/default/hikidoc.rb	2009-07-31 15:56:07 UTC (rev 947)
@@ -1,4 +1,6 @@
+# -*- coding: utf-8; -*-
 # Copyright (c) 2005, Kazuhiko <kazuh****@fdiar*****>
+# Copyright (c) 2007 Minero Aoki
 # All rights reserved.
 # 
 # Redistribution and use in source and binary forms, with or without
@@ -26,446 +28,876 @@
 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- 
-require 'uri'
 
-class HikiDoc < String
-  Revision = %q$Rev: 38 $
+require "stringio"
+require "strscan"
+require "uri"
+begin
+  require "syntax/convertors/html"
+rescue LoadError
+end
 
-  def initialize( content = '', options = {} )
+class HikiDoc
+  VERSION = "0.0.4" # FIXME
+
+  class Error < StandardError
+  end
+
+  class UnexpectedError < Error
+  end
+
+  def HikiDoc.to_html(src, options = {})
+    new(HTMLOutput.new(">"), options).compile(src)
+  end
+
+  def HikiDoc.to_xhtml(src, options = {})
+    new(HTMLOutput.new(" />"), options).compile(src)
+  end
+
+  def initialize(output, options = {})
+    @output = output
+    @options = default_options.merge(options)
+    @header_re = nil
     @level = options[:level] || 1
-    @empty_element_suffix = options[:empty_element_suffix] || ' />'
-    super( content )
+    @plugin_syntax = options[:plugin_syntax] || method(:valid_plugin_syntax?)
   end
 
+  def compile(src)
+    @output.reset
+    escape_plugin_blocks(src) {|escaped|
+      compile_blocks escaped
+      @output.finish
+    }
+  end
+
+  # for backward compatibility
   def to_html
-    @stack = []
-    @plugin_stack = []
-    text = self.gsub( /\r\n?/, "\n" )
-    text.sub!( /\n*\z/, "\n\n" )
-    # escape '&', '<' and '>'
-    text = escape_html( text )
-    # escape some symbols
-    text = escape_meta_char( text )
-    # parse blocks
-    text = block_parser( text )
-    # remove needless new lines
-    text.gsub!( /\n{2,}/, "\n" )
-    # restore some html parts
-    text = restore_block( text )
-    text = restore_plugin_block( text )
-    # unescape some symbols
-    text = unescape_meta_char( text )
-    # terminate with a single new line
-    text.sub!( /\n*\z/, "\n" )
-    text
+    $stderr.puts("warning: HikiDoc#to_html is deprecated. Please use HikiDoc.to_html or HikiDoc.to_xhtml instead.")
+    self.class.to_html(@output, @options)
   end
 
   private
 
-  ######################################################################
-  # block parser
-  ######################################################################
+  def default_options
+    {
+      :allow_bracket_inline_image => true,
+      :use_wiki_name => true,
+      :use_not_wiki_name => true,
+    }
+  end
 
-  def block_parser( text )
-    ret = text
-    ret = parse_plugin( ret )
-    ret = parse_pre( ret )
-    ret = parse_comment( ret )
-    ret = parse_header( ret )
-    ret = parse_hrules( ret )
-    ret = parse_list( ret )
-    ret = parse_definition( ret )
-    ret = parse_blockquote( ret )
-    ret = parse_table( ret )
-    ret = parse_paragraph( ret )
-    ret.lstrip
+  #
+  # Plugin
+  #
+
+  def valid_plugin_syntax?(code)
+    /['"]/ !~ code.gsub(/'(?:[^\\']+|\\.)*'|"(?:[^\\"]+|\\.)*"/m, "")
   end
 
-  ######################################################################
-  # plugin
+  def escape_plugin_blocks(text)
+    s = StringScanner.new(text)
+    buf = ""
+    @plugin_blocks = []
+    while chunk = s.scan_until(/\{\{/)
+      tail = chunk[-2, 2]
+      chunk[-2, 2] = ""
+      buf << chunk
+      # plugin
+      if block = extract_plugin_block(s)
+        @plugin_blocks.push block
+        buf << "\0#{@plugin_blocks.size - 1}\0"
+      else
+        buf << "{{"
+      end
+    end
+    buf << s.rest
+    yield(buf)
+  end
 
-  PLUGIN_OPEN = '{{'
-  PLUGIN_CLOSE = '}}'
-  PLUGIN_SPLIT_RE = /(#{Regexp.union( PLUGIN_OPEN, PLUGIN_CLOSE )})/
+  def restore_plugin_block(str)
+    str.gsub(/\0(\d+)\0/) {
+      "{{" + plugin_block($1.to_i) + "}}"
+    }
+  end
 
-  def parse_plugin( text )
-    ret = ''
-    plugin = false
-    plugin_str = ''
-    text.split( PLUGIN_SPLIT_RE ).each do |str|
-      case str
-      when PLUGIN_OPEN
-        plugin = true
-        plugin_str += str
-      when PLUGIN_CLOSE
-        if plugin
-          plugin_str += str
-          unless /['"]/ =~ plugin_str.gsub( /(['"]).*?\1/m, '' )
-            plugin = false
-            ret << store_plugin_block( unescape_meta_char( plugin_str, true ) )
-            plugin_str = ''
-          end
-        else
-          ret << str
-        end
+  def evaluate_plugin_block(str, buf = nil)
+    buf ||=****@outpu*****
+    str.split(/(\0\d+\0)/).each do |s|
+      if s[0, 1] == "\0" and s[-1, 1] == "\0"
+        buf << @output.inline_plugin(plugin_block(s[1..-2].to_i))
       else
-        if plugin
-          plugin_str << str
-        else
-          ret << str
-        end
+        buf << @output.text(s)
       end
     end
-    ret << plugin_str if plugin
-    ret
+    buf
   end
 
-  ######################################################################
-  # pre
+  def plugin_block(id)
+    @plugin_blocks[id] or raise UnexpectedError, "must not happen: #{id.inspect}"
+  end
 
-  MULTI_PRE_OPEN_RE = /&lt;&lt;&lt;/
-  MULTI_PRE_CLOSE_RE = /&gt;&gt;&gt;/
-  PRE_RE = /^[ \t]/
+  def extract_plugin_block(s)
+    pos = s.pos
+    buf = ""
+    while chunk = s.scan_until(/\}\}/)
+      buf << chunk
+      buf.chomp!("}}")
+      if @plugin_syntax.call(buf)
+        return buf
+      end
+      buf << "}}"
+    end
+    s.pos = pos
+    nil
+  end
 
-  def parse_pre( text )
-    ret = text
-    ret.gsub!( /^#{MULTI_PRE_OPEN_RE}$(.*?)^#{MULTI_PRE_CLOSE_RE}$/m ) do |str|
-      "\n" + store_block( "<pre>%s</pre>" % restore_pre( $1 ) ) + "\n\n"
+  #
+  # Block Level
+  #
+
+  def compile_blocks(src)
+    f = LineInput.new(StringIO.new(src))
+    while line = f.peek
+      case line
+      when COMMENT_RE
+        f.gets
+      when HEADER_RE
+        compile_header f.gets
+      when HRULE_RE
+        f.gets
+        compile_hrule
+      when LIST_RE
+        compile_list f
+      when DLIST_RE
+        compile_dlist f
+      when TABLE_RE
+        compile_table f
+      when BLOCKQUOTE_RE
+        compile_blockquote f
+      when INDENTED_PRE_RE
+        compile_indented_pre f
+      when BLOCK_PRE_OPEN_RE
+        compile_block_pre f
+      else
+        if /^$/ =~ line
+          f.gets
+          next
+        end
+        compile_paragraph f
+      end
     end
-    ret.gsub!( /(?:#{PRE_RE}.*\n?)+/ ) do |str|
-      str.chomp!
-      str.gsub!( PRE_RE, '' )
-      "\n" + store_block( "<pre>\n%s\n</pre>" % restore_pre( str ) ) + "\n\n"
+  end
+
+  COMMENT_RE = %r<\A//>
+
+  def skip_comments(f)
+    f.while_match(COMMENT_RE) do |line|
     end
-    ret
   end
 
-  def restore_pre( text )
-    ret = unescape_meta_char( text, true )
-    ret = restore_plugin_block( ret, true )
+  HEADER_RE = /\A!+/
+
+  def compile_header(line)
+    @header_re ||= /\A!{1,#{7 - @level}}/
+    level = @level + (line.slice!(@header_re).size - 1)
+    title = strip(line)
+    @output.headline level, compile_inline(title)
   end
 
-  ######################################################################
-  # header
+  HRULE_RE = /\A----$/
 
-  HEADER_RE = /!/
+  def compile_hrule
+    @output.hrule
+  end
 
-  def parse_header( text )
-    text.gsub( /^(#{HEADER_RE}{1,#{7- @ level}})\s*(.*)\n?/ ) do |str|
-      level, title = $1.size + @level - 1, $2
-      "\n<h#{level}>%s</h#{level}>\n\n" %
-        inline_parser(title)
+  ULIST = "*"
+  OLIST = "#"
+  LIST_RE = /\A#{Regexp.union(ULIST, OLIST)}+/
+
+  def compile_list(f)
+    typestack = []
+    level = 0
+    @output.list_begin
+    f.while_match(LIST_RE) do |line|
+      list_type = (line[0,1] == ULIST ? "ul" : "ol")
+      new_level = line.slice(LIST_RE).size
+      item = strip(line.sub(LIST_RE, ""))
+      if new_level > level
+        (new_level - level).times do
+          typestack.push list_type
+          @output.list_open list_type
+          @output.listitem_open
+        end
+        @output.listitem compile_inline(item)
+      elsif new_level < level
+        (level - new_level).times do
+          @output.listitem_close
+          @output.list_close typestack.pop
+        end
+        @output.listitem_close
+        @output.listitem_open
+        @output.listitem compile_inline(item)
+      elsif list_type == typestack.last
+        @output.listitem_close
+        @output.listitem_open
+        @output.listitem compile_inline(item)
+      else
+        @output.listitem_close
+        @output.list_close typestack.pop
+        @output.list_open list_type
+        @output.listitem_open
+        @output.listitem compile_inline(item)
+        typestack.push list_type
+      end
+      level = new_level
+      skip_comments f
     end
+    level.times do
+      @output.listitem_close
+      @output.list_close typestack.pop
+    end
+    @output.list_end
   end
 
-  ######################################################################
-  # hrules
+  DLIST_RE = /\A:/
 
-  HRULES_RE = /^----$/
+  def compile_dlist(f)
+    @output.dlist_open
+    f.while_match(DLIST_RE) do |line|
+      dt, dd = split_dlitem(line.sub(DLIST_RE, ""))
+      @output.dlist_item compile_inline(dt), compile_inline(dd)
+      skip_comments f
+    end
+    @output.dlist_close
+  end
 
-  def parse_hrules( text )
-    text.gsub( HRULES_RE ) do |str|
-      "\n<hr#{@empty_element_suffix}\n"
+  def split_dlitem(line)
+    re = /\A((?:#{BRACKET_LINK_RE}|.)*?):/o
+    if m = re.match(line)
+      return m[1], m.post_match
+    else
+      return line, ""
     end
   end
 
-  ######################################################################
-  # list
+  TABLE_RE = /\A\|\|/
 
-  LIST_UL = '*'
-  LIST_OL = '#'
-  LIST_MARK_RE = Regexp.union( LIST_UL, LIST_OL )
-  LIST_RE = /^((#{LIST_MARK_RE})\2*)\s*(.*)\n?/
-  LISTS_RE = /(?:#{LIST_RE})+/
-
-  def parse_list( text )
-    text.gsub( LISTS_RE ) do |str|
-      cur_str = "\n"
-      list_type_array = []
-      level = 0
-      str.each do |line|
-        if LIST_RE =~ line
-          list_type = ( $2 == LIST_UL ? 'ul' : 'ol' )
-          new_level, item = $1.size, $3
-          if new_level > level
-            (new_level - level).times do
-              list_type_array << list_type
-              cur_str << "<#{list_type}>\n<li>"
-            end
-            cur_str << "%s" % inline_parser( item )
-          elsif new_level < level
-            (level - new_level).times do
-              cur_str << "</li>\n</#{list_type_array.pop}>"
-            end
-            cur_str << "</li>\n<li>%s" % inline_parser( item )
-          elsif list_type == list_type_array.last
-            cur_str << "</li>\n<li>%s" % inline_parser( item )
-          else
-            cur_str << "</li>\n</%s>\n" % list_type_array.pop
-            cur_str << "<%s>\n" % list_type
-            cur_str << "<li>%s" % inline_parser( item )
-            list_type_array << list_type
-          end
-          level = new_level
-        end
+  def compile_table(f)
+    lines = []
+    f.while_match(TABLE_RE) do |line|
+      lines.push line
+      skip_comments f
+    end
+    @output.table_open
+    lines.each do |line|
+      @output.table_record_open
+      split_columns(line.sub(TABLE_RE, "")).each do |col|
+        mid = col.sub!(/\A!/, "") ? "table_head" : "table_data"
+        span = col.slice!(/\A[\^>]*/)
+        rs = span_count(span, "^")
+        cs = span_count(span, ">")
+        @output.__send__(mid, compile_inline(col), rs, cs)
       end
-      level.times do
-        cur_str << "</li>\n</#{list_type_array.pop}>"
-      end
-      cur_str << "\n\n"
-      cur_str
+      @output.table_record_close
     end
+    @output.table_close
   end
 
-  ######################################################################
-  # definition
+  def split_columns(str)
+    cols = str.split(/\|\|/)
+    cols.pop if cols.last.chomp.empty?
+    cols
+  end
 
-  DEFINITION_RE = /^:(.*?)?:(.*)\n?/
-  DEFINITIONS_RE = /(#{DEFINITION_RE})+/
+  def span_count(str, ch)
+    c = str.count(ch)
+    c == 0 ? nil : c + 1
+  end
 
-  def parse_definition( text )
-    parsed_text = text.gsub( DEFINITION_RE ) do |str|
-      inline_parser( str )
+  BLOCKQUOTE_RE = /\A""[ \t]?/
+
+  def compile_blockquote(f)
+    @output.blockquote_open
+    lines = []
+    f.while_match(BLOCKQUOTE_RE) do |line|
+      lines.push line.sub(BLOCKQUOTE_RE, "")
+      skip_comments f
     end
-    parsed_text.gsub( DEFINITIONS_RE ) do |str|
-      ret = "\n<dl>\n"
-      str.chomp!
-      str.scan( DEFINITION_RE ) do |t, d|
-        if t.empty?
-          ret << "<dd>%s</dd>\n" % d
-        elsif d.empty?
-          ret << "<dt>%s</dt>\n" % t
-        else
-          ret << "<dt>%s</dt><dd>%s</dd>\n" % [ t, d ]
-        end
+    compile_blocks lines.join("")
+    @output.blockquote_close
+  end
+
+  INDENTED_PRE_RE = /\A[ \t]/
+
+  def compile_indented_pre(f)
+    lines = f.span(INDENTED_PRE_RE)\
+        .map {|line| rstrip(line.sub(INDENTED_PRE_RE, "")) }
+    text = restore_plugin_block(lines.join("\n"))
+    @output.preformatted(@output.text(text))
+  end
+
+  BLOCK_PRE_OPEN_RE = /\A<<<\s*(\w+)?/
+  BLOCK_PRE_CLOSE_RE = /\A>>>/
+
+  def compile_block_pre(f)
+    m = BLOCK_PRE_OPEN_RE.match(f.gets) or raise UnexpectedError, "must not happen"
+    str = restore_plugin_block(f.break(BLOCK_PRE_CLOSE_RE).join.chomp)
+    f.gets
+    @output.block_preformatted(str, m[1])
+  end
+
+  BLANK = /\A$/
+  PARAGRAPH_END_RE = Regexp.union(BLANK,
+                                  HEADER_RE, HRULE_RE, LIST_RE, DLIST_RE,
+                                  BLOCKQUOTE_RE, TABLE_RE,
+                                  INDENTED_PRE_RE, BLOCK_PRE_OPEN_RE)
+
+  def compile_paragraph(f)
+    lines = f.break(PARAGRAPH_END_RE)\
+        .reject {|line| COMMENT_RE =~ line }
+    if lines.size == 1 and /\A\0(\d+)\0\z/ =~ strip(lines[0])
+      @output.block_plugin plugin_block($1.to_i)
+    else
+      line_buffer =****@outpu*****(:paragraph)
+      lines.each_with_index do |line, i|
+        buffer =****@outpu*****
+        line_buffer << buffer
+        compile_inline(lstrip(line).chomp, buffer)
       end
-      ret << "</dl>\n\n"
-      ret
+      @output.paragraph(line_buffer)
     end
   end
 
-  ######################################################################
-  # blockquote
+  #
+  # Inline Level
+  #
 
-  BLOCKQUOTE_RE = /^""[ \t]?/
-  BLOCKQUOTES_RE = /(#{BLOCKQUOTE_RE}.*\n?)+/
+  BRACKET_LINK_RE = /\[\[.+?\]\]/
+  URI_RE = /(?:https?|ftp|file|mailto):[A-Za-z0-9;\/?:@&=+$,\-_.!~*\'()#%]+/
+  WIKI_NAME_RE = /\b(?:[A-Z]+[a-z\d]+){2,}\b/
 
-  def parse_blockquote( text )
-    text.gsub( BLOCKQUOTES_RE ) do |str|
-      str.chomp!
-      str.gsub!( BLOCKQUOTE_RE, '' )
-      "\n<blockquote>\n%s\n</blockquote>\n\n" % block_parser(str)
+  def inline_syntax_re
+    if @options[:use_wiki_name]
+      if @options[:use_not_wiki_name]
+        / (#{BRACKET_LINK_RE})
+        | (#{URI_RE})
+        | (#{MODIFIER_RE})
+        | (\^?#{WIKI_NAME_RE})
+        /xo
+      else
+        / (#{BRACKET_LINK_RE})
+        | (#{URI_RE})
+        | (#{MODIFIER_RE})
+        | (#{WIKI_NAME_RE})
+        /xo
+      end
+    else
+      / (#{BRACKET_LINK_RE})
+      | (#{URI_RE})
+      | (#{MODIFIER_RE})
+      /xo
     end
   end
 
-  ######################################################################
-  # table
+  def compile_inline(str, buf = nil)
+    buf ||=****@outpu*****
+    re = inline_syntax_re
+    pending_str = nil
+    while m = re.match(str)
+      str = m.post_match
 
-  TABLE_SPLIT_RE = /\|\|/
-  TABLE_RE = /^#{TABLE_SPLIT_RE}.+\n?/
-  TABLES_RE = /(#{TABLE_RE})+/
+      link, uri, mod, wiki_name = m[1, 4]
+      if wiki_name and wiki_name[0, 1] == "^"
+        pending_str = m.pre_match + wiki_name[1..-1]
+        next
+      end
 
-  def parse_table( text )
-    parsed_text = text.gsub( TABLE_RE ) do |str|
-      inline_parser( str )
+      pre_str = "#{pending_str}#{m.pre_match}"
+      pending_str = nil
+      evaluate_plugin_block(pre_str, buf)
+      compile_inline_markup(buf, link, uri, mod, wiki_name)
     end
-    parsed_text.gsub( TABLES_RE ) do |str|
-      ret = %Q|\n<table border="1">\n|
-      str.each do |line|
-        ret << "<tr>"
-        line.chomp.sub( /#{TABLE_SPLIT_RE}$/, '').split( TABLE_SPLIT_RE, -1 )[1..-1].each do |i|
-          tag = i.sub!( /^!/, '' ) ? 'th' : 'td'
-          attr = ''
-          if i.sub!( /^((?:\^|&gt;)+)/, '' )
-            rs = $1.count( '^' ) + 1
-            cs = $1.scan( /&gt;/ ).size + 1
-            attr << ' rowspan="%d"' % rs if rs > 1
-            attr << ' colspan="%d"' % cs if cs > 1
-          end
-          ret << "<#{tag}#{attr}>#{inline_parser( i )}</#{tag}>"
-        end
-        ret << "</tr>\n"
+    evaluate_plugin_block(pending_str || str, buf)
+    buf
+  end
+
+  def compile_inline_markup(buf, link, uri, mod, wiki_name)
+    case
+    when link
+      buf << compile_bracket_link(link[2...-2])
+    when uri
+      buf << compile_uri_autolink(uri)
+    when mod
+      buf << compile_modifier(mod)
+    when wiki_name
+      buf << @output.wiki_name(wiki_name)
+    else
+      raise UnexpectedError, "must not happen"
+    end
+  end
+
+  def compile_bracket_link(link)
+    if m = /\A(?>[^|\\]+|\\.)*\|/.match(link)
+      title = m[0].chop
+      uri = m.post_match
+      fixed_uri = fix_uri(uri)
+      if can_image_link?(uri)
+        @output.image_hyperlink(fixed_uri, title)
+      else
+        @output.hyperlink(fixed_uri, compile_modifier(title))
       end
-      ret << "</table>\n\n"
-      ret
+    else
+      fixed_link = fix_uri(link)
+      if can_image_link?(link)
+        @output.image_hyperlink(fixed_link)
+      else
+        @output.hyperlink(fixed_link, @output.text(link))
+      end
     end
   end
 
-  ######################################################################
-  # comment
+  def can_image_link?(uri)
+    image?(uri) and @options[:allow_bracket_inline_image]
+  end
 
-  COMMENT_RE = %r|^//.*\n?|
+  def compile_uri_autolink(uri)
+    if image?(uri)
+      @output.image_hyperlink(fix_uri(uri))
+    else
+      @output.hyperlink(fix_uri(uri), @output.text(uri))
+    end
+  end
 
-  def parse_comment( text )
-    text.gsub( COMMENT_RE, '' )
+  def fix_uri(uri)
+    if /\A(?:https?|ftp|file):(?!\/\/)/ =~ uri
+      uri.sub(/\A\w+:/, "")
+    else
+      uri
+    end
   end
 
-  ######################################################################
-  # paragraph
+  IMAGE_EXTS = %w(.jpg .jpeg .gif .png)
 
-  PARAGRAPH_BOUNDARY_RE = /\n{2,}/
-  NON_PARAGRAPH_RE = /^<[^!]/
+  def image?(uri)
+    IMAGE_EXTS.include?(File.extname(uri).downcase)
+  end
 
-  def parse_paragraph( text )
-    text.split( PARAGRAPH_BOUNDARY_RE ).collect { |str|
-      str.chomp!
-      if str.empty?
-        ''
-      elsif NON_PARAGRAPH_RE =~ str
-        str
+  STRONG = "'''"
+  EM = "''"
+  DEL = "=="
+
+  STRONG_RE = /'''.+?'''/
+  EM_RE     = /''.+?''/
+  DEL_RE    = /==.+?==/
+
+  MODIFIER_RE = Regexp.union(STRONG_RE, EM_RE, DEL_RE)
+
+  MODTAG = {
+    STRONG => "strong",
+    EM     => "em",
+    DEL    => "del"
+  }
+
+  def compile_modifier(str)
+    buf =****@outpu*****
+    while m = / (#{MODIFIER_RE})
+              /xo.match(str)
+      evaluate_plugin_block(m.pre_match, buf)
+      case
+      when chunk = m[1]
+        mod, s = split_mod(chunk)
+        mid = MODTAG[mod]
+        buf << @output.__send__(mid, compile_inline(s))
       else
-        "<p>%s</p>" % inline_parser( str )
+        raise UnexpectedError, "must not happen"
       end
-    }.join( "\n\n" )
+      str = m.post_match
+    end
+    evaluate_plugin_block(str, buf)
+    buf
   end
 
-  ######################################################################
-  # inline parser
-  ######################################################################
+  def split_mod(str)
+    case str
+    when /\A'''/
+      return str[0, 3], str[3...-3]
+    when /\A''/
+      return str[0, 2], str[2...-2]
+    when /\A==/
+      return str[0, 2], str[2...-2]
+    else
+      raise UnexpectedError, "must not happen: #{str.inspect}"
+    end
+  end
 
-  def inline_parser( text )
-    text = parse_link( text )
-    text = parse_modifier( text )
+  def strip(str)
+    rstrip(lstrip(str))
   end
 
-  ######################################################################
-  # link and image
+  def rstrip(str)
+    str.sub(/[ \t\r\n\v\f]+\z/, "")
+  end
 
-  IMAGE_RE = /\.(jpg|jpeg|gif|png)\z/i
-  BRACKET_LINK_RE = /\[\[(.+?)\]\]/
-  NAMED_LINK_RE = /(.+?)\|(.+)/
-  URI_RE = /(?:(?:https?|ftp|file):|mailto:)[A-Za-z0-9;\/?:@&=+$,\-_.!~*\'()#%]+/
+  def lstrip(str)
+    str.sub(/\A[ \t\r\n\v\f]+/, "")
+  end
 
-  def parse_link( text )
-    ret = text
-    ret.gsub!( BRACKET_LINK_RE ) do |str|
-      link = $1
-      if NAMED_LINK_RE =~ link
-        uri, title = $2, $1
-        title = parse_modifier( title )
+
+  class HTMLOutput
+    def initialize(suffix = " />")
+      @suffix = suffix
+      @f = nil
+    end
+
+    def reset
+      @f = StringIO.new
+    end
+
+    def finish
+      @f.string
+    end
+
+    def container(_for=nil)
+      case _for
+      when :paragraph
+        []
       else
-        uri = title = link
+        ""
       end
-      uri.sub!( /^(?:https?|ftp|file)+:/, '' ) if %r|://| !~ uri && /^mailto:/ !~ uri
-      store_block( %Q|<a href="#{escape_quote( uri )}">#{title}</a>| )
     end
-    ret.gsub!( URI_RE ) do |uri|
-      uri.sub!( /^\w+:/, '' ) if %r|://| !~ uri && /^mailto:/ !~ uri
-      if IMAGE_RE =~ uri
-        store_block( %Q|<img src="#{uri}" alt="#{File.basename( uri )}"#{@empty_element_suffix}| )
+
+    #
+    # Procedures
+    #
+
+    def headline(level, title)
+      @f.puts "<h#{level}>#{title}</h#{level}>"
+    end
+
+    def hrule
+      @f.puts "<hr#{@suffix}"
+    end
+
+    def list_begin
+    end
+
+    def list_end
+      @f.puts
+    end
+
+    def list_open(type)
+      @f.puts "<#{type}>"
+    end
+
+    def list_close(type)
+      @f.print "</#{type}>"
+    end
+
+    def listitem_open
+      @f.print "<li>"
+    end
+
+    def listitem_close
+      @f.puts "</li>"
+    end
+
+    def listitem(item)
+      @f.print item
+    end
+
+    def dlist_open
+      @f.puts "<dl>"
+    end
+
+    def dlist_close
+      @f.puts "</dl>"
+    end
+
+    def dlist_item(dt, dd)
+      case
+      when dd.empty?
+        @f.puts "<dt>#{dt}</dt>"
+      when dt.empty?
+        @f.puts "<dd>#{dd}</dd>"
       else
-        store_block( %Q|<a href="#{uri}">#{uri}</a>| )
+        @f.puts "<dt>#{dt}</dt>"
+        @f.puts "<dd>#{dd}</dd>"
       end
     end
-    ret
-  end
 
-  ######################################################################
-  # modifier( strong, em, re )
+    def table_open
+      @f.puts %Q(<table border="1">)
+    end
 
-  STRONG = "'''"
-  EM = "''"
-  DEL = '=='
-  MODIFIER_RE = /(#{STRONG}|#{EM}|#{DEL})(.+?)(?:\1)/
+    def table_close
+      @f.puts "</table>"
+    end
 
-  def parse_modifier( text )
-    text.gsub( MODIFIER_RE ) do |str|
-      case $1
-      when STRONG
-        store_block( "<strong>#{parse_modifier($2)}</strong>" )
-      when EM
-        store_block( "<em>#{parse_modifier($2)}</em>" )
-      when DEL
-        store_block( "<del>#{parse_modifier($2)}</del>" )
+    def table_record_open
+      @f.print "<tr>"
+    end
+
+    def table_record_close
+      @f.puts "</tr>"
+    end
+
+    def table_head(item, rs, cs)
+      @f.print "<th#{tdattr(rs, cs)}>#{item}</th>"
+    end
+
+    def table_data(item, rs, cs)
+      @f.print "<td#{tdattr(rs, cs)}>#{item}</td>"
+    end
+
+    def tdattr(rs, cs)
+      buf = ""
+      buf << %Q( rowspan="#{rs}") if rs
+      buf << %Q( colspan="#{cs}") if cs
+      buf
+    end
+    private :tdattr
+
+    def blockquote_open
+      @f.print "<blockquote>"
+    end
+
+    def blockquote_close
+      @f.puts "</blockquote>"
+    end
+
+    def block_preformatted(str, info)
+      syntax = info ? info.downcase : nil
+      if syntax
+        begin
+          convertor = Syntax::Convertors::HTML.for_syntax(syntax)
+          @f.puts convertor.convert(str)
+          return
+        rescue NameError, RuntimeError
+        end
       end
+      preformatted(text(str))
     end
-  end
 
-  ######################################################################
-  # utility methods
-  ######################################################################
+    def preformatted(str)
+      @f.print "<pre>"
+      @f.print str
+      @f.puts "</pre>"
+    end
 
-  def escape_html( text )
-    text.gsub( /&/, '&amp;' ).
-      gsub( /</, '&lt;' ).
-      gsub( />/, '&gt;' )
-  end
+    def paragraph(lines)
+      @f.puts "<p>#{lines.join("\n")}</p>"
+    end
 
-  def escape_quote( text )
-    text.gsub( /"/, '&quot;' )
-  end
+    def block_plugin(str)
+      @f.puts %Q(<div class="plugin">{{#{escape_html(str)}}}</div>)
+    end
 
-  def store_block( text )
-    key = "<#{@stack.size}>"
-    @stack << text
-    key
-  end
+    #
+    # Functions
+    #
 
-  BLOCK_RE = /<(\d+)>/
+    def hyperlink(uri, title)
+      %Q(<a href="#{escape_html_param(uri)}">#{title}</a>)
+    end
 
-  def restore_block( text )
-    return text if****@stack*****?
-    ret = text.dup
-    while ret.gsub!( BLOCK_RE ) { |str|
-      ( @stack[$1.to_i] || '' ).rstrip
-      }
+    def wiki_name(name)
+      hyperlink(name, text(name))
     end
-    ret
-  end
 
-  def store_plugin_block( text )
-    key = "<!#{@plugin_stack.size}>"
-    @plugin_stack << text
-    key
+    def image_hyperlink(uri, alt = nil)
+      alt ||= uri.split(/\//).last
+      alt = escape_html(alt)
+      %Q(<img src="#{escape_html_param(uri)}" alt="#{alt}"#{@suffix})
+    end
+
+    def strong(item)
+      "<strong>#{item}</strong>"
+    end
+
+    def em(item)
+      "<em>#{item}</em>"
+    end
+
+    def del(item)
+      "<del>#{item}</del>"
+    end
+
+    def text(str)
+      escape_html(str)
+    end
+
+    def inline_plugin(src)
+      %Q(<span class="plugin">{{#{src}}}</span>)
+    end
+
+    #
+    # Utilities
+    #
+
+    def escape_html_param(str)
+      escape_quote(escape_html(str))
+    end
+
+    def escape_html(text)
+      text.gsub(/&/, "&amp;").gsub(/</, "&lt;").gsub(/>/, "&gt;")
+    end
+
+    def unescape_html(text)
+      text.gsub(/&gt;/, ">").gsub(/&lt;/, "<").gsub(/&amp;/, "&")
+    end
+
+    def escape_quote(text)
+      text.gsub(/"/, "&quot;")
+    end
   end
 
-  PLUGIN_BLOCK_RE = /<!(\d+)>/
-  INLINE_PLUGIN_RE = %r|<p><!(\d+)></p>|
-  INLINE_PLUGIN_OPEN = '<span class="plugin">'
-  INLINE_PLUGIN_CLOSE = '</span>'
-  BLOCK_PLUGIN_OPEN = '<div class="plugin">'
-  BLOCK_PLUGIN_CLOSE = '</div>'
 
-  def restore_plugin_block( text, original = false )
-    return text if @plugin_stack.empty?
-    if original
-      text.gsub!( PLUGIN_BLOCK_RE ) do |str|
-        @plugin_stack[$1.to_i]
+  class LineInput
+    def initialize(f)
+      @input = f
+      @buf = []
+      @lineno = 0
+      @eof_p = false
+    end
+
+    def inspect
+      "\#<#{self.class} file=#{@input.inspect} line=#{lineno()}>"
+    end
+
+    def eof?
+      @eof_p
+    end
+
+    def lineno
+      @lineno
+    end
+
+    def gets
+      unles****@buf*****?
+        @lineno += 1
+        retur****@buf*****
       end
-    else
-      # block plugin
-      text.gsub!( INLINE_PLUGIN_RE ) do |str|
-        "#{BLOCK_PLUGIN_OPEN}#{@plugin_stack[$1.to_i]}#{BLOCK_PLUGIN_CLOSE}"
+      return nil if @eof_p   # to avoid ARGF blocking.
+      line =****@input*****
+      line = line.sub(/\r\n/, "\n") if line
+      @eof_p = line.nil?
+      @lineno += 1
+      line
+    end
+
+    def ungets(line)
+      return unless line
+      @lineno -= 1
+      @buf.push line
+      line
+    end
+
+    def peek
+      line = gets()
+      ungets line if line
+      line
+    end
+
+    def next?
+      peek() ? true : false
+    end
+
+    def skip_blank_lines
+      n = 0
+      while line = gets()
+        unless line.strip.empty?
+          ungets line
+          return n
+        end
+        n += 1
       end
-      text.gsub!( PLUGIN_BLOCK_RE ) do |str|
-        "#{INLINE_PLUGIN_OPEN}#{@plugin_stack[$1.to_i]}#{INLINE_PLUGIN_CLOSE}"
+      n
+    end
+
+    def gets_if(re)
+      line = gets()
+      if not line or not (re =~ line)
+        ungets line
+        return nil
       end
+      line
     end
-    text
-  end
 
-  META_CHAR_RE = /\\\{|\\\}|\\:|\\'|\\"|\\\|/
+    def gets_unless(re)
+      line = gets()
+      if not line or re =~ line
+        ungets line
+        return nil
+      end
+      line
+    end
 
-  def escape_meta_char( text )
-    text.gsub( META_CHAR_RE ) do |s|
-      '&#x%x;' % s[1]
+    def each
+      while line = gets()
+        yield line
+      end
     end
-  end
 
-  ESCAPED_META_CHAR_RE = /(?:&\#x([0-9a-f]{2});)/i
+    def while_match(re)
+      while line = gets()
+        unless re =~ line
+          ungets line
+          return
+        end
+        yield line
+      end
+      nil
+    end
 
-  def unescape_meta_char( text, original = false )
-    text.gsub( ESCAPED_META_CHAR_RE ) do
-      if original
-        '\\' + [$1].pack( 'H2' )
-      else
-        [$1].pack( 'H2' )
+    def getlines_while(re)
+      buf = []
+      while_match(re) do |line|
+        buf.push line
       end
+      buf
     end
+
+    alias span getlines_while   # from Haskell
+
+    def until_match(re)
+      while line = gets()
+        if re =~ line
+          ungets line
+          return
+        end
+        yield line
+      end
+      nil
+    end
+
+    def getlines_until(re)
+      buf = []
+      until_match(re) do |line|
+        buf.push line
+      end
+      buf
+    end
+
+    alias break getlines_until   # from Haskell
+
+    def until_terminator(re)
+      while line = gets()
+        return if re =~ line   # discard terminal line
+        yield line
+      end
+      nil
+    end
+
+    def getblock(term_re)
+      buf = []
+      until_terminator(term_re) do |line|
+        buf.push line
+      end
+      buf
+    end
   end
 end
 
 if __FILE__ == $0
-  puts HikiDoc.new( ARGF.read ).to_html
+  puts HikiDoc.to_html(ARGF.read(nil))
 end




Hiki-cvs メーリングリストの案内
Back to archive index