Project

General

Profile

Patch #5035 » migrate_from_trac_v3.patch

Mike Stupalov, 2010-03-19 14:15

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

  
27
require 'tempfile'
28

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

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

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

  
......
271 278

  
272 279
      # Basic wiki syntax conversion
273 280
      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
281
        convert_wiki_text_mapping(text, TICKET_MAP)
364 282
      end
365 283

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

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

  
379
        # Trac ticket id as a Redmine custom field
380
        tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
381
        tid = IssueCustomField.new(:name => 'TracID',
382
                                 :field_format => 'string',
383
                                 :is_filter => true) if tid.nil?
384
        tid.trackers = Tracker.find(:all)
385
        tid.projects << @target_project
386
        tid.save!
387
        custom_field_map['tracid'] = tid
388
  
459 389
        # Tickets
460 390
        print "Migrating tickets"
461 391
          TracTicket.find_each(:batch_size => 200) do |ticket|
......
463 393
          STDOUT.flush
464 394
          i = Issue.new :project => @target_project,
465 395
                          :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
466
                          :description => convert_wiki_text(encode(ticket.description)),
396
                          :description => encode(ticket.description),
467 397
                          :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
468 398
                          :created_on => ticket.time
469 399
          i.author = find_or_create_user(ticket.reporter)
......
488 418
              resolution_change = changeset.select {|change| change.field == 'resolution'}.first
489 419
              comment_change = changeset.select {|change| change.field == 'comment'}.first
490 420

  
491
              n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
421
              n = Journal.new :notes => (comment_change ? encode(comment_change.newvalue) : ''),
492 422
                              :created_on => time
493 423
              n.user = find_or_create_user(changeset.first.author)
494 424
              n.journalized = i
......
534 464
          if custom_field_map['resolution'] && !ticket.resolution.blank?
535 465
            custom_values[custom_field_map['resolution'].id] = ticket.resolution
536 466
          end
467
          if custom_field_map['tracid'] 
468
            custom_values[custom_field_map['tracid'].id] = ticket.id
469
          end
537 470
          i.custom_field_values = custom_values
538 471
          i.save_custom_field_values
539 472
        end
......
576 509
            end
577 510
          end
578 511

  
512
        end
513
        puts
514

  
515
    # Now load each wiki page and transform its content into textile format
516
    print "Transform texts to textile format:"
517
    puts
518
    
519
    print "   in Wiki pages"
579 520
          wiki.reload
580 521
          wiki.pages.each do |page|
522
            print '.'
581 523
            page.content.text = convert_wiki_text(page.content.text)
582 524
            Time.fake(page.content.updated_on) { page.content.save }
583 525
          end
584
        end
585
        puts
526
    puts
586 527

  
528
    print "   in Issue descriptions"
529
          TICKET_MAP.each do |newId|
530

  
531
            next if newId.nil?
532
            
533
            print '.'
534
            issue = findIssue(newId)
535
            next if issue.nil?
536

  
537
            issue.description = convert_wiki_text(issue.description)
538
      issue.save            
539
          end
540
    puts
541

  
542
    print "   in Issue journal descriptions"
543
          TICKET_MAP.each do |newId|
544
            next if newId.nil?
545
            
546
            print '.'
547
            issue = findIssue(newId)
548
            next if issue.nil?
549
            
550
            issue.journals.find(:all).each do |journal|
551
              print '.'
552
              journal.notes = convert_wiki_text(journal.notes)
553
              journal.save
554
            end
555
  
556
          end
557
    puts
558
    
559
    print "   in Milestone descriptions"
560
          milestone_wiki.each do |name|
561
            p = wiki.find_page(name)            
562
            next if p.nil?
563
                  
564
            print '.'            
565
            p.content.text = convert_wiki_text(p.content.text)
566
            p.content.save
567
    end
568
    puts
569

  
587 570
        puts
588 571
        puts "Components:      #{migrated_components}/#{TracComponent.count}"
589 572
        puts "Milestones:      #{migrated_milestones}/#{TracMilestone.count}"
......
593 576
        puts "Wiki edits:      #{migrated_wiki_edits}/#{wiki_edit_count}"
594 577
        puts "Wiki files:      #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
595 578
      end
579
      
580
      def self.findIssue(id)
581
        
582
        return Issue.find(id)
596 583

  
584
      rescue ActiveRecord::RecordNotFound
585
  puts
