migrate_from_trac_v4.patch

Mike Stupalov, 2010-03-22 15:08

Download (25.9 KB)

View differences:

lib/tasks/migrate_from_trac.rake (working copy)
19 19
require 'iconv'
20 20
require 'pp'
21 21

  
22
require 'redmine/scm/adapters/abstract_adapter'
23
require 'redmine/scm/adapters/subversion_adapter'
24
require 'rexml/document'
25
require 'uri'
26
require 'tempfile'
27

  
22 28
namespace :redmine do
23 29
  desc 'Trac migration script'
24 30
  task :migrate_from_trac => :environment do
......
192 198
        def time; Time.at(read_attribute(:time)) end
193 199
      end
194 200

  
195
      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
201
      TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup \
202
                           TracBrowser TracCgi TracChangeset TracInstallPlatforms TracMultipleProjects TracModWSGI \
196 203
                           TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
197 204
                           TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
198 205
                           TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
199 206
                           TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
200 207
                           WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
201 208
                           CamelCase TitleIndex)
202

  
203 209
      class TracWikiPage < ActiveRecord::Base
204 210
        set_table_name :wiki
205 211
        set_primary_key :name
......
241 247
          if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
242 248
            name = name_attr.value
243 249
          end
244
          name =~ (/(.*)(\s+\w+)?/)
250
          name =~ (/(.+?)(?:[\ \t]+(.+)?|[\ \t]+|)$/)
245 251
          fn = $1.strip
246 252
          ln = ($2 || '-').strip
247 253

  
......
271 277

  
272 278
      # Basic wiki syntax conversion
273 279
      def self.convert_wiki_text(text)
274
        # Titles
275
        text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
276
        # External Links
277
        text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
278
        # Ticket links:
279
        #      [ticket:234 Text],[ticket:234 This is a test]
280
        text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
281
        #      ticket:1234
282
        #      #1 is working cause Redmine uses the same syntax.
283
        text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
284
        # Milestone links:
285
        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
286
        #      The text "Milestone 0.1.0 (Mercury)" is not converted,
287
        #      cause Redmine's wiki does not support this.
288
        text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
289
        #      [milestone:"0.1.0 Mercury"]
290
        text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
291
        text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
292
        #      milestone:0.1.0
293
        text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
294
        text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
295
        # Internal Links
296
        text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
297
        text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
298
        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
299
        text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
300
        text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
301
        text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
302

  
303
  # Links to pages UsingJustWikiCaps
304
  text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
305
  # Normalize things that were supposed to not be links
306
  # like !NotALink
307
  text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
308
        # Revisions links
309
        text = text.gsub(/\[(\d+)\]/, 'r\1')
310
        # Ticket number re-writing
