1  #!/usr/bin/env ruby
   2  
   3  require 'logger'
   4  require 'strscan'
   5  require 'uri'
   6  
   7  class PukipaParseError < StandardError;end
   8  
   9  class Pukipa
  10    def initialize(plain)
  11      @plain = plain 
  12  		@log = Logger::new( $deferr )
  13  		@log.level = $DEBUG ?
  14  			Logger::DEBUG :
  15  			($VERBOSE ? Logger::INFO : Logger::WARN)
  16      @h_start_level = 2
  17      @pagelist = nil
  18    end
  19  
  20    # arrayでpagelistを渡す
  21    # pagelistはwikiページ一覧
  22    def pagelist(pagelist,base_uri = '/',suffix = '/')
  23      @base_uri = base_uri
  24      @pagelist_suffix = suffix
  25      if pagelist.size > 0
  26        #三文字以下はそもそも対象外
  27        pagelist.reject!{|pn| pn.size <= 3 }
  28        pagelist.map!{|pn| Regexp.escape(pn)}
  29        @pagelist = Regexp.new('(?!<a.*?>.*?)((?:' + pagelist.join(')|(?:') + '))(?!.*?</a>)',Regexp::IGNORECASE )
  30      end
  31    end
  32  
  33    # htmlに変換して返す
  34    def to_html
  35      plain = @plain.gsub(/\r?\n/,"\n").chomp + "\n"
  36      result = []
  37      block_regex = /^([:\-+> ]).*\n(?:(?:\1.*\n)+)?/
  38  		@scanner = StringScanner.new( plain )
  39      while !@scanner.eos?
  40        if @scanner.scan(/^\n/)
  41          #空行
  42        elsif @scanner.scan(/^----.*?\n/)
  43          result << '<hr />'
  44        elsif @scanner.scan(block_regex)
  45          result << block_parse(@scanner.matched)
  46        elsif @scanner.scan(/^\*.*?\n/)
  47          result << h_parse(@scanner.matched)
  48        elsif @scanner.scan(/^([^*:\-+> \n].+?\n){1,}/)
  49          result << block_parse(@scanner.matched)
  50        elsif @scanner.scan(/(.*)/)
  51          result << block_parse(@scanner.matched)
  52        else
  53          #ここにはこないはずなので・・・
  54          raise PukipaParseError.new
  55        end
  56      end
  57      result.join("\n")
  58    end
  59    
  60    protected
  61    # h(見出し) のパーサ
  62    def h_parse(str)
  63      str.chomp!
  64  
  65      h_regexs = {
  66        '*'    => 'h' + @h_start_level.to_s,
  67        '**'   => 'h' + (@h_start_level + 1).to_s,
  68        '***'  => 'h' + (@h_start_level + 2).to_s,
  69        '****' => 'h' + (@h_start_level + 3).to_s,
  70      }
  71      h_regexs.each do |tmp_regex,prefix|
  72        regex = Regexp.new('^' + Regexp.escape(tmp_regex) + '([^*])')
  73        if str =~ regex 
  74          str.gsub!(regex,"\\1").gsub!(/^\s+/,'')
  75          str = "<%s>%s</%s>" % [prefix,str_parse(str),prefix]
  76          break
  77        end
  78      end
  79      @log.debug "inline:\n%s" % str
  80      str
  81    end
  82    
  83    #blockなもののパーサ
  84    def block_parse(str)
  85      str.chomp!
  86  
  87      if str =~ /^ /
  88        str = str.map{|s| s.gsub(/^ /,'')}.join
  89        # preの時はstr_parseしない
  90        str = ['<pre><code>' + escapeHTML(str),'</code></pre>'].join "\n"
  91      elsif str =~ /^>/
  92        str = str.map{|s| s.gsub(/^>/,'')}.join
  93        str = ['<blockquote><p>',str_parse(str),'</p></blockquote>'].join "\n"
  94      elsif str =~ /^-/
  95        str = list_parse(str,'ul')
  96      elsif str =~ /^\+/
  97        str = list_parse(str,'ol')
  98      elsif str =~ /^:/
  99        str = dl_parse(str)
 100      else
 101        str = ['<p>',str_parse(str),'</p>'].join "\n"
 102      end
 103      @log.debug "block:\n%s" % str
 104      str
 105    end
 106  
 107    # liのパーサ
 108    def list_parse(str,list)
 109      if list == 'ul'
 110        regex = /^-/
 111      else
 112        regex = /^\+/
 113      end
 114      str = str.map{|s| s.gsub(regex,'')}.join
 115      result = []
 116      result << "<#{list}>"
 117      tmp = []
 118      str.each_line do |s|
 119        s.chomp!
 120        if s =~ regex
 121          tmp << s.gsub(regex,'')
 122        else
 123          if tmp.size > 0
 124            result.pop
 125            result << list_parse(tmp.join("\n"),list)
 126            result << "</li>"
 127            tmp.clear
 128          end
 129          result << "<li>#{str_parse(s)}"
 130          result << "</li>"
 131        end
 132      end
 133      if tmp.size > 0
 134        result.pop
 135        result << list_parse(tmp.join("\n"),list)
 136        result << "</li>"
 137        tmp.clear
 138      end
 139      result << "</#{list}>"
 140      result.join "\n"
 141    end
 142  
 143    # dlのパーサ
 144    def dl_parse(str)
 145      regex = /^:/
 146      regex2 = /(.*?)\|(.*)/
 147      str = str.map{|s| s.gsub(regex,'')}.join
 148      result = []
 149      result << "<dl>"
 150      tmp = []
 151      str.each_line do |s|
 152        s.chomp!
 153        m = regex2.match(s)
 154        if m
 155          result << "<dt>" + str_parse(m[1]) + "</dt>"
 156          result << "<dd>" + str_parse(m[2]) + "</dd>"
 157        else
 158          result << "<dt>" + str_parse(s) + "</dt>"
 159        end
 160      end
 161      result << "</dl>"
 162      result.join "\n"
 163    end
 164  
 165    #文字列のパーサ
 166    def str_parse(str)
 167      #URIへの自動リンクは一番最初にやる
 168      uri_regex = Regexp.new('(?!\[\[.*?)(' + URI.regexp('http').source + ')(?!.*?\]\])',Regexp::EXTENDED)
 169      str.gsub!(uri_regex) do |match|
 170        uri = $1.dup
 171        re = match
 172        re = '[[%s:%s]]' % [uri,uri] if not uri =~ /\]\]$/
 173        re 
 174      end
 175  
 176      str = escapeHTML(str)
 177  
 178      #リンク
 179      str.gsub!(/\[\[(.+?):\s*(https?:\/\/.+?)\s*\]\]/) do
 180        name = $1.dup
 181        uri = $2.dup
 182        '<a class="outlink" href="%s">%s</a>' % [uri,name]
 183      end
 184  
 185      #ページにリンク
 186      if @pagelist
 187        str.gsub!(@pagelist) do
 188          s = $1.dup
 189          '<a class="pagelink" href="%s%s%s">%s</a>' % [@base_uri,escape(s),@pagelist_suffix,s]
 190        end
 191      end
 192  
 193      str
 194    end
 195      
 196    def escapeHTML(string)
 197      string.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
 198    end
 199  
 200    def escape(string)
 201      string.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
 202        '%' + $1.unpack('H2' * $1.size).join('%').upcase
 203      end.tr(' ', '+')
 204    end
 205  end
 206  
 207  if $0 == __FILE__
 208    body =<<EOF
 209  *Wiki文法
 210  
 211  **見出し
 212   *h2
 213   **h3
 214   ***h4
 215   ****h5
 216  *h2
 217  **h3
 218  ***h4
 219  ****h5
 220  
 221  **リスト
 222   -list
 223   -list2
 224   --list2-1
 225   --list2-2
 226  -list
 227  -list2
 228  --list2-1
 229  --list2-2
 230  
 231  **番号付きリスト
 232   +olist
 233   +olist2
 234   ++olist2-1
 235   ++olist2-2
 236  +olist
 237  +olist2
 238  ++olist2-1
 239  ++olist2-2
 240  
 241  **引用
 242   >引用文
 243   >example
 244  >引用文
 245  >example
 246  
 247  **pre
 248  頭にスペースを入れる
 249   def takahashi
 250     'takahashi'
 251   end
 252  
 253  **定義語
 254   :apple|リンゴ
 255   :orange|オランゲ
 256  :apple|リンゴ
 257  :orange|オランゲ
 258  
 259  **リンク
 260   [[example:http://example.com]]
 261  [[example:http://example.com]]
 262  
 263  ページ名には自動リンクされる。
 264  
 265  例:)文法
 266  EOF
 267    pu = Pukipa.new(body)
 268    pu.pagelist(['文法','自動リンク'])
 269    puts pu.to_html
 270  end