586
        print "[#{id}] not found"
587

  
588
        nil      
589
      end
590
      
597 591
      def self.limit_for(klass, attribute)
598 592
        klass.columns_hash[attribute.to_s].limit
599 593
      end
......
746 740
    DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
747 741

  
748 742
    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}
743
    prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
750 744
    unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
751 745
      prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
752 746
      prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
......
764 758
    
765 759
    TracMigrate.migrate
766 760
  end
761

  
762

  
763
  desc 'Subversion migration script'
764
  task :migrate_from_trac_svn => :environment do
765
  
766
    module SvnMigrate 
767
        TICKET_MAP = []
768

  
769
        class Commit
770
          attr_accessor :revision, :message
771
          
772
          def initialize(attributes={})
773
            self.message = attributes[:message] || ""
774
            self.revision = attributes[:revision]
775
          end
776
        end
777
        
778
        class SvnExtendedAdapter < Redmine::Scm::Adapters::SubversionAdapter
779
        
780

  
781

  
782
            def set_message(path=nil, revision=nil, msg=nil)
783
              path ||= ''
784

  
785
              Tempfile.open('msg') do |tempfile|
786

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

  
791
                filePath = tempfile.path.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
792

  
793
                cmd = "#{SVN_BIN} propset svn:log --quiet --revprop -r #{revision}  -F \"#{filePath}\" "
794
                cmd << credentials_string
795
                cmd << ' ' + target(URI.escape(path))
796

  
797
                shellout(cmd) do |io|
798
                  begin
799
                    loop do 
800
                      line = io.readline
801
                      puts line
802
                    end
803
                  rescue EOFError
804
                  end  
805
                end
806

  
807
                raise if $? && $?.exitstatus != 0
808

  
809
              end
810
              
811
            end
812
        
813
            def messages(path=nil)
814
              path ||= ''
815

  
816
              commits = Array.new
817

  
818
              cmd = "#{SVN_BIN} log --xml -r 1:HEAD"
819
              cmd << credentials_string
820
              cmd << ' ' + target(URI.escape(path))
821
                            
822
              shellout(cmd) do |io|
823
                begin
824
                  doc = REXML::Document.new(io)
825
                  doc.elements.each("log/logentry") do |logentry|
826

  
827
                    commits << Commit.new(
828
                                                {
829
                                                  :revision => logentry.attributes['revision'].to_i,
830
                                                  :message => logentry.elements['msg'].text
831
                                                })
832
                  end
833
                rescue => e
834
                  puts"Error !!!"
835
                  puts e
836
                end
837
              end
838
              return nil if $? && $?.exitstatus != 0
839
              commits
840
            end
841
          
842
        end
843
        
844
        def self.migrate
845

  
846
          project = Project.find(@@redmine_project)
847
          if !project
848
            puts "Could not find project identifier '#{@@redmine_project}'"
849
            raise 
850
          end
851
                    
852
          tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
853
          if !tid
854
            puts "Could not find issue custom field 'TracID'"
855
            raise 
856
          end
857
          
858
          Issue.find( :all, :conditions => { :project_id => project }).each do |issue|
859
            val = nil
860
            issue.custom_values.each do |value|
861
              if value.custom_field.id == tid.id
862
                val = value
863
                break
864
              end
865
            end
866
            
867
            TICKET_MAP[val.value.to_i] = issue.id if !val.nil?            
868
          end
869
          
870
          svn = self.scm          
871
          msgs = svn.messages(@svn_url)
872
          msgs.each do |commit| 
873
          
874
            newText = convert_wiki_text(commit.message)
875
            
876
            if newText != commit.message             
877
              puts "Updating message #{commit.revision}"
878
              scm.set_message(@svn_url, commit.revision, newText)
879
            end
880
          end
881
          
882
          
883
        end
884
        
885
        # Basic wiki syntax conversion
886
        def self.convert_wiki_text(text)
887
          convert_wiki_text_mapping(text, TICKET_MAP )
888
        end
889
        
890
        def self.set_svn_url(url)
891
          @@svn_url = url
892
        end
893

  
894
        def self.set_svn_username(username)
895
          @@svn_username = username
896
        end
897

  
898
        def self.set_svn_password(password)
899
          @@svn_password = password
900
        end
901

  
902
        def self.set_redmine_project_identifier(identifier)
903
          @@redmine_project = identifier
904
        end
905
      