311
        text = text.gsub(/#(\d+)/) do |s|
312
          if $1.length < 10
313
#            TICKET_MAP[$1.to_i] ||= $1
314
            "\##{TICKET_MAP[$1.to_i] || $1}"
315
          else
316
            s
317
          end
318
        end
319
        # We would like to convert the Code highlighting too
320
        # This will go into the next line.
321
        shebang_line = false
322
        # Reguar expression for start of code
323
        pre_re = /\{\{\{/
324
        # Code hightlighing...
325
        shebang_re = /^\#\!([a-z]+)/
326
        # Regular expression for end of code
327
        pre_end_re = /\}\}\}/
328

  
329
        # Go through the whole text..extract it line by line
330
        text = text.gsub(/^(.*)$/) do |line|
331
          m_pre = pre_re.match(line)
332
          if m_pre
333
            line = '<pre>'
334
          else
335
            m_sl = shebang_re.match(line)
336
            if m_sl
337
              shebang_line = true
338
              line = '<code class="' + m_sl[1] + '">'
339
            end
340
            m_pre_end = pre_end_re.match(line)
341
            if m_pre_end
342
              line = '</pre>'
343
              if shebang_line
344
                line = '</code>' + line
345
              end
346
            end
347
          end
348
          line
349
        end
350

  
351
        # Highlighting
352
        text = text.gsub(/'''''([^\s])/, '_*\1')
353
        text = text.gsub(/([^\s])'''''/, '\1*_')
354
        text = text.gsub(/'''/, '*')
355
        text = text.gsub(/''/, '_')
356
        text = text.gsub(/__/, '+')
357
        text = text.gsub(/~~/, '-')
358
        text = text.gsub(/`/, '@')
359
        text = text.gsub(/,,/, '~')
360
        # Lists
361
        text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
362

  
363
        text
280
        convert_wiki_text_mapping(text, TICKET_MAP)
364 281
      end
365 282

  
366 283
      def self.migrate
......
400 317
        # Milestones
401 318
        print "Migrating milestones"
402 319
        version_map = {}
320
        milestone_wiki = Array.new
403 321
        TracMilestone.find(:all).each do |milestone|
404 322
          print '.'
405 323
          STDOUT.flush
......
419 337

  
420 338
          next unless v.save
421 339
          version_map[milestone.name] = v
340
          milestone_wiki.push(milestone.name);
422 341
          migrated_milestones += 1
423 342
        end
424 343
        puts
......
456 375
        r.save!
457 376
        custom_field_map['resolution'] = r
458 377

  
378
        # Trac 'keywords' field as a Redmine custom field
379
        k = IssueCustomField.find(:first, :conditions => { :name => "Keywords" })
380
        k = IssueCustomField.new(:name => 'Keywords',
381
                                 :field_format => 'string',
382
                                 :is_filter => true) if k.nil?
383
        k.trackers = Tracker.find(:all)
384
        k.projects << @target_project
385
        k.save!
386
        custom_field_map['keywords'] = k
387

  
388
        # Trac ticket id as a Redmine custom field
389
        tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
390
        tid = IssueCustomField.new(:name => 'TracID',
391
                                 :field_format => 'string',
392
                                 :is_filter => true) if tid.nil?
393
        tid.trackers = Tracker.find(:all)
394
        tid.projects << @target_project
395
        tid.save!
396
        custom_field_map['tracid'] = tid
397
  
459 398
        # Tickets
460 399
        print "Migrating tickets"
461 400
          TracTicket.find_each(:batch_size => 200) do |ticket|
......
463 402
          STDOUT.flush
464 403
          i = Issue.new :project => @target_project,
465 404
                          :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
466
                          :description => convert_wiki_text(encode(ticket.description)),
405
                          :description => encode(ticket.description),
467 406
                          :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
468 407
                          :created_on => ticket.time
469 408
          i.author = find_or_create_user(ticket.reporter)
......
482 421
              Time.fake(ticket.changetime) { i.save }
483 422
            end
484 423

  
485
          # Comments and status/resolution changes
424
          # Comments and status/resolution/keywords changes
486 425
          ticket.changes.group_by(&:time).each do |time, changeset|
487 426
              status_change = changeset.select {|change| change.field == 'status'}.first
488 427
              resolution_change = changeset.select {|change| change.field == 'resolution'}.first
428
              keywords_change = changeset.select {|change| change.field == 'keywords'}.first
489 429
              comment_change = changeset.select {|change| change.field == 'comment'}.first
490 430

  
491
              n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
431
              n = Journal.new :notes => (comment_change ? encode(comment_change.newvalue) : ''),
492 432
                              :created_on => time
493 433
              n.user = find_or_create_user(changeset.first.author)
494 434
              n.journalized = i
......
507 447
                                               :old_value => resolution_change.oldvalue,
508 448
                                               :value => resolution_change.newvalue)
509 449
              end
450
              if keywords_change
451
                n.details << JournalDetail.new(:property => 'cf',
452
                                               :prop_key => custom_field_map['keywords'].id,
453
                                               :old_value => keywords_change.oldvalue,
454
                                               :value => keywords_change.newvalue)
455
              end
510 456
              n.save unless n.details.empty? && n.notes.blank?
511 457
          end
512 458

  
......
534 480
          if custom_field_map['resolution'] && !ticket.resolution.blank?
535 481
            custom_values[custom_field_map['resolution'].id] = ticket.resolution
536 482
          end
483
          if custom_field_map['keywords'] && !ticket.keywords.blank?
484
            custom_values[custom_field_map['keywords'].id] = ticket.keywords
485
          end
486
          if custom_field_map['tracid'] 
487
            custom_values[custom_field_map['tracid'].id] = ticket.id
488
          end
537 489
          i.custom_field_values = custom_values
538 490
          i.save_custom_field_values
539 491
        end
......
576 528
            end
577 529
          end
578 530

  
531
        end
532
        puts
533

  
534
    # Now load each wiki page and transform its content into textile format
535
    print "Transform texts to textile format:"
536
    puts
537

  
538
    print "   in Wiki pages..................."
579 539
          wiki.reload
580 540
          wiki.pages.each do |page|
541
            #print '.'
581 542
            page.content.text = convert_wiki_text(page.content.text)
582 543
            Time.fake(page.content.updated_on) { page.content.save }
583 544
          end
584
        end
585
        puts
545
    puts
586 546

  
547
    print "   in Issue descriptions..........."
548
          TICKET_MAP.each do |newId|
549

  
550
            next if newId.nil?
551
            
552
            #print '.'
553
            issue = findIssue(newId)
554
            next if issue.nil?
555

  
556
            issue.description = convert_wiki_text(issue.description)
557
      issue.save            
558
          end
559
    puts
560

  
561
    print "   in Issue journal descriptions..."
562
          TICKET_MAP.each do |newId|
563
            next if newId.nil?
564
            
565
            #print '.'
566
            issue = findIssue(newId)
567
            next if issue.nil?
568
            
569
            issue.journals.find(:all).each do |journal|
570
              #print '.'
571
              journal.notes = convert_wiki_text(journal.notes)
572
              journal.save
573
            end
574
  
575
          end
576
    puts
577

  
578
    print "   in Milestone descriptions......."
579
          milestone_wiki.each do |name|
580
            p = wiki.find_page(name)            
581
            next if p.nil?
582
                  
583
            #print '.'            
584
            p.content.text = convert_wiki_text(p.content.text)
585
            p.content.save
586
    end
587
    puts
588

  
587 589
        puts
588 590
        puts "Components:      #{migrated_components}/#{TracComponent.count}"
589 591
        puts "Milestones:      #{migrated_milestones}/#{TracMilestone.count}"
......
593 595
        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edit_count}"
594 596
        puts "Wiki files:      #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
595 597
      end
598
      
599
      def self.findIssue(id)
600
        
601
        return Issue.find(id)
596 602

  
603
      rescue ActiveRecord::RecordNotFound
604
  puts
605
        print "[#{id}] not found"
606

  
607
        nil      
608
      end
609
      
597 610
      def self.limit_for(klass, attribute)
598 611
        klass.columns_hash[attribute.to_s].limit
599 612
      end
......
746 759
    DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
747 760

  
748 761
    prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
749
    prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
762
    prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
750 763
    unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
751 764
      prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
752 765
      prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
......
764 777
    
765 778
    TracMigrate.migrate
766 779
  end
780

  
781

  
782
  desc 'Subversion migration script'
783
  task :migrate_from_trac_svn => :environment do
784
  
785
    module SvnMigrate 
786
        TICKET_MAP = []
787

  
788
        class Commit
789
          attr_accessor :revision, :message
790
          
791
          def initialize(attributes={})
792
            self.message = attributes[:message] || ""
793
            self.revision = attributes[:revision]
794
          end
795
        end
796
        
797
        class SvnExtendedAdapter < Redmine::Scm::Adapters::SubversionAdapter
798
        
799

  
800

  
801
            def set_message(path=nil, revision=nil, msg=nil)
802
              path ||= ''
803

  
804
              Tempfile.open('msg') do |tempfile|
805

  
806
                # This is a weird thing. We need to cleanup cr/lf so we have uniform line separators              
807
                tempfile.print msg.gsub(/\r\n/,'\n')
808
                tempfile.flush
809

  
810
                filePath = tempfile.path.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
811

  
812
                cmd = "#{SVN_BIN} propset svn:log --quiet --revprop -r #{revision}  -F \"#{filePath}\" "
813
                cmd << credentials_string
814
                cmd << ' ' + target(URI.escape(path))
815

  
816
                shellout(cmd) do |io|
817
                  begin
818
                    loop do 
819
                      line = io.readline
820
                      puts line
821
                    end
822
                  rescue EOFError
823
                  end  
824
                end
825

  
826
                raise if $? && $?.exitstatus != 0
827

  
828
              end
829
              
830
            end
831
        
832
            def messages(path=nil)
833
              path ||= ''
834

  
835
              commits = Array.new
836

  
837
              cmd = "#{SVN_BIN} log --xml -r 1:HEAD"
838
              cmd << credentials_string
839
              cmd << ' ' + target(URI.escape(path))
840
                            
841
              shellout(cmd) do |io|
842
                begin
843
                  doc = REXML::Document.new(io)
844
                  doc.elements.each("log/logentry") do |logentry|
845

  
846
                    commits << Commit.new(
847
                                                {
848
                                                  :revision => logentry.attributes['revision'].to_i,
849
                                                  :message => logentry.elements['msg'].text
850
                                                })
851
                  end
852
                rescue => e
853
                  puts"Error !!!"
854
                  puts e
855
                end
856
              end
857
              return nil if $? && $?.exitstatus != 0
858
              commits
859
            end
860
          
861
        end
862
        
863
        def self.migrate
864

  
865
          project = Project.find(@@redmine_project)
866
          if !project
867
            puts "Could not find project identifier '#{@@redmine_project}'"
868
            raise 
869
          end
870
                    
871
          tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
872
          if !tid
873
            puts "Could not find issue custom field 'TracID'"
874
            raise 
875
          end
876
          
877
          Issue.find( :all, :conditions => { :project_id => project }).each do |issue|
878
            val = nil
879
            issue.custom_values.each do |value|
880
              if value.custom_field.id == tid.id
881
                val = value
882
                break
883
              end
884
            end
885
            
886
            TICKET_MAP[val.value.to_i] = issue.id if !val.nil?            
887
          end
888
          
889
          svn = self.scm          
890
          msgs = svn.messages(@svn_url)
891
          msgs.each do |commit| 
892
          
893
            newText = convert_wiki_text(commit.message)
894
            
895
            if newText != commit.message             
896
              puts "Updating message #{commit.revision}"
897
              scm.set_message(@svn_url, commit.revision, newText)
898
            end
899
          end
900
          
901
          
902
        end
903
        
904
        # Basic wiki syntax conversion
905
        def self.convert_wiki_text(text)
906
          convert_wiki_text_mapping(text, TICKET_MAP )
907
        end
908
        
909
        def self.set_svn_url(url)
910
          @@svn_url = url
911
        end
912

  
913
        def self.set_svn_username(username)
914
          @@svn_username = username
915
        end
916

  
917
        def self.set_svn_password(password)
918
          @@svn_password = password
919
        end
920

  
921
        def self.set_redmine_project_identifier(identifier)
922
          @@redmine_project = identifier
923
        end
924
      
925
        def self.scm
926
          @scm ||= SvnExtendedAdapter.new @@svn_url, @@svn_url, @@svn_username, @@svn_password, 0, "", nil
927
          @scm
928
        end
929
    end
930
    
931
    def prompt(text, options = {}, &block)
932
      default = options[:default] || ''
933
      while true
934
        print "#{text} [#{default}]: "
935
        value = STDIN.gets.chomp!
936
        value = default if value.blank?
937
        break if yield value
938
      end
939
    end
940

  
941
    puts
942
    if Redmine::DefaultData::Loader.no_data?
943
      puts "Redmine configuration need to be loaded before importing data."
944
      puts "Please, run this first:"
945
      puts
946
      puts "  rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
947
      exit
948
    end
949

  
950
    puts "WARNING: all commit messages with references to trac pages will be modified"
951
    print "Are you sure you want to continue ? [y/N] "
952
    break unless STDIN.gets.match(/^y$/i)
953
    puts
954

  
955
    prompt('Subversion repository url') {|repository| SvnMigrate.set_svn_url repository.strip}
956
    prompt('Subversion repository username') {|username| SvnMigrate.set_svn_username username}
957
    prompt('Subversion repository password') {|password| SvnMigrate.set_svn_password password}
958
    prompt('Redmine project identifier') {|identifier| SvnMigrate.set_redmine_project_identifier identifier}
959
    puts
960

  
961
    SvnMigrate.migrate
962
    
963
  end
964

  
965

  
966
  # Basic wiki syntax conversion
967
  def convert_wiki_text_mapping(text, ticket_map = [])
968
        # New line
969
        text = text.gsub(/\[\[[Bb][Rr]\]\]/, "\n") # This has to go before the rules below
970
        # Titles (only h1. to h6., and remove #...)
971
        text = text.gsub(/(?:^|^\ +)(\={1,6})\ (.+)\ (?:\1)(?:\ *(\ \#.*))?/) {|s| "\nh#{$1.length}. #{$2}#{$3}\n"}
972
        
973
        # External Links:
974
        #      [http://example.com/]
975
        text = text.gsub(/\[((?:https?|s?ftp)\:\S+)\]/, '\1')
976
        #      [http://example.com/ Example],[http://example.com/ "Example"]
977
        #      [http://example.com/ "Example for "Example""] -> "Example for 'Example'":http://example.com/
978
        text = text.gsub(/\[((?:https?|s?ftp)\:\S+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":#{$1}"}
979
        #      [mailto:some@example.com],[mailto:"some@example.com"]
980
        text = text.gsub(/\[mailto\:([\"']?)(.+?)\1\]/, '\2')
981
        
982
        # Ticket links:
983
        #      [ticket:234 Text],[ticket:234 This is a test],[ticket:234 "This is a test"]
984
        #      [ticket:234 "Test "with quotes""] -> "Test 'with quotes'":issues/show/234
985
        text = text.gsub(/\[ticket\:(\d+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":/issues/show/#{$1}"}
986
        #      ticket:1234
987
        #      excluding ticket:1234:file.txt (used in macros)
988
        #      #1 - working cause Redmine uses the same syntax.
989
        text = text.gsub(/ticket\:(\d+?)([^\:])/, '#\1\2')
990

  
991
        # Source & attachments links:
992
        #      [source:/trunk/readme.txt Readme File],[source:"/trunk/readme.txt" Readme File],
993
        #      [source:/trunk/readme.txt],[source:"/trunk/readme.txt"]
994
        #       The text "Readme File" is not converted,
995
        #       cause Redmine's wiki does not support this.
996
        #      Attachments use same syntax.
997
        text = text.gsub(/\[(source|attachment)\:([\"']?)([^\"']+?)\2(?:\ +(.+?))?\]/, '\1:"\3"')
998
        #      source:"/trunk/readme.txt"
999
        #      source:/trunk/readme.txt - working cause Redmine uses the same syntax.
1000
        text = text.gsub(/(source|attachment)\:([\"'])([^\"']+?)\2/, '\1:"\3"')
1001

  
1002
        # Milestone links:
1003
        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)],
1004
        #      [milestone:"0.1.0 Mercury"],milestone:"0.1.0 Mercury"
1005
        #       The text "Milestone 0.1.0 (Mercury)" is not converted,
1006
        #       cause Redmine's wiki does not support this.
1007
        text = text.gsub(/\[milestone\:([\"'])([^\"']+?)\1(?:\ +(.+?))?\]/, 'version:"\2"')
1008
        text = text.gsub(/milestone\:([\"'])([^\"']+?)\1/, 'version:"\2"')
1009
        #      [milestone:0.1.0],milestone:0.1.0
1010
        text = text.gsub(/\[milestone\:([^\ ]+?)\]/, 'version:\1')
1011
        text = text.gsub(/milestone\:([^\ ]+?)/, 'version:\1')
1012

  
1013
        # Internal Links:
1014
        #      ["Some Link"]
1015
        text = text.gsub(/\[([\"'])(.+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
1016
        #      [wiki:"Some Link" "Link description"],[wiki:"Some Link" Link description]
1017
        text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1[\ \t]+([\"']?)(.+?)\3\]/) {|s| "[[#{$2.delete(',./?;|:')}|#{$4}]]"}
1018
        #      [wiki:"Some Link"]
1019
        text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
1020
        #      [wiki:SomeLink]
1021
        text = text.gsub(/\[wiki\:([^\s\]]+?)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
1022
        #      [wiki:SomeLink Link description],[wiki:SomeLink "Link description"]
1023
        text = text.gsub(/\[wiki\:([^\s\]\"']+?)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$3}]]"}
1024

  
1025
        # Links to CamelCase pages (not work for unicode)
1026
        #      UsingJustWikiCaps,UsingJustWikiCaps/Subpage
1027
        text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+(?:\/[^\s[:punct:]]+)*)/) {|s| "#{$1}#{$2}[[#{$3.delete('/')}]]"}
1028
        # Normalize things that were supposed to not be links
1029
        # like !NotALink
1030
        text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
1031

  
1032
        # Revisions links
1033
        text = text.gsub(/\[(\d+)\]/, 'r\1')
1034
        # Ticket number re-writing
1035
        text = text.gsub(/#(\d+)/) do |s|
1036
          if $1.length < 10
1037
#            ticket_map[$1.to_i] ||= $1
1038
            "\##{ticket_map[$1.to_i] || $1}"
1039
          else
1040
            s
1041
          end
1042
        end
1043
        
1044
        # Before convert Code highlighting, need processing inline code
1045
        #      {{{hello world}}}
1046
        text = text.gsub(/\{\{\{(.+?)\}\}\}/, '@\1@')
1047
        
1048
        # We would like to convert the Code highlighting too
1049
        # This will go into the next line.
1050
        shebang_line = false
1051
        # Reguar expression for start of code
1052
        pre_re = /\{\{\{/
1053
        # Code hightlighing...
1054
        shebang_re = /^\#\!([a-z]+)/
1055
        # Regular expression for end of code
1056
        pre_end_re = /\}\}\}/
1057

  
1058
        # Go through the whole text..extract it line by line
1059
        text = text.gsub(/^(.*)$/) do |line|
1060
          m_pre = pre_re.match(line)
1061
          if m_pre
1062
            line = '<pre>'
1063
          else
1064
            m_sl = shebang_re.match(line)
1065
            if m_sl
1066
              shebang_line = true
1067
              line = '<code class="' + m_sl[1] + '">'
1068
            end
1069
            m_pre_end = pre_end_re.match(line)
1070
            if m_pre_end
1071
              line = '</pre>'
1072
              if shebang_line
1073
                line = '</code>' + line
1074
              end
1075
            end
1076
          end
1077
          line
1078
        end
1079

  
1080
        # Highlighting
1081
        text = text.gsub(/'''''([^\s])/, '_*\1')
1082
        text = text.gsub(/([^\s])'''''/, '\1*_')
1083
        text = text.gsub(/'''/, '*')
1084
        text = text.gsub(/''/, '_')
1085
        text = text.gsub(/__/, '+')
1086
        text = text.gsub(/~~/, '-')
1087
        text = text.gsub(/`/, '@')
1088
        text = text.gsub(/,,/, '~')
1089
        # Tables
1090
        text = text.gsub(/\|\|/, '|')
1091
        # Lists:
1092
        #      bullet
1093
        text = text.gsub(/^(\ +)\* /) {|s| '*' * $1.length + " "}
1094
        #      numbered
1095
        text = text.gsub(/^(\ +)\d+\. /) {|s| '#' * $1.length + " "}
1096
        # Images (work for only attached in current page [[Image(picture.gif)]])
1097
        # need rules for:  * [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page)
1098
        #                  * [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
1099
        #                  * [[Image(htdocs:picture.gif)]] (referring to a file inside project htdocs)
1100
        #                  * [[Image(source:/trunk/trac/htdocs/trac_logo_mini.png)]] (a file in repository) 
1101
        text = text.gsub(/\[\[image\((.+?)(?:,.+?)?\)\]\]/i, '!\1!')
1102
        # TOC
1103
        text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"}
1104
        
1105
        text
1106
  end
767 1107
end
768 1108