Index: lib/tasks/migrate_from_trac.rake =================================================================== --- lib/tasks/migrate_from_trac.rake (revision 3608) +++ lib/tasks/migrate_from_trac.rake (working copy) @@ -19,6 +19,12 @@ require 'iconv' require 'pp' +require 'redmine/scm/adapters/abstract_adapter' +require 'redmine/scm/adapters/subversion_adapter' +require 'rexml/document' +require 'uri' +require 'tempfile' + namespace :redmine do desc 'Trac migration script' task :migrate_from_trac => :environment do @@ -153,8 +159,11 @@ private def trac_fullpath attachment_type = read_attribute(:type) - trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) } - "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}" + trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02X', x[0]) } + trac_dir = id.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02X', x[0]) } + puts + puts filename + " : " + "#{attachment_type}/#{trac_dir}/#{trac_file}" + "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{trac_dir}/#{trac_file}" end end @@ -192,14 +201,14 @@ def time; Time.at(read_attribute(:time)) end end - TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \ + TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup \ + TracBrowser TracCgi TracChangeset TracInstallPlatforms TracMultipleProjects TracModWSGI \ TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \ TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \ TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \ TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \ WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \ CamelCase TitleIndex) - class TracWikiPage < ActiveRecord::Base set_table_name :wiki set_primary_key :name @@ -241,7 +250,7 @@ if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name') name = name_attr.value end - name =~ (/(.*)(\s+\w+)?/) + name =~ (/(.+?)(?:[\ \t]+(.+)?|[\ \t]+|)$/) fn = $1.strip ln = ($2 || '-').strip @@ -271,96 +280,7 @@ # Basic wiki syntax conversion def self.convert_wiki_text(text) - # Titles - text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"} - # External Links - text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"} - # Ticket links: - # [ticket:234 Text],[ticket:234 This is a test] - text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1') - # ticket:1234 - # #1 is working cause Redmine uses the same syntax. - text = text.gsub(/ticket\:([^\ ]+)/, '#\1') - # Milestone links: - # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)] - # The text "Milestone 0.1.0 (Mercury)" is not converted, - # cause Redmine's wiki does not support this. - text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"') - # [milestone:"0.1.0 Mercury"] - text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"') - text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"') - # milestone:0.1.0 - text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1') - text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1') - # Internal Links - text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below - text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} - text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} - text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} - text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} - text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"} - - # Links to pages UsingJustWikiCaps - text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]') - # Normalize things that were supposed to not be links - # like !NotALink - text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2') - # Revisions links - text = text.gsub(/\[(\d+)\]/, 'r\1') - # Ticket number re-writing - text = text.gsub(/#(\d+)/) do |s| - if $1.length < 10 -# TICKET_MAP[$1.to_i] ||= $1 - "\##{TICKET_MAP[$1.to_i] || $1}" - else - s - end - end - # We would like to convert the Code highlighting too - # This will go into the next line. - shebang_line = false - # Reguar expression for start of code - pre_re = /\{\{\{/ - # Code hightlighing... - shebang_re = /^\#\!([a-z]+)/ - # Regular expression for end of code - pre_end_re = /\}\}\}/ - - # Go through the whole text..extract it line by line - text = text.gsub(/^(.*)$/) do |line| - m_pre = pre_re.match(line) - if m_pre - line = '
'
- else
- m_sl = shebang_re.match(line)
- if m_sl
- shebang_line = true
- line = ''
- end
- m_pre_end = pre_end_re.match(line)
- if m_pre_end
- line = '
'
- if shebang_line
- line = '' + line
- end
- end
- end
- line
- end
-
- # Highlighting
- text = text.gsub(/'''''([^\s])/, '_*\1')
- text = text.gsub(/([^\s])'''''/, '\1*_')
- text = text.gsub(/'''/, '*')
- text = text.gsub(/''/, '_')
- text = text.gsub(/__/, '+')
- text = text.gsub(/~~/, '-')
- text = text.gsub(/`/, '@')
- text = text.gsub(/,,/, '~')
- # Lists
- text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
-
- text
+ convert_wiki_text_mapping(text, TICKET_MAP)
end
def self.migrate
@@ -391,6 +311,10 @@
STDOUT.flush
c = IssueCategory.new :project => @target_project,
:name => encode(component.name[0, limit_for(IssueCategory, 'name')])
+ #Owner
+ unless component.owner.blank?
+ c.assigned_to = find_or_create_user(component.owner, true)
+ end
next unless c.save
issues_category_map[component.name] = c
migrated_components += 1
@@ -400,6 +324,7 @@
# Milestones
print "Migrating milestones"
version_map = {}
+ milestone_wiki = Array.new
TracMilestone.find(:all).each do |milestone|
print '.'
STDOUT.flush
@@ -419,6 +344,7 @@
next unless v.save
version_map[milestone.name] = v
+ milestone_wiki.push(milestone.name);
migrated_milestones += 1
end
puts
@@ -456,6 +382,26 @@
r.save!
custom_field_map['resolution'] = r
+ # Trac 'keywords' field as a Redmine custom field
+ k = IssueCustomField.find(:first, :conditions => { :name => "Keywords" })
+ k = IssueCustomField.new(:name => 'Keywords',
+ :field_format => 'string',
+ :is_filter => true) if k.nil?
+ k.trackers = Tracker.find(:all)
+ k.projects << @target_project
+ k.save!
+ custom_field_map['keywords'] = k
+
+ # Trac ticket id as a Redmine custom field
+ tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
+ tid = IssueCustomField.new(:name => 'TracID',
+ :field_format => 'string',
+ :is_filter => true) if tid.nil?
+ tid.trackers = Tracker.find(:all)
+ tid.projects << @target_project
+ tid.save!
+ custom_field_map['tracid'] = tid
+
# Tickets
print "Migrating tickets"
TracTicket.find_each(:batch_size => 200) do |ticket|
@@ -463,7 +409,7 @@
STDOUT.flush
i = Issue.new :project => @target_project,
:subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
- :description => convert_wiki_text(encode(ticket.description)),
+ :description => encode(ticket.description),
:priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
:created_on => ticket.time
i.author = find_or_create_user(ticket.reporter)
@@ -482,13 +428,14 @@
Time.fake(ticket.changetime) { i.save }
end
- # Comments and status/resolution changes
+ # Comments and status/resolution/keywords changes
ticket.changes.group_by(&:time).each do |time, changeset|
status_change = changeset.select {|change| change.field == 'status'}.first
resolution_change = changeset.select {|change| change.field == 'resolution'}.first
+ keywords_change = changeset.select {|change| change.field == 'keywords'}.first
comment_change = changeset.select {|change| change.field == 'comment'}.first
- n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
+ n = Journal.new :notes => (comment_change ? encode(comment_change.newvalue) : ''),
:created_on => time
n.user = find_or_create_user(changeset.first.author)
n.journalized = i
@@ -507,6 +454,12 @@
:old_value => resolution_change.oldvalue,
:value => resolution_change.newvalue)
end
+ if keywords_change
+ n.details << JournalDetail.new(:property => 'cf',
+ :prop_key => custom_field_map['keywords'].id,
+ :old_value => keywords_change.oldvalue,
+ :value => keywords_change.newvalue)
+ end
n.save unless n.details.empty? && n.notes.blank?
end
@@ -534,6 +487,12 @@
if custom_field_map['resolution'] && !ticket.resolution.blank?
custom_values[custom_field_map['resolution'].id] = ticket.resolution
end
+ if custom_field_map['keywords'] && !ticket.keywords.blank?
+ custom_values[custom_field_map['keywords'].id] = ticket.keywords
+ end
+ if custom_field_map['tracid']
+ custom_values[custom_field_map['tracid'].id] = ticket.id
+ end
i.custom_field_values = custom_values
i.save_custom_field_values
end
@@ -576,14 +535,64 @@
end
end
+ end
+ puts
+
+ # Now load each wiki page and transform its content into textile format
+ print "Transform texts to textile format:"
+ puts
+
+ print " in Wiki pages..................."
wiki.reload
wiki.pages.each do |page|
+ #print '.'
page.content.text = convert_wiki_text(page.content.text)
Time.fake(page.content.updated_on) { page.content.save }
end
- end
- puts
+ puts
+ print " in Issue descriptions..........."
+ TICKET_MAP.each do |newId|
+
+ next if newId.nil?
+
+ #print '.'
+ issue = findIssue(newId)
+ next if issue.nil?
+
+ issue.description = convert_wiki_text(issue.description)
+ issue.save
+ end
+ puts
+
+ print " in Issue journal descriptions..."
+ TICKET_MAP.each do |newId|
+ next if newId.nil?
+
+ #print '.'
+ issue = findIssue(newId)
+ next if issue.nil?
+
+ issue.journals.find(:all).each do |journal|
+ #print '.'
+ journal.notes = convert_wiki_text(journal.notes)
+ journal.save
+ end
+
+ end
+ puts
+
+ print " in Milestone descriptions......."
+ milestone_wiki.each do |name|
+ p = wiki.find_page(name)
+ next if p.nil?
+
+ #print '.'
+ p.content.text = convert_wiki_text(p.content.text)
+ p.content.save
+ end
+ puts
+
puts
puts "Components: #{migrated_components}/#{TracComponent.count}"
puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
@@ -593,7 +602,18 @@
puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
end
+
+ def self.findIssue(id)
+
+ return Issue.find(id)
+ rescue ActiveRecord::RecordNotFound
+ puts
+ print "[#{id}] not found"
+
+ nil
+ end
+
def self.limit_for(klass, attribute)
klass.columns_hash[attribute.to_s].limit
end
@@ -746,7 +766,7 @@
DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
- prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
+ prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
@@ -756,7 +776,7 @@
prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
end
prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
- prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
+ prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier.downcase}
puts
# Turn off email notifications
@@ -764,5 +784,332 @@
TracMigrate.migrate
end
+
+
+ desc 'Subversion migration script'
+ task :migrate_from_trac_svn => :environment do
+
+ module SvnMigrate
+ TICKET_MAP = []
+
+ class Commit
+ attr_accessor :revision, :message
+
+ def initialize(attributes={})
+ self.message = attributes[:message] || ""
+ self.revision = attributes[:revision]
+ end
+ end
+
+ class SvnExtendedAdapter < Redmine::Scm::Adapters::SubversionAdapter
+
+
+
+ def set_message(path=nil, revision=nil, msg=nil)
+ path ||= ''
+
+ Tempfile.open('msg') do |tempfile|
+
+ # This is a weird thing. We need to cleanup cr/lf so we have uniform line separators
+ tempfile.print msg.gsub(/\r\n/,'\n')
+ tempfile.flush
+
+ filePath = tempfile.path.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
+
+ cmd = "#{SVN_BIN} propset svn:log --quiet --revprop -r #{revision} -F \"#{filePath}\" "
+ cmd << credentials_string
+ cmd << ' ' + target(URI.escape(path))
+
+ shellout(cmd) do |io|
+ begin
+ loop do
+ line = io.readline
+ puts line
+ end
+ rescue EOFError
+ end
+ end
+
+ raise if $? && $?.exitstatus != 0
+
+ end
+
+ end
+
+ def messages(path=nil)
+ path ||= ''
+
+ commits = Array.new
+
+ cmd = "#{SVN_BIN} log --xml -r 1:HEAD"
+ cmd << credentials_string
+ cmd << ' ' + target(URI.escape(path))
+
+ shellout(cmd) do |io|
+ begin
+ doc = REXML::Document.new(io)
+ doc.elements.each("log/logentry") do |logentry|
+
+ commits << Commit.new(
+ {
+ :revision => logentry.attributes['revision'].to_i,
+ :message => logentry.elements['msg'].text
+ })
+ end
+ rescue => e
+ puts"Error !!!"
+ puts e
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ commits
+ end
+
+ end
+
+ def self.migrate
+
+ project = Project.find(@@redmine_project)
+ if !project
+ puts "Could not find project identifier '#{@@redmine_project}'"
+ raise
+ end
+
+ tid = IssueCustomField.find(:first, :conditions => { :name => "TracID" })
+ if !tid
+ puts "Could not find issue custom field 'TracID'"
+ raise
+ end
+
+ Issue.find( :all, :conditions => { :project_id => project }).each do |issue|
+ val = nil
+ issue.custom_values.each do |value|
+ if value.custom_field.id == tid.id
+ val = value
+ break
+ end
+ end
+
+ TICKET_MAP[val.value.to_i] = issue.id if !val.nil?
+ end
+
+ svn = self.scm
+ msgs = svn.messages(@svn_url)
+ msgs.each do |commit|
+
+ newText = convert_wiki_text(commit.message)
+
+ if newText != commit.message
+ puts "Updating message #{commit.revision}"
+ scm.set_message(@svn_url, commit.revision, newText)
+ end
+ end
+
+
+ end
+
+ # Basic wiki syntax conversion
+ def self.convert_wiki_text(text)
+ convert_wiki_text_mapping(text, TICKET_MAP )
+ end
+
+ def self.set_svn_url(url)
+ @@svn_url = url
+ end
+
+ def self.set_svn_username(username)
+ @@svn_username = username
+ end
+
+ def self.set_svn_password(password)
+ @@svn_password = password
+ end
+
+ def self.set_redmine_project_identifier(identifier)
+ @@redmine_project = identifier
+ end
+
+ def self.scm
+ @scm ||= SvnExtendedAdapter.new @@svn_url, @@svn_url, @@svn_username, @@svn_password, 0, "", nil
+ @scm
+ end
+ end
+
+ def prompt(text, options = {}, &block)
+ default = options[:default] || ''
+ while true
+ print "#{text} [#{default}]: "
+ value = STDIN.gets.chomp!
+ value = default if value.blank?
+ break if yield value
+ end
+ end
+
+ puts
+ if Redmine::DefaultData::Loader.no_data?
+ puts "Redmine configuration need to be loaded before importing data."
+ puts "Please, run this first:"
+ puts
+ puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
+ exit
+ end
+
+ puts "WARNING: all commit messages with references to trac pages will be modified"
+ print "Are you sure you want to continue ? [y/N] "
+ break unless STDIN.gets.match(/^y$/i)
+ puts
+
+ prompt('Subversion repository url') {|repository| SvnMigrate.set_svn_url repository.strip}
+ prompt('Subversion repository username') {|username| SvnMigrate.set_svn_username username}
+ prompt('Subversion repository password') {|password| SvnMigrate.set_svn_password password}
+ prompt('Redmine project identifier') {|identifier| SvnMigrate.set_redmine_project_identifier identifier}
+ puts
+
+ SvnMigrate.migrate
+
+ end
+
+
+ # Basic wiki syntax conversion
+ def convert_wiki_text_mapping(text, ticket_map = [])
+ # New line
+ text = text.gsub(/\[\[[Bb][Rr]\]\]/, "\n") # This has to go before the rules below
+ # Titles (only h1. to h6., and remove #...)
+ text = text.gsub(/(?:^|^\ +)(\={1,6})\ (.+)\ (?:\1)(?:\ *(\ \#.*))?/) {|s| "\nh#{$1.length}. #{$2}#{$3}\n"}
+
+ # External Links:
+ # [http://example.com/]
+ text = text.gsub(/\[((?:https?|s?ftp)\:\S+)\]/, '\1')
+ # [http://example.com/ Example],[http://example.com/ "Example"]
+ # [http://example.com/ "Example for "Example""] -> "Example for 'Example'":http://example.com/
+ text = text.gsub(/\[((?:https?|s?ftp)\:\S+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":#{$1}"}
+ # [mailto:some@example.com],[mailto:"some@example.com"]
+ text = text.gsub(/\[mailto\:([\"']?)(.+?)\1\]/, '\2')
+
+ # Ticket links:
+ # [ticket:234 Text],[ticket:234 This is a test],[ticket:234 "This is a test"]
+ # [ticket:234 "Test "with quotes""] -> "Test 'with quotes'":issues/show/234
+ text = text.gsub(/\[ticket\:(\d+)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "\"#{$3.tr('"','\'')}\":/issues/show/#{$1}"}
+ # ticket:1234
+ # excluding ticket:1234:file.txt (used in macros)
+ # #1 - working cause Redmine uses the same syntax.
+ text = text.gsub(/ticket\:(\d+?)([^\:])/, '#\1\2')
+
+ # Source & attachments links:
+ # [source:/trunk/readme.txt Readme File],[source:"/trunk/readme.txt" Readme File],
+ # [source:/trunk/readme.txt],[source:"/trunk/readme.txt"]
+ # The text "Readme File" is not converted,
+ # cause Redmine's wiki does not support this.
+ # Attachments use same syntax.
+ text = text.gsub(/\[(source|attachment)\:([\"']?)([^\"']+?)\2(?:\ +(.+?))?\]/, '\1:"\3"')
+ # source:"/trunk/readme.txt"
+ # source:/trunk/readme.txt - working cause Redmine uses the same syntax.
+ text = text.gsub(/(source|attachment)\:([\"'])([^\"']+?)\2/, '\1:"\3"')
+
+ # Milestone links:
+ # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)],
+ # [milestone:"0.1.0 Mercury"],milestone:"0.1.0 Mercury"
+ # The text "Milestone 0.1.0 (Mercury)" is not converted,
+ # cause Redmine's wiki does not support this.
+ text = text.gsub(/\[milestone\:([\"'])([^\"']+?)\1(?:\ +(.+?))?\]/, 'version:"\2"')
+ text = text.gsub(/milestone\:([\"'])([^\"']+?)\1/, 'version:"\2"')
+ # [milestone:0.1.0],milestone:0.1.0
+ text = text.gsub(/\[milestone\:([^\ ]+?)\]/, 'version:\1')
+ text = text.gsub(/milestone\:([^\ ]+?)/, 'version:\1')
+
+ # Internal Links:
+ # ["Some Link"]
+ text = text.gsub(/\[([\"'])(.+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
+ # [wiki:"Some Link" "Link description"],[wiki:"Some Link" Link description]
+ text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1[\ \t]+([\"']?)(.+?)\3\]/) {|s| "[[#{$2.delete(',./?;|:')}|#{$4}]]"}
+ # [wiki:"Some Link"]
+ text = text.gsub(/\[wiki\:([\"'])([^\]\"']+?)\1\]/) {|s| "[[#{$2.delete(',./?;|:')}]]"}
+ # [wiki:SomeLink]
+ text = text.gsub(/\[wiki\:([^\s\]]+?)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
+ # [wiki:SomeLink Link description],[wiki:SomeLink "Link description"]
+ text = text.gsub(/\[wiki\:([^\s\]\"']+?)[\ \t]+([\"']?)(.+?)\2\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$3}]]"}
+
+ # Links to CamelCase pages (not work for unicode)
+ # UsingJustWikiCaps,UsingJustWikiCaps/Subpage
+ text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+(?:\/[^\s[:punct:]]+)*)/) {|s| "#{$1}#{$2}[[#{$3.delete('/')}]]"}
+ # Normalize things that were supposed to not be links
+ # like !NotALink
+ text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
+
+ # Revisions links
+ text = text.gsub(/\[(\d+)\]/, 'r\1')
+ # Ticket number re-writing
+ text = text.gsub(/#(\d+)/) do |s|
+ if $1.length < 10
+# ticket_map[$1.to_i] ||= $1
+ "\##{ticket_map[$1.to_i] || $1}"
+ else
+ s
+ end
+ end
+
+ # Before convert Code highlighting, need processing inline code
+ # {{{hello world}}}
+ text = text.gsub(/\{\{\{(.+?)\}\}\}/, '@\1@')
+
+ # We would like to convert the Code highlighting too
+ # This will go into the next line.
+ shebang_line = false
+ # Reguar expression for start of code
+ pre_re = /\{\{\{/
+ # Code hightlighing...
+ shebang_re = /^\#\!([a-z]+)/
+ # Regular expression for end of code
+ pre_end_re = /\}\}\}/
+
+ # Go through the whole text..extract it line by line
+ text = text.gsub(/^(.*)$/) do |line|
+ m_pre = pre_re.match(line)
+ if m_pre
+ line = ''
+ else
+ m_sl = shebang_re.match(line)
+ if m_sl
+ shebang_line = true
+ line = ''
+ end
+ m_pre_end = pre_end_re.match(line)
+ if m_pre_end
+ line = '
'
+ if shebang_line
+ line = '' + line
+ end
+ end
+ end
+ line
+ end
+
+ # Highlighting
+ text = text.gsub(/'''''([^\s])/, '_*\1')
+ text = text.gsub(/([^\s])'''''/, '\1*_')
+ text = text.gsub(/'''/, '*')
+ text = text.gsub(/''/, '_')
+ text = text.gsub(/__/, '+')
+ text = text.gsub(/~~/, '-')
+ text = text.gsub(/`/, '@')
+ text = text.gsub(/,,/, '~')
+ # Tables
+ text = text.gsub(/\|\|/, '|')
+ # Lists:
+ # bullet
+ text = text.gsub(/^(\ +)\* /) {|s| '*' * $1.length + " "}
+ # numbered
+ text = text.gsub(/^(\ +)\d+\. /) {|s| '#' * $1.length + " "}
+ # Images (work for only attached in current page [[Image(picture.gif)]])
+ # need rules for: * [[Image(wiki:WikiFormatting:picture.gif)]] (referring to attachment on another page)
+ # * [[Image(ticket:1:picture.gif)]] (file attached to a ticket)
+ # * [[Image(htdocs:picture.gif)]] (referring to a file inside project htdocs)
+ # * [[Image(source:/trunk/trac/htdocs/trac_logo_mini.png)]] (a file in repository)
+ text = text.gsub(/\[\[image\((.+?)(?:,.+?)?\)\]\]/i, '!\1!')
+ # TOC
+ text = text.gsub(/\[\[TOC(?:\((.*?)\))?\]\]/m) {|s| "{{>toc}}\n"}
+
+ text
+ end
end