906
        def self.scm
907
          @scm ||= SvnExtendedAdapter.new @@svn_url, @@svn_url, @@svn_username, @@svn_password, 0, "", nil
908
          @scm
909
        end
910
    end
911
    
912
    def prompt(text, options = {}, &block)
913
      default = options[:default] || ''
914
      while true
915
        print "#{text} [#{default}]: "
916
        value = STDIN.gets.chomp!
917
        value = default if value.blank?
918
        break if yield value
919
      end
920
    end
921

  
922
    puts
923
    if Redmine::DefaultData::Loader.no_data?
924
      puts "Redmine configuration need to be loaded before importing data."
925
      puts "Please, run this first:"
926
      puts
927
      puts "  rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
928
      exit
929
    end
930

  
931
    puts "WARNING: all commit messages with references to trac pages will be modified"
932
    print "Are you sure you want to continue ? [y/N] "
933
    break unless STDIN.gets.match(/^y$/i)
934
    puts
935

  
936
    prompt('Subversion repository url') {|repository| SvnMigrate.set_svn_url repository.strip}
937
    prompt('Subversion repository username') {|username| SvnMigrate.set_svn_username username}
938
    prompt('Subversion repository password') {|password| SvnMigrate.set_svn_password password}
939
    prompt('Redmine project identifier') {|identifier| SvnMigrate.set_redmine_project_identifier identifier}
940
    puts
941

  
942
    SvnMigrate.migrate
943
    
944
  end
945

  
946

  
947
  # Basic wiki syntax conversion
948
  def convert_wiki_text_mapping(text, ticket_map = [])
949
        # New line
950
        text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
951
        # Titles (only h1. to h6., and remove #...)
952
        text = text.gsub(/(?:^|^\ +)(\={1,6})\ (.+)\ (?:\1)(?:\ *(\ \#.*))?/) {|s| "\nh#{$1.length}. #{$2}#{$3}\n"}
953
        
954
        # External Links:
955
        #      [http://example.com/]
956
        text = text.gsub(/\[((?:https?|s?ftp)\:\S+)\]/, '\1')
957
        #      [http://example.com/ Example],[http://example.com/ "Example"]
958
        text = text.gsub(/\[((?:https?|s?ftp)\:\S+)[\ \t]+([\"']?)(.+?)\2\]/, '"\3":\1')
959
        #      [mailto:some@example.com],[mailto:"some@example.com"]
960
        text = text.gsub(/\[mailto\:([\"']?)(.+?)\1\]/, '\2')
961
        
962
        # Ticket links:
963
        #      [ticket:234 Text],[ticket:234 This is a test],[ticket:234 "This is a test"]
964
        text = text.gsub(/\[ticket\:([^\ ]+)[\ \t]+([\"']?)(.+?)\2\]/, '"\3":/issues/show/\1')
965
        #      ticket:1234
966
        #      #1 - working cause Redmine uses the same syntax.
967
        text = text.gsub(/ticket\:([^\ ]+?)/, '#\1')
968

  
969
        # Source links:
970
        #      [source:/trunk/readme.txt Readme File],[source:"/trunk/readme.txt" Readme File],
971
        #      [source:/trunk/readme.txt],[source:"/trunk/readme.txt"]
972
        #       The text "Readme File" is not converted,
973
        #       cause Redmine's wiki does not support this.
974
        text = text.gsub(/\[source\:([\"']?)([^\"']+?)\1(?:\ +(.+?))?\]/, 'source:"\2"')
975
        #      source:"/trunk/readme.txt"
976
        #      source:/trunk/readme.txt - working cause Redmine uses the same syntax.
977
        text = text.gsub(/source\:([\"'])([^\"']+?)\1/, 'source:"\2"')
978

  
979
        # Milestone links:
980
        #      [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)],
981
        #      [milestone:"0.1.0 Mercury"],milestone:"0.1.0 Mercury"
982
        #       The text "Milestone 0.1.0 (Mercury)" is not converted,
983
        #       cause Redmine's wiki does not support this.
984
        text = text.gsub(/\[milestone\:([\"'])([^\"']+?)\1(?:\ +(.+?))?\]/, 'version:"\2"')
985
        text = text.gsub(/milestone\:([\"'])([^\"']+?)\1/, 'version:"\2"')
986
        #      [milestone:0.1.0],milestone:0.1.0
987
        text = text.gsub(/\[milestone\:([^\ ]+?)\]/, 'version:\1')
988
        text = text.gsub(/milestone\:([^\ ]+?)/, 'version:\1')
989

  
990
        # Internal Links:
991
        #      ["Some Link"]
992
        text = text.gsub(/\[([\"'])(.+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
993
        #      [wiki:"Some Link" "Link description"],[wiki:"Some Link" Link description]
994
        text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1[\ \t]+([\"']?)(.+?)\3\]/) {|s| "[[#{$2.delete(',./?;|:')}|#{$4}]]"}
995
        #      [wiki:"Some Link"]
996
        text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
997
        #      [wiki:SomeLink]
998
        text = text.gsub(/\[wiki\:([^\s\]]+?)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
999
        #      [wiki:SomeLink Link description],[wiki:SomeLink "Link description"]
1000
        text = text.gsub(/\[wiki\:([^\s\]\"']+?)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$3}]]"}
1001

  
1002
        # Links to pages UsingJustWikiCaps (not work for unicode)
1003
        text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
1004
        # Normalize things that were supposed to not be links
1005
        # like !NotALink
1006
        text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
1007

  
1008
        # Revisions links
1009
        text = text.gsub(/\[(\d+)\]/, 'r\1')
1010
        # Ticket number re-writing
1011
        text = text.gsub(/#(\d+)/) do |s|
1012
          if $1.length < 10
1013
#            ticket_map[$1.to_i] ||= $1
1014
            "\##{ticket_map[$1.to_i] || $1}"
1015
          else
1016
            s
1017
          end
1018
        end
1019
        
1020
        # Before convert Code highlighting, need processing inline code
1021
        #      {{{hello world}}}
1022
        text = text.gsub(/\{\{\{(.+?)\}\}\}/, '@\1@')
1023
        
1024
        # We would like to convert the Code highlighting too
1025
        # This will go into the next line.
1026
        shebang_line = false
1027
        # Reguar expression for start of code
1028
        pre_re = /\{\{\{/
1029
        # Code hightlighing...
1030
        shebang_re = /^\#\!([a-z]+)/
1031
        # Regular expression for end of code
1032
        pre_end_re = /\}\}\}/
1033

  
1034
        # Go through the whole text..extract it line by line
1035
        text = text.gsub(/^(.*)$/) do |line|
1036
          m_pre = pre_re.match(line)
1037
          if m_pre
1038
            line = '<pre>'
1039
          else
1040
            m_sl = shebang_re.match(line)
1041
            if m_sl
1042
              shebang_line = true
1043
              line = '<code class="' + m_sl[1] + '">'
1044
            end
1045
            m_pre_end = pre_end_re.match(line)
1046
            if m_pre_end
1047
              line = '</pre>'
1048
              if shebang_line
1049
                line = '</code>' + line
1050
              end
1051
            end
1052
          end
1053
          line
1054
        end
1055

  
1056
        # Highlighting
1057
        text = text.gsub(/'''''([^\s])/, '_*\1')
1058
        text = text.gsub(/([^\s])'''''/, '\1*_')
1059
        text = text.gsub(/'''/, '*')
1060
        text = text.gsub(/''/, '_')
1061
        text = text.gsub(/__/, '+')
1062
        text = text.gsub(/~~/, '-')
1063
        text = text.gsub(/`/, '@')
1064
        text = text.gsub(/,,/, '~')
1065
        # Tables
1066
        text = text.gsub(/\|\|/, '|')
1067
        # Lists:
1068
        #      bullet
1069
        text = text.gsub(/^(\ +)\* /) {|s| '*' * $1.length + " "}
1070
        #      numbered
1071
        text = text.gsub(/^(\ +)\d+\. /) {|s| '#' * $1.length + " "}
1072
        # Images (work for only attached in current page [[Image(picture.gif)]])
1073
        # need rules for:  * [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page)
1074
        #                  * [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
1075
        #                  * [[Image(htdocs:picture.gif)]] (referring to a file inside project htdocs)
1076
        #                  * [[Image(source:/trunk/trac/htdocs/trac_logo_mini.png)]] (a file in repository) 
1077
        text = text.gsub(/\[\[image\((.+?)(?:,.+?)?\)\]\]/i, '!\1!')
1078
        # TOC
1079
        text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"}
1080
        
1081
        text
1082
  end
767 1083
end
768 1084

  
(3-3/7)