##// END OF EJS Templates
Merged r1505, r1506, r1511, r1512, r1517, r1518, r1524 to 1526 from trunk....
Jean-Philippe Lang -
r1518:2af910e8e61f
parent child
Show More
@@ -1,66 +1,66
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Journal < ActiveRecord::Base
18 class Journal < ActiveRecord::Base
19 belongs_to :journalized, :polymorphic => true
19 belongs_to :journalized, :polymorphic => true
20 # added as a quick fix to allow eager loading of the polymorphic association
20 # added as a quick fix to allow eager loading of the polymorphic association
21 # since always associated to an issue, for now
21 # since always associated to an issue, for now
22 belongs_to :issue, :foreign_key => :journalized_id
22 belongs_to :issue, :foreign_key => :journalized_id
23
23
24 belongs_to :user
24 belongs_to :user
25 has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
25 has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
26 attr_accessor :indice
26 attr_accessor :indice
27
27
28 acts_as_searchable :columns => 'notes',
28 acts_as_searchable :columns => 'notes',
29 :include => :issue,
29 :include => :issue,
30 :project_key => "#{Issue.table_name}.project_id",
30 :project_key => "#{Issue.table_name}.project_id",
31 :date_column => "#{Issue.table_name}.created_on"
31 :date_column => "#{Issue.table_name}.created_on"
32
32
33 acts_as_event :title => Proc.new {|o| "#{o.issue.tracker.name} ##{o.issue.id}: #{o.issue.subject}" + ((s = o.new_status) ? " (#{s})" : '') },
33 acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
34 :description => :notes,
34 :description => :notes,
35 :author => :user,
35 :author => :user,
36 :type => Proc.new {|o| (s = o.new_status) && s.is_closed? ? 'issue-closed' : 'issue-edit' },
36 :type => Proc.new {|o| (s = o.new_status) && s.is_closed? ? 'issue-closed' : 'issue-edit' },
37 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
37 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
38
38
39 def save
39 def save
40 # Do not save an empty journal
40 # Do not save an empty journal
41 (details.empty? && notes.blank?) ? false : super
41 (details.empty? && notes.blank?) ? false : super
42 end
42 end
43
43
44 # Returns the new status if the journal contains a status change, otherwise nil
44 # Returns the new status if the journal contains a status change, otherwise nil
45 def new_status
45 def new_status
46 c = details.detect {|detail| detail.prop_key == 'status_id'}
46 c = details.detect {|detail| detail.prop_key == 'status_id'}
47 (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
47 (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
48 end
48 end
49
49
50 def new_value_for(prop)
50 def new_value_for(prop)
51 c = details.detect {|detail| detail.prop_key == prop}
51 c = details.detect {|detail| detail.prop_key == prop}
52 c ? c.value : nil
52 c ? c.value : nil
53 end
53 end
54
54
55 def editable_by?(usr)
55 def editable_by?(usr)
56 usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
56 usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
57 end
57 end
58
58
59 def project
59 def project
60 journalized.respond_to?(:project) ? journalized.project : nil
60 journalized.respond_to?(:project) ? journalized.project : nil
61 end
61 end
62
62
63 def attachments
63 def attachments
64 journalized.respond_to?(:attachments) ? journalized.attachments : nil
64 journalized.respond_to?(:attachments) ? journalized.attachments : nil
65 end
65 end
66 end
66 end
@@ -1,155 +1,156
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/cvs_adapter'
18 require 'redmine/scm/adapters/cvs_adapter'
19 require 'digest/sha1'
19 require 'digest/sha1'
20
20
21 class Repository::Cvs < Repository
21 class Repository::Cvs < Repository
22 validates_presence_of :url, :root_url
22 validates_presence_of :url, :root_url
23
23
24 def scm_adapter
24 def scm_adapter
25 Redmine::Scm::Adapters::CvsAdapter
25 Redmine::Scm::Adapters::CvsAdapter
26 end
26 end
27
27
28 def self.scm_name
28 def self.scm_name
29 'CVS'
29 'CVS'
30 end
30 end
31
31
32 def entry(path, identifier)
32 def entry(path, identifier)
33 e = entries(path, identifier)
33 e = entries(path, identifier)
34 e ? e.first : nil
34 e ? e.first : nil
35 end
35 end
36
36
37 def entries(path=nil, identifier=nil)
37 def entries(path=nil, identifier=nil)
38 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
38 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
39 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
39 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
40 if entries
40 if entries
41 entries.each() do |entry|
41 entries.each() do |entry|
42 unless entry.lastrev.nil? || entry.lastrev.identifier
42 unless entry.lastrev.nil? || entry.lastrev.identifier
43 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
43 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
44 if change
44 if change
45 entry.lastrev.identifier=change.changeset.revision
45 entry.lastrev.identifier=change.changeset.revision
46 entry.lastrev.author=change.changeset.committer
46 entry.lastrev.author=change.changeset.committer
47 entry.lastrev.revision=change.revision
47 entry.lastrev.revision=change.revision
48 entry.lastrev.branch=change.branch
48 entry.lastrev.branch=change.branch
49 end
49 end
50 end
50 end
51 end
51 end
52 end
52 end
53 entries
53 entries
54 end
54 end
55
55
56 def diff(path, rev, rev_to, type)
56 def diff(path, rev, rev_to, type)
57 #convert rev to revision. CVS can't handle changesets here
57 #convert rev to revision. CVS can't handle changesets here
58 diff=[]
58 diff=[]
59 changeset_from=changesets.find_by_revision(rev)
59 changeset_from=changesets.find_by_revision(rev)
60 if rev_to.to_i > 0
60 if rev_to.to_i > 0
61 changeset_to=changesets.find_by_revision(rev_to)
61 changeset_to=changesets.find_by_revision(rev_to)
62 end
62 end
63 changeset_from.changes.each() do |change_from|
63 changeset_from.changes.each() do |change_from|
64
64
65 revision_from=nil
65 revision_from=nil
66 revision_to=nil
66 revision_to=nil
67
67
68 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
68 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
69
69
70 if revision_from
70 if revision_from
71 if changeset_to
71 if changeset_to
72 changeset_to.changes.each() do |change_to|
72 changeset_to.changes.each() do |change_to|
73 revision_to=change_to.revision if change_to.path==change_from.path
73 revision_to=change_to.revision if change_to.path==change_from.path
74 end
74 end
75 end
75 end
76 unless revision_to
76 unless revision_to
77 revision_to=scm.get_previous_revision(revision_from)
77 revision_to=scm.get_previous_revision(revision_from)
78 end
78 end
79 diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
79 file_diff = scm.diff(change_from.path, revision_from, revision_to)
80 diff = diff + file_diff unless file_diff.nil?
80 end
81 end
81 end
82 end
82 return diff
83 return diff
83 end
84 end
84
85
85 def fetch_changesets
86 def fetch_changesets
86 # some nifty bits to introduce a commit-id with cvs
87 # some nifty bits to introduce a commit-id with cvs
87 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
88 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
88 # we now take a guess using the author, the commitlog and the commit-date.
89 # we now take a guess using the author, the commitlog and the commit-date.
89
90
90 # last one is the next step to take. the commit-date is not equal for all
91 # last one is the next step to take. the commit-date is not equal for all
91 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
92 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
92 # we use a small delta here, to merge all changes belonging to _one_ changeset
93 # we use a small delta here, to merge all changes belonging to _one_ changeset
93 time_delta=10.seconds
94 time_delta=10.seconds
94
95
95 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
96 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
96 transaction do
97 transaction do
97 tmp_rev_num = 1
98 tmp_rev_num = 1
98 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
99 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
99 # only add the change to the database, if it doen't exists. the cvs log
100 # only add the change to the database, if it doen't exists. the cvs log
100 # is not exclusive at all.
101 # is not exclusive at all.
101 unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
102 unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
102 revision
103 revision
103 cs = changesets.find(:first, :conditions=>{
104 cs = changesets.find(:first, :conditions=>{
104 :committed_on=>revision.time-time_delta..revision.time+time_delta,
105 :committed_on=>revision.time-time_delta..revision.time+time_delta,
105 :committer=>revision.author,
106 :committer=>revision.author,
106 :comments=>revision.message
107 :comments=>revision.message
107 })
108 })
108
109
109 # create a new changeset....
110 # create a new changeset....
110 unless cs
111 unless cs
111 # we use a temporaray revision number here (just for inserting)
112 # we use a temporaray revision number here (just for inserting)
112 # later on, we calculate a continous positive number
113 # later on, we calculate a continous positive number
113 latest = changesets.find(:first, :order => 'id DESC')
114 latest = changesets.find(:first, :order => 'id DESC')
114 cs = Changeset.create(:repository => self,
115 cs = Changeset.create(:repository => self,
115 :revision => "_#{tmp_rev_num}",
116 :revision => "_#{tmp_rev_num}",
116 :committer => revision.author,
117 :committer => revision.author,
117 :committed_on => revision.time,
118 :committed_on => revision.time,
118 :comments => revision.message)
119 :comments => revision.message)
119 tmp_rev_num += 1
120 tmp_rev_num += 1
120 end
121 end
121
122
122 #convert CVS-File-States to internal Action-abbrevations
123 #convert CVS-File-States to internal Action-abbrevations
123 #default action is (M)odified
124 #default action is (M)odified
124 action="M"
125 action="M"
125 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
126 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
126 action="A" #add-action always at first revision (= 1.1)
127 action="A" #add-action always at first revision (= 1.1)
127 elsif revision.paths[0][:action]=="dead"
128 elsif revision.paths[0][:action]=="dead"
128 action="D" #dead-state is similar to Delete
129 action="D" #dead-state is similar to Delete
129 end
130 end
130
131
131 Change.create(:changeset => cs,
132 Change.create(:changeset => cs,
132 :action => action,
133 :action => action,
133 :path => scm.with_leading_slash(revision.paths[0][:path]),
134 :path => scm.with_leading_slash(revision.paths[0][:path]),
134 :revision => revision.paths[0][:revision],
135 :revision => revision.paths[0][:revision],
135 :branch => revision.paths[0][:branch]
136 :branch => revision.paths[0][:branch]
136 )
137 )
137 end
138 end
138 end
139 end
139
140
140 # Renumber new changesets in chronological order
141 # Renumber new changesets in chronological order
141 changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
142 changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
142 changeset.update_attribute :revision, next_revision_number
143 changeset.update_attribute :revision, next_revision_number
143 end
144 end
144 end # transaction
145 end # transaction
145 end
146 end
146
147
147 private
148 private
148
149
149 # Returns the next revision number to assign to a CVS changeset
150 # Returns the next revision number to assign to a CVS changeset
150 def next_revision_number
151 def next_revision_number
151 # Need to retrieve existing revision numbers to sort them as integers
152 # Need to retrieve existing revision numbers to sort them as integers
152 @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
153 @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
153 @current_revision_number += 1
154 @current_revision_number += 1
154 end
155 end
155 end
156 end
@@ -1,65 +1,65
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 <head>
3 <head>
4 <title><%=h html_title %></title>
4 <title><%=h html_title %></title>
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 <meta name="keywords" content="issue,bug,tracker" />
7 <meta name="keywords" content="issue,bug,tracker" />
8 <%= stylesheet_link_tag 'application', :media => 'all' %>
8 <%= stylesheet_link_tag 'application', :media => 'all' %>
9 <%= javascript_include_tag :defaults %>
9 <%= javascript_include_tag :defaults %>
10 <%= stylesheet_link_tag 'jstoolbar' %>
10 <%= stylesheet_link_tag 'jstoolbar' %>
11 <!--[if IE]>
11 <!--[if IE]>
12 <style type="text/css">
12 <style type="text/css">
13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
15 </style>
15 </style>
16 <![endif]-->
16 <![endif]-->
17
17
18 <!-- page specific tags --><%= yield :header_tags %>
18 <!-- page specific tags --><%= yield :header_tags %>
19 </head>
19 </head>
20 <body>
20 <body>
21 <div id="wrapper">
21 <div id="wrapper">
22 <div id="top-menu">
22 <div id="top-menu">
23 <div id="account">
23 <div id="account">
24 <%= render_menu :account_menu -%>
24 <%= render_menu :account_menu -%>
25 </div>
25 </div>
26 <%= content_tag('div', "#{l(:label_logged_as)} #{User.current.login}", :id => 'loggedas') if User.current.logged? %>
26 <%= content_tag('div', "#{l(:label_logged_as)} #{User.current.login}", :id => 'loggedas') if User.current.logged? %>
27 <%= render_menu :top_menu -%>
27 <%= render_menu :top_menu -%>
28 </div>
28 </div>
29
29
30 <div id="header">
30 <div id="header">
31 <div id="quick-search">
31 <div id="quick-search">
32 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
32 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
33 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
33 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
34 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
34 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
35 <% end %>
35 <% end %>
36 <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %>
36 <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %>
37 </div>
37 </div>
38
38
39 <h1><%= h(@project ? @project.name : Setting.app_title) %></h1>
39 <h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1>
40
40
41 <div id="main-menu">
41 <div id="main-menu">
42 <%= render_main_menu(@project) %>
42 <%= render_main_menu(@project) %>
43 </div>
43 </div>
44 </div>
44 </div>
45
45
46 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
46 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
47 <div id="sidebar">
47 <div id="sidebar">
48 <%= yield :sidebar %>
48 <%= yield :sidebar %>
49 </div>
49 </div>
50
50
51 <div id="content">
51 <div id="content">
52 <%= content_tag('div', flash[:error], :class => 'flash error') if flash[:error] %>
52 <%= content_tag('div', flash[:error], :class => 'flash error') if flash[:error] %>
53 <%= content_tag('div', flash[:notice], :class => 'flash notice') if flash[:notice] %>
53 <%= content_tag('div', flash[:notice], :class => 'flash notice') if flash[:notice] %>
54 <%= yield %>
54 <%= yield %>
55 </div>
55 </div>
56 </div>
56 </div>
57
57
58 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
58 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
59
59
60 <div id="footer">
60 <div id="footer">
61 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2008 Jean-Philippe Lang
61 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2008 Jean-Philippe Lang
62 </div>
62 </div>
63 </div>
63 </div>
64 </body>
64 </body>
65 </html>
65 </html>
@@ -1,32 +1,32
1 <% @entries.each do |entry| %>
1 <% @entries.each do |entry| %>
2 <% tr_id = Digest::MD5.hexdigest(entry.path)
2 <% tr_id = Digest::MD5.hexdigest(entry.path)
3 depth = params[:depth].to_i %>
3 depth = params[:depth].to_i %>
4 <tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry">
4 <tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry">
5 <td class="filename">
5 <td class="filename">
6 <%= if entry.is_dir?
6 <%= if entry.is_dir?
7 link_to_remote h(entry.name),
7 link_to_remote h(entry.name),
8 {:url => {:action => 'browse', :id => @project, :path => entry.path, :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
8 {:url => {:action => 'browse', :id => @project, :path => entry.path, :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
9 :update => { :success => tr_id },
9 :update => { :success => tr_id },
10 :position => :after,
10 :position => :after,
11 :success => "scmEntryLoaded('#{tr_id}')",
11 :success => "scmEntryLoaded('#{tr_id}')",
12 :condition => "scmEntryClick('#{tr_id}')"
12 :condition => "scmEntryClick('#{tr_id}')"
13 },
13 },
14 {:href => url_for({:action => 'browse', :id => @project, :path => entry.path, :rev => @rev}),
14 {:href => url_for({:action => 'browse', :id => @project, :path => entry.path, :rev => @rev}),
15 :class => ('icon icon-folder'),
15 :class => ('icon icon-folder'),
16 :style => "margin-left: #{18 * depth}px;"
16 :style => "margin-left: #{18 * depth}px;"
17 }
17 }
18 else
18 else
19 link_to h(entry.name),
19 link_to h(entry.name),
20 {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => entry.path, :rev => @rev},
20 {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => entry.path, :rev => @rev},
21 :class => 'icon icon-file',
21 :class => 'icon icon-file',
22 :style => "margin-left: #{18 * depth}px;"
22 :style => "margin-left: #{18 * depth}px;"
23 end %>
23 end %>
24 </td>
24 </td>
25 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
25 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
26 <td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
26 <td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
27 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
27 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
28 <td class="author"><%=h(entry.lastrev.author.to_s.split('<').first) if entry.lastrev %></td>
28 <td class="author"><%=h(entry.lastrev.author.to_s.split('<').first) if entry.lastrev %></td>
29 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %>
29 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
30 <td class="comments"><%=h truncate(changeset.comments, 50) unless changeset.nil? %></td>
30 <td class="comments"><%=h truncate(changeset.comments, 50) unless changeset.nil? %></td>
31 </tr>
31 </tr>
32 <% end %>
32 <% end %>
@@ -1,18 +1,19
1 <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2>
1 <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2>
2
2
3 <h3><%=h @entry.name %></h3>
3 <h3><%=h @entry.name %></h3>
4
4
5 <p>
5 <p>
6 <% if @repository.supports_cat? %>
6 <% if @repository.supports_cat? %>
7 <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
7 <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
8 <% end %>
8 <% end %>
9 <% if @repository.supports_annotate? %>
9 <% if @repository.supports_annotate? %>
10 <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => @path, :rev => @rev } %> |
10 <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => @path, :rev => @rev } %> |
11 <% end %>
11 <% end %>
12 <%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %>
12 <%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %>
13 <%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
13 <%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
14 </p>
14 </p>
15
15
16 <%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }%>
16 <%= render(:partial => 'revisions',
17 :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }) unless @changesets.empty? %>
17
18
18 <% html_title(l(:label_change_plural)) -%>
19 <% html_title(l(:label_change_plural)) -%>
@@ -1,977 +1,977
1 begin
1 begin
2 require 'zlib'
2 require 'zlib'
3 @@__have_zlib = true
3 @@__have_zlib = true
4 rescue
4 rescue
5 @@__have_zlib = false
5 @@__have_zlib = false
6 end
6 end
7
7
8 require 'rexml/document'
8 require 'rexml/document'
9
9
10 module SVG
10 module SVG
11 module Graph
11 module Graph
12 VERSION = '@ANT_VERSION@'
12 VERSION = '@ANT_VERSION@'
13
13
14 # === Base object for generating SVG Graphs
14 # === Base object for generating SVG Graphs
15 #
15 #
16 # == Synopsis
16 # == Synopsis
17 #
17 #
18 # This class is only used as a superclass of specialized charts. Do not
18 # This class is only used as a superclass of specialized charts. Do not
19 # attempt to use this class directly, unless creating a new chart type.
19 # attempt to use this class directly, unless creating a new chart type.
20 #
20 #
21 # For examples of how to subclass this class, see the existing specific
21 # For examples of how to subclass this class, see the existing specific
22 # subclasses, such as SVG::Graph::Pie.
22 # subclasses, such as SVG::Graph::Pie.
23 #
23 #
24 # == Examples
24 # == Examples
25 #
25 #
26 # For examples of how to use this package, see either the test files, or
26 # For examples of how to use this package, see either the test files, or
27 # the documentation for the specific class you want to use.
27 # the documentation for the specific class you want to use.
28 #
28 #
29 # * file:test/plot.rb
29 # * file:test/plot.rb
30 # * file:test/single.rb
30 # * file:test/single.rb
31 # * file:test/test.rb
31 # * file:test/test.rb
32 # * file:test/timeseries.rb
32 # * file:test/timeseries.rb
33 #
33 #
34 # == Description
34 # == Description
35 #
35 #
36 # This package should be used as a base for creating SVG graphs.
36 # This package should be used as a base for creating SVG graphs.
37 #
37 #
38 # == Acknowledgements
38 # == Acknowledgements
39 #
39 #
40 # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
40 # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
41 # port is based on.
41 # port is based on.
42 #
42 #
43 # Stephen Morgan for creating the TT template and SVG.
43 # Stephen Morgan for creating the TT template and SVG.
44 #
44 #
45 # == See
45 # == See
46 #
46 #
47 # * SVG::Graph::BarHorizontal
47 # * SVG::Graph::BarHorizontal
48 # * SVG::Graph::Bar
48 # * SVG::Graph::Bar
49 # * SVG::Graph::Line
49 # * SVG::Graph::Line
50 # * SVG::Graph::Pie
50 # * SVG::Graph::Pie
51 # * SVG::Graph::Plot
51 # * SVG::Graph::Plot
52 # * SVG::Graph::TimeSeries
52 # * SVG::Graph::TimeSeries
53 #
53 #
54 # == Author
54 # == Author
55 #
55 #
56 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
56 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
57 #
57 #
58 # Copyright 2004 Sean E. Russell
58 # Copyright 2004 Sean E. Russell
59 # This software is available under the Ruby license[LICENSE.txt]
59 # This software is available under the Ruby license[LICENSE.txt]
60 #
60 #
61 class Graph
61 class Graph
62 include REXML
62 include REXML
63
63
64 # Initialize the graph object with the graph settings. You won't
64 # Initialize the graph object with the graph settings. You won't
65 # instantiate this class directly; see the subclass for options.
65 # instantiate this class directly; see the subclass for options.
66 # [width] 500
66 # [width] 500
67 # [height] 300
67 # [height] 300
68 # [show_x_guidelines] false
68 # [show_x_guidelines] false
69 # [show_y_guidelines] true
69 # [show_y_guidelines] true
70 # [show_data_values] true
70 # [show_data_values] true
71 # [min_scale_value] 0
71 # [min_scale_value] 0
72 # [show_x_labels] true
72 # [show_x_labels] true
73 # [stagger_x_labels] false
73 # [stagger_x_labels] false
74 # [rotate_x_labels] false
74 # [rotate_x_labels] false
75 # [step_x_labels] 1
75 # [step_x_labels] 1
76 # [step_include_first_x_label] true
76 # [step_include_first_x_label] true
77 # [show_y_labels] true
77 # [show_y_labels] true
78 # [rotate_y_labels] false
78 # [rotate_y_labels] false
79 # [scale_integers] false
79 # [scale_integers] false
80 # [show_x_title] false
80 # [show_x_title] false
81 # [x_title] 'X Field names'
81 # [x_title] 'X Field names'
82 # [show_y_title] false
82 # [show_y_title] false
83 # [y_title_text_direction] :bt
83 # [y_title_text_direction] :bt
84 # [y_title] 'Y Scale'
84 # [y_title] 'Y Scale'
85 # [show_graph_title] false
85 # [show_graph_title] false
86 # [graph_title] 'Graph Title'
86 # [graph_title] 'Graph Title'
87 # [show_graph_subtitle] false
87 # [show_graph_subtitle] false
88 # [graph_subtitle] 'Graph Sub Title'
88 # [graph_subtitle] 'Graph Sub Title'
89 # [key] true,
89 # [key] true,
90 # [key_position] :right, # bottom or righ
90 # [key_position] :right, # bottom or righ
91 # [font_size] 12
91 # [font_size] 12
92 # [title_font_size] 16
92 # [title_font_size] 16
93 # [subtitle_font_size] 14
93 # [subtitle_font_size] 14
94 # [x_label_font_size] 12
94 # [x_label_font_size] 12
95 # [x_title_font_size] 14
95 # [x_title_font_size] 14
96 # [y_label_font_size] 12
96 # [y_label_font_size] 12
97 # [y_title_font_size] 14
97 # [y_title_font_size] 14
98 # [key_font_size] 10
98 # [key_font_size] 10
99 # [no_css] false
99 # [no_css] false
100 # [add_popups] false
100 # [add_popups] false
101 def initialize( config )
101 def initialize( config )
102 @config = config
102 @config = config
103
103
104 self.top_align = self.top_font = self.right_align = self.right_font = 0
104 self.top_align = self.top_font = self.right_align = self.right_font = 0
105
105
106 init_with({
106 init_with({
107 :width => 500,
107 :width => 500,
108 :height => 300,
108 :height => 300,
109 :show_x_guidelines => false,
109 :show_x_guidelines => false,
110 :show_y_guidelines => true,
110 :show_y_guidelines => true,
111 :show_data_values => true,
111 :show_data_values => true,
112
112
113 :min_scale_value => 0,
113 :min_scale_value => 0,
114
114
115 :show_x_labels => true,
115 :show_x_labels => true,
116 :stagger_x_labels => false,
116 :stagger_x_labels => false,
117 :rotate_x_labels => false,
117 :rotate_x_labels => false,
118 :step_x_labels => 1,
118 :step_x_labels => 1,
119 :step_include_first_x_label => true,
119 :step_include_first_x_label => true,
120
120
121 :show_y_labels => true,
121 :show_y_labels => true,
122 :rotate_y_labels => false,
122 :rotate_y_labels => false,
123 :stagger_y_labels => false,
123 :stagger_y_labels => false,
124 :scale_integers => false,
124 :scale_integers => false,
125
125
126 :show_x_title => false,
126 :show_x_title => false,
127 :x_title => 'X Field names',
127 :x_title => 'X Field names',
128
128
129 :show_y_title => false,
129 :show_y_title => false,
130 :y_title_text_direction => :bt,
130 :y_title_text_direction => :bt,
131 :y_title => 'Y Scale',
131 :y_title => 'Y Scale',
132
132
133 :show_graph_title => false,
133 :show_graph_title => false,
134 :graph_title => 'Graph Title',
134 :graph_title => 'Graph Title',
135 :show_graph_subtitle => false,
135 :show_graph_subtitle => false,
136 :graph_subtitle => 'Graph Sub Title',
136 :graph_subtitle => 'Graph Sub Title',
137 :key => true,
137 :key => true,
138 :key_position => :right, # bottom or right
138 :key_position => :right, # bottom or right
139
139
140 :font_size =>10,
140 :font_size =>10,
141 :title_font_size =>12,
141 :title_font_size =>12,
142 :subtitle_font_size =>14,
142 :subtitle_font_size =>14,
143 :x_label_font_size =>11,
143 :x_label_font_size =>11,
144 :x_title_font_size =>14,
144 :x_title_font_size =>14,
145 :y_label_font_size =>11,
145 :y_label_font_size =>11,
146 :y_title_font_size =>14,
146 :y_title_font_size =>14,
147 :key_font_size => 9,
147 :key_font_size => 9,
148
148
149 :no_css =>false,
149 :no_css =>false,
150 :add_popups =>false,
150 :add_popups =>false,
151 })
151 })
152
152
153 set_defaults if methods.include? "set_defaults"
153 set_defaults if methods.include? "set_defaults"
154
154
155 init_with config
155 init_with config
156 end
156 end
157
157
158
158
159 # This method allows you do add data to the graph object.
159 # This method allows you do add data to the graph object.
160 # It can be called several times to add more data sets in.
160 # It can be called several times to add more data sets in.
161 #
161 #
162 # data_sales_02 = [12, 45, 21];
162 # data_sales_02 = [12, 45, 21];
163 #
163 #
164 # graph.add_data({
164 # graph.add_data({
165 # :data => data_sales_02,
165 # :data => data_sales_02,
166 # :title => 'Sales 2002'
166 # :title => 'Sales 2002'
167 # })
167 # })
168 def add_data conf
168 def add_data conf
169 @data = [] unless defined? @data
169 @data = [] unless defined? @data
170
170
171 if conf[:data] and conf[:data].kind_of? Array
171 if conf[:data] and conf[:data].kind_of? Array
172 @data << conf
172 @data << conf
173 else
173 else
174 raise "No data provided by #{conf.inspect}"
174 raise "No data provided by #{conf.inspect}"
175 end
175 end
176 end
176 end
177
177
178
178
179 # This method removes all data from the object so that you can
179 # This method removes all data from the object so that you can
180 # reuse it to create a new graph but with the same config options.
180 # reuse it to create a new graph but with the same config options.
181 #
181 #
182 # graph.clear_data
182 # graph.clear_data
183 def clear_data
183 def clear_data
184 @data = []
184 @data = []
185 end
185 end
186
186
187
187
188 # This method processes the template with the data and
188 # This method processes the template with the data and
189 # config which has been set and returns the resulting SVG.
189 # config which has been set and returns the resulting SVG.
190 #
190 #
191 # This method will croak unless at least one data set has
191 # This method will croak unless at least one data set has
192 # been added to the graph object.
192 # been added to the graph object.
193 #
193 #
194 # print graph.burn
194 # print graph.burn
195 def burn
195 def burn
196 raise "No data available" unless @data.size > 0
196 raise "No data available" unless @data.size > 0
197
197
198 calculations if methods.include? 'calculations'
198 calculations if methods.include? 'calculations'
199
199
200 start_svg
200 start_svg
201 calculate_graph_dimensions
201 calculate_graph_dimensions
202 @foreground = Element.new( "g" )
202 @foreground = Element.new( "g" )
203 draw_graph
203 draw_graph
204 draw_titles
204 draw_titles
205 draw_legend
205 draw_legend
206 draw_data
206 draw_data
207 @graph.add_element( @foreground )
207 @graph.add_element( @foreground )
208 style
208 style
209
209
210 data = ""
210 data = ""
211 @doc.write( data, 0 )
211 @doc.write( data, 0 )
212
212
213 if @config[:compress]
213 if @config[:compress]
214 if @@__have_zlib
214 if @@__have_zlib
215 inp, out = IO.pipe
215 inp, out = IO.pipe
216 gz = Zlib::GzipWriter.new( out )
216 gz = Zlib::GzipWriter.new( out )
217 gz.write data
217 gz.write data
218 gz.close
218 gz.close
219 data = inp.read
219 data = inp.read
220 else
220 else
221 data << "<!-- Ruby Zlib not available for SVGZ -->";
221 data << "<!-- Ruby Zlib not available for SVGZ -->";
222 end
222 end
223 end
223 end
224
224
225 return data
225 return data
226 end
226 end
227
227
228
228
229 # Set the height of the graph box, this is the total height
229 # Set the height of the graph box, this is the total height
230 # of the SVG box created - not the graph it self which auto
230 # of the SVG box created - not the graph it self which auto
231 # scales to fix the space.
231 # scales to fix the space.
232 attr_accessor :height
232 attr_accessor :height
233 # Set the width of the graph box, this is the total width
233 # Set the width of the graph box, this is the total width
234 # of the SVG box created - not the graph it self which auto
234 # of the SVG box created - not the graph it self which auto
235 # scales to fix the space.
235 # scales to fix the space.
236 attr_accessor :width
236 attr_accessor :width
237 # Set the path to an external stylesheet, set to '' if
237 # Set the path to an external stylesheet, set to '' if
238 # you want to revert back to using the defaut internal version.
238 # you want to revert back to using the defaut internal version.
239 #
239 #
240 # To create an external stylesheet create a graph using the
240 # To create an external stylesheet create a graph using the
241 # default internal version and copy the stylesheet section to
241 # default internal version and copy the stylesheet section to
242 # an external file and edit from there.
242 # an external file and edit from there.
243 attr_accessor :style_sheet
243 attr_accessor :style_sheet
244 # (Bool) Show the value of each element of data on the graph
244 # (Bool) Show the value of each element of data on the graph
245 attr_accessor :show_data_values
245 attr_accessor :show_data_values
246 # The point at which the Y axis starts, defaults to '0',
246 # The point at which the Y axis starts, defaults to '0',
247 # if set to nil it will default to the minimum data value.
247 # if set to nil it will default to the minimum data value.
248 attr_accessor :min_scale_value
248 attr_accessor :min_scale_value
249 # Whether to show labels on the X axis or not, defaults
249 # Whether to show labels on the X axis or not, defaults
250 # to true, set to false if you want to turn them off.
250 # to true, set to false if you want to turn them off.
251 attr_accessor :show_x_labels
251 attr_accessor :show_x_labels
252 # This puts the X labels at alternative levels so if they
252 # This puts the X labels at alternative levels so if they
253 # are long field names they will not overlap so easily.
253 # are long field names they will not overlap so easily.
254 # Default it false, to turn on set to true.
254 # Default it false, to turn on set to true.
255 attr_accessor :stagger_x_labels
255 attr_accessor :stagger_x_labels
256 # This puts the Y labels at alternative levels so if they
256 # This puts the Y labels at alternative levels so if they
257 # are long field names they will not overlap so easily.
257 # are long field names they will not overlap so easily.
258 # Default it false, to turn on set to true.
258 # Default it false, to turn on set to true.
259 attr_accessor :stagger_y_labels
259 attr_accessor :stagger_y_labels
260 # This turns the X axis labels by 90 degrees.
260 # This turns the X axis labels by 90 degrees.
261 # Default it false, to turn on set to true.
261 # Default it false, to turn on set to true.
262 attr_accessor :rotate_x_labels
262 attr_accessor :rotate_x_labels
263 # This turns the Y axis labels by 90 degrees.
263 # This turns the Y axis labels by 90 degrees.
264 # Default it false, to turn on set to true.
264 # Default it false, to turn on set to true.
265 attr_accessor :rotate_y_labels
265 attr_accessor :rotate_y_labels
266 # How many "steps" to use between displayed X axis labels,
266 # How many "steps" to use between displayed X axis labels,
267 # a step of one means display every label, a step of two results
267 # a step of one means display every label, a step of two results
268 # in every other label being displayed (label <gap> label <gap> label),
268 # in every other label being displayed (label <gap> label <gap> label),
269 # a step of three results in every third label being displayed
269 # a step of three results in every third label being displayed
270 # (label <gap> <gap> label <gap> <gap> label) and so on.
270 # (label <gap> <gap> label <gap> <gap> label) and so on.
271 attr_accessor :step_x_labels
271 attr_accessor :step_x_labels
272 # Whether to (when taking "steps" between X axis labels) step from
272 # Whether to (when taking "steps" between X axis labels) step from
273 # the first label (i.e. always include the first label) or step from
273 # the first label (i.e. always include the first label) or step from
274 # the X axis origin (i.e. start with a gap if step_x_labels is greater
274 # the X axis origin (i.e. start with a gap if step_x_labels is greater
275 # than one).
275 # than one).
276 attr_accessor :step_include_first_x_label
276 attr_accessor :step_include_first_x_label
277 # Whether to show labels on the Y axis or not, defaults
277 # Whether to show labels on the Y axis or not, defaults
278 # to true, set to false if you want to turn them off.
278 # to true, set to false if you want to turn them off.
279 attr_accessor :show_y_labels
279 attr_accessor :show_y_labels
280 # Ensures only whole numbers are used as the scale divisions.
280 # Ensures only whole numbers are used as the scale divisions.
281 # Default it false, to turn on set to true. This has no effect if
281 # Default it false, to turn on set to true. This has no effect if
282 # scale divisions are less than 1.
282 # scale divisions are less than 1.
283 attr_accessor :scale_integers
283 attr_accessor :scale_integers
284 # This defines the gap between markers on the Y axis,
284 # This defines the gap between markers on the Y axis,
285 # default is a 10th of the max_value, e.g. you will have
285 # default is a 10th of the max_value, e.g. you will have
286 # 10 markers on the Y axis. NOTE: do not set this too
286 # 10 markers on the Y axis. NOTE: do not set this too
287 # low - you are limited to 999 markers, after that the
287 # low - you are limited to 999 markers, after that the
288 # graph won't generate.
288 # graph won't generate.
289 attr_accessor :scale_divisions
289 attr_accessor :scale_divisions
290 # Whether to show the title under the X axis labels,
290 # Whether to show the title under the X axis labels,
291 # default is false, set to true to show.
291 # default is false, set to true to show.
292 attr_accessor :show_x_title
292 attr_accessor :show_x_title
293 # What the title under X axis should be, e.g. 'Months'.
293 # What the title under X axis should be, e.g. 'Months'.
294 attr_accessor :x_title
294 attr_accessor :x_title
295 # Whether to show the title under the Y axis labels,
295 # Whether to show the title under the Y axis labels,
296 # default is false, set to true to show.
296 # default is false, set to true to show.
297 attr_accessor :show_y_title
297 attr_accessor :show_y_title
298 # Aligns writing mode for Y axis label.
298 # Aligns writing mode for Y axis label.
299 # Defaults to :bt (Bottom to Top).
299 # Defaults to :bt (Bottom to Top).
300 # Change to :tb (Top to Bottom) to reverse.
300 # Change to :tb (Top to Bottom) to reverse.
301 attr_accessor :y_title_text_direction
301 attr_accessor :y_title_text_direction
302 # What the title under Y axis should be, e.g. 'Sales in thousands'.
302 # What the title under Y axis should be, e.g. 'Sales in thousands'.
303 attr_accessor :y_title
303 attr_accessor :y_title
304 # Whether to show a title on the graph, defaults
304 # Whether to show a title on the graph, defaults
305 # to false, set to true to show.
305 # to false, set to true to show.
306 attr_accessor :show_graph_title
306 attr_accessor :show_graph_title
307 # What the title on the graph should be.
307 # What the title on the graph should be.
308 attr_accessor :graph_title
308 attr_accessor :graph_title
309 # Whether to show a subtitle on the graph, defaults
309 # Whether to show a subtitle on the graph, defaults
310 # to false, set to true to show.
310 # to false, set to true to show.
311 attr_accessor :show_graph_subtitle
311 attr_accessor :show_graph_subtitle
312 # What the subtitle on the graph should be.
312 # What the subtitle on the graph should be.
313 attr_accessor :graph_subtitle
313 attr_accessor :graph_subtitle
314 # Whether to show a key, defaults to false, set to
314 # Whether to show a key, defaults to false, set to
315 # true if you want to show it.
315 # true if you want to show it.
316 attr_accessor :key
316 attr_accessor :key
317 # Where the key should be positioned, defaults to
317 # Where the key should be positioned, defaults to
318 # :right, set to :bottom if you want to move it.
318 # :right, set to :bottom if you want to move it.
319 attr_accessor :key_position
319 attr_accessor :key_position
320 # Set the font size (in points) of the data point labels
320 # Set the font size (in points) of the data point labels
321 attr_accessor :font_size
321 attr_accessor :font_size
322 # Set the font size of the X axis labels
322 # Set the font size of the X axis labels
323 attr_accessor :x_label_font_size
323 attr_accessor :x_label_font_size
324 # Set the font size of the X axis title
324 # Set the font size of the X axis title
325 attr_accessor :x_title_font_size
325 attr_accessor :x_title_font_size
326 # Set the font size of the Y axis labels
326 # Set the font size of the Y axis labels
327 attr_accessor :y_label_font_size
327 attr_accessor :y_label_font_size
328 # Set the font size of the Y axis title
328 # Set the font size of the Y axis title
329 attr_accessor :y_title_font_size
329 attr_accessor :y_title_font_size
330 # Set the title font size
330 # Set the title font size
331 attr_accessor :title_font_size
331 attr_accessor :title_font_size
332 # Set the subtitle font size
332 # Set the subtitle font size
333 attr_accessor :subtitle_font_size
333 attr_accessor :subtitle_font_size
334 # Set the key font size
334 # Set the key font size
335 attr_accessor :key_font_size
335 attr_accessor :key_font_size
336 # Show guidelines for the X axis
336 # Show guidelines for the X axis
337 attr_accessor :show_x_guidelines
337 attr_accessor :show_x_guidelines
338 # Show guidelines for the Y axis
338 # Show guidelines for the Y axis
339 attr_accessor :show_y_guidelines
339 attr_accessor :show_y_guidelines
340 # Do not use CSS if set to true. Many SVG viewers do not support CSS, but
340 # Do not use CSS if set to true. Many SVG viewers do not support CSS, but
341 # not using CSS can result in larger SVGs as well as making it impossible to
341 # not using CSS can result in larger SVGs as well as making it impossible to
342 # change colors after the chart is generated. Defaults to false.
342 # change colors after the chart is generated. Defaults to false.
343 attr_accessor :no_css
343 attr_accessor :no_css
344 # Add popups for the data points on some graphs
344 # Add popups for the data points on some graphs
345 attr_accessor :add_popups
345 attr_accessor :add_popups
346
346
347
347
348 protected
348 protected
349
349
350 def sort( *arrys )
350 def sort( *arrys )
351 sort_multiple( arrys )
351 sort_multiple( arrys )
352 end
352 end
353
353
354 # Overwrite configuration options with supplied options. Used
354 # Overwrite configuration options with supplied options. Used
355 # by subclasses.
355 # by subclasses.
356 def init_with config
356 def init_with config
357 config.each { |key, value|
357 config.each { |key, value|
358 self.send( key.to_s+"=", value ) if methods.include? key.to_s
358 self.send( key.to_s+"=", value ) if methods.include? key.to_s
359 }
359 }
360 end
360 end
361
361
362 attr_accessor :top_align, :top_font, :right_align, :right_font
362 attr_accessor :top_align, :top_font, :right_align, :right_font
363
363
364 KEY_BOX_SIZE = 12
364 KEY_BOX_SIZE = 12
365
365
366 # Override this (and call super) to change the margin to the left
366 # Override this (and call super) to change the margin to the left
367 # of the plot area. Results in @border_left being set.
367 # of the plot area. Results in @border_left being set.
368 def calculate_left_margin
368 def calculate_left_margin
369 @border_left = 7
369 @border_left = 7
370 # Check for Y labels
370 # Check for Y labels
371 max_y_label_height_px = rotate_y_labels ?
371 max_y_label_height_px = rotate_y_labels ?
372 y_label_font_size :
372 y_label_font_size :
373 get_y_labels.max{|a,b|
373 get_y_labels.max{|a,b|
374 a.to_s.length<=>b.to_s.length
374 a.to_s.length<=>b.to_s.length
375 }.to_s.length * y_label_font_size * 0.6
375 }.to_s.length * y_label_font_size * 0.6
376 @border_left += max_y_label_height_px if show_y_labels
376 @border_left += max_y_label_height_px if show_y_labels
377 @border_left += max_y_label_height_px + 10 if stagger_y_labels
377 @border_left += max_y_label_height_px + 10 if stagger_y_labels
378 @border_left += y_title_font_size + 5 if show_y_title
378 @border_left += y_title_font_size + 5 if show_y_title
379 end
379 end
380
380
381
381
382 # Calculates the width of the widest Y label. This will be the
382 # Calculates the width of the widest Y label. This will be the
383 # character height if the Y labels are rotated
383 # character height if the Y labels are rotated
384 def max_y_label_width_px
384 def max_y_label_width_px
385 return font_size if rotate_y_labels
385 return font_size if rotate_y_labels
386 end
386 end
387
387
388
388
389 # Override this (and call super) to change the margin to the right
389 # Override this (and call super) to change the margin to the right
390 # of the plot area. Results in @border_right being set.
390 # of the plot area. Results in @border_right being set.
391 def calculate_right_margin
391 def calculate_right_margin
392 @border_right = 7
392 @border_right = 7
393 if key and key_position == :right
393 if key and key_position == :right
394 val = keys.max { |a,b| a.length <=> b.length }
394 val = keys.max { |a,b| a.length <=> b.length }
395 @border_right += val.length * key_font_size * 0.7
395 @border_right += val.length * key_font_size * 0.7
396 @border_right += KEY_BOX_SIZE
396 @border_right += KEY_BOX_SIZE
397 @border_right += 10 # Some padding around the box
397 @border_right += 10 # Some padding around the box
398 end
398 end
399 end
399 end
400
400
401
401
402 # Override this (and call super) to change the margin to the top
402 # Override this (and call super) to change the margin to the top
403 # of the plot area. Results in @border_top being set.
403 # of the plot area. Results in @border_top being set.
404 def calculate_top_margin
404 def calculate_top_margin
405 @border_top = 5
405 @border_top = 5
406 @border_top += title_font_size if show_graph_title
406 @border_top += title_font_size if show_graph_title
407 @border_top += 5
407 @border_top += 5
408 @border_top += subtitle_font_size if show_graph_subtitle
408 @border_top += subtitle_font_size if show_graph_subtitle
409 end
409 end
410
410
411
411
412 # Adds pop-up point information to a graph.
412 # Adds pop-up point information to a graph.
413 def add_popup( x, y, label )
413 def add_popup( x, y, label )
414 txt_width = label.length * font_size * 0.6 + 10
414 txt_width = label.length * font_size * 0.6 + 10
415 tx = (x+txt_width > width ? x-5 : x+5)
415 tx = (x+txt_width > width ? x-5 : x+5)
416 t = @foreground.add_element( "text", {
416 t = @foreground.add_element( "text", {
417 "x" => tx.to_s,
417 "x" => tx.to_s,
418 "y" => (y - font_size).to_s,
418 "y" => (y - font_size).to_s,
419 "visibility" => "hidden",
419 "visibility" => "hidden",
420 })
420 })
421 t.attributes["style"] = "fill: #000; "+
421 t.attributes["style"] = "fill: #000; "+
422 (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
422 (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
423 t.text = label.to_s
423 t.text = label.to_s
424 t.attributes["id"] = t.id.to_s
424 t.attributes["id"] = t.id.to_s
425
425
426 @foreground.add_element( "circle", {
426 @foreground.add_element( "circle", {
427 "cx" => x.to_s,
427 "cx" => x.to_s,
428 "cy" => y.to_s,
428 "cy" => y.to_s,
429 "r" => "10",
429 "r" => "10",
430 "style" => "opacity: 0",
430 "style" => "opacity: 0",
431 "onmouseover" =>
431 "onmouseover" =>
432 "document.getElementById(#{t.id}).setAttribute('visibility', 'visible' )",
432 "document.getElementById(#{t.id}).setAttribute('visibility', 'visible' )",
433 "onmouseout" =>
433 "onmouseout" =>
434 "document.getElementById(#{t.id}).setAttribute('visibility', 'hidden' )",
434 "document.getElementById(#{t.id}).setAttribute('visibility', 'hidden' )",
435 })
435 })
436
436
437 end
437 end
438
438
439
439
440 # Override this (and call super) to change the margin to the bottom
440 # Override this (and call super) to change the margin to the bottom
441 # of the plot area. Results in @border_bottom being set.
441 # of the plot area. Results in @border_bottom being set.
442 def calculate_bottom_margin
442 def calculate_bottom_margin
443 @border_bottom = 7
443 @border_bottom = 7
444 if key and key_position == :bottom
444 if key and key_position == :bottom
445 @border_bottom += @data.size * (font_size + 5)
445 @border_bottom += @data.size * (font_size + 5)
446 @border_bottom += 10
446 @border_bottom += 10
447 end
447 end
448 if show_x_labels
448 if show_x_labels
449 max_x_label_height_px = rotate_x_labels ?
449 max_x_label_height_px = rotate_x_labels ?
450 get_x_labels.max{|a,b|
450 get_x_labels.max{|a,b|
451 a.length<=>b.length
451 a.length<=>b.length
452 }.length * x_label_font_size * 0.6 :
452 }.length * x_label_font_size * 0.6 :
453 x_label_font_size
453 x_label_font_size
454 @border_bottom += max_x_label_height_px
454 @border_bottom += max_x_label_height_px
455 @border_bottom += max_x_label_height_px + 10 if stagger_x_labels
455 @border_bottom += max_x_label_height_px + 10 if stagger_x_labels
456 end
456 end
457 @border_bottom += x_title_font_size + 5 if show_x_title
457 @border_bottom += x_title_font_size + 5 if show_x_title
458 end
458 end
459
459
460
460
461 # Draws the background, axis, and labels.
461 # Draws the background, axis, and labels.
462 def draw_graph
462 def draw_graph
463 @graph = @root.add_element( "g", {
463 @graph = @root.add_element( "g", {
464 "transform" => "translate( #@border_left #@border_top )"
464 "transform" => "translate( #@border_left #@border_top )"
465 })
465 })
466
466
467 # Background
467 # Background
468 @graph.add_element( "rect", {
468 @graph.add_element( "rect", {
469 "x" => "0",
469 "x" => "0",
470 "y" => "0",
470 "y" => "0",
471 "width" => @graph_width.to_s,
471 "width" => @graph_width.to_s,
472 "height" => @graph_height.to_s,
472 "height" => @graph_height.to_s,
473 "class" => "graphBackground"
473 "class" => "graphBackground"
474 })
474 })
475
475
476 # Axis
476 # Axis
477 @graph.add_element( "path", {
477 @graph.add_element( "path", {
478 "d" => "M 0 0 v#@graph_height",
478 "d" => "M 0 0 v#@graph_height",
479 "class" => "axis",
479 "class" => "axis",
480 "id" => "xAxis"
480 "id" => "xAxis"
481 })
481 })
482 @graph.add_element( "path", {
482 @graph.add_element( "path", {
483 "d" => "M 0 #@graph_height h#@graph_width",
483 "d" => "M 0 #@graph_height h#@graph_width",
484 "class" => "axis",
484 "class" => "axis",
485 "id" => "yAxis"
485 "id" => "yAxis"
486 })
486 })
487
487
488 draw_x_labels
488 draw_x_labels
489 draw_y_labels
489 draw_y_labels
490 end
490 end
491
491
492
492
493 # Where in the X area the label is drawn
493 # Where in the X area the label is drawn
494 # Centered in the field, should be width/2. Start, 0.
494 # Centered in the field, should be width/2. Start, 0.
495 def x_label_offset( width )
495 def x_label_offset( width )
496 0
496 0
497 end
497 end
498
498
499 def make_datapoint_text( x, y, value, style="" )
499 def make_datapoint_text( x, y, value, style="" )
500 if show_data_values
500 if show_data_values
501 @foreground.add_element( "text", {
501 @foreground.add_element( "text", {
502 "x" => x.to_s,
502 "x" => x.to_s,
503 "y" => y.to_s,
503 "y" => y.to_s,
504 "class" => "dataPointLabel",
504 "class" => "dataPointLabel",
505 "style" => "#{style} stroke: #fff; stroke-width: 2;"
505 "style" => "#{style} stroke: #fff; stroke-width: 2;"
506 }).text = value.to_s
506 }).text = value.to_s
507 text = @foreground.add_element( "text", {
507 text = @foreground.add_element( "text", {
508 "x" => x.to_s,
508 "x" => x.to_s,
509 "y" => y.to_s,
509 "y" => y.to_s,
510 "class" => "dataPointLabel"
510 "class" => "dataPointLabel"
511 })
511 })
512 text.text = value.to_s
512 text.text = value.to_s
513 text.attributes["style"] = style if style.length > 0
513 text.attributes["style"] = style if style.length > 0
514 end
514 end
515 end
515 end
516
516
517
517
518 # Draws the X axis labels
518 # Draws the X axis labels
519 def draw_x_labels
519 def draw_x_labels
520 stagger = x_label_font_size + 5
520 stagger = x_label_font_size + 5
521 if show_x_labels
521 if show_x_labels
522 label_width = field_width
522 label_width = field_width
523
523
524 count = 0
524 count = 0
525 for label in get_x_labels
525 for label in get_x_labels
526 if step_include_first_x_label == true then
526 if step_include_first_x_label == true then
527 step = count % step_x_labels
527 step = count % step_x_labels
528 else
528 else
529 step = (count + 1) % step_x_labels
529 step = (count + 1) % step_x_labels
530 end
530 end
531
531
532 if step == 0 then
532 if step == 0 then
533 text = @graph.add_element( "text" )
533 text = @graph.add_element( "text" )
534 text.attributes["class"] = "xAxisLabels"
534 text.attributes["class"] = "xAxisLabels"
535 text.text = label.to_s
535 text.text = label.to_s
536
536
537 x = count * label_width + x_label_offset( label_width )
537 x = count * label_width + x_label_offset( label_width )
538 y = @graph_height + x_label_font_size + 3
538 y = @graph_height + x_label_font_size + 3
539 t = 0 - (font_size / 2)
539 t = 0 - (font_size / 2)
540
540
541 if stagger_x_labels and count % 2 == 1
541 if stagger_x_labels and count % 2 == 1
542 y += stagger
542 y += stagger
543 @graph.add_element( "path", {
543 @graph.add_element( "path", {
544 "d" => "M#{x} #@graph_height v#{stagger}",
544 "d" => "M#{x} #@graph_height v#{stagger}",
545 "class" => "staggerGuideLine"
545 "class" => "staggerGuideLine"
546 })
546 })
547 end
547 end
548
548
549 text.attributes["x"] = x.to_s
549 text.attributes["x"] = x.to_s
550 text.attributes["y"] = y.to_s
550 text.attributes["y"] = y.to_s
551 if rotate_x_labels
551 if rotate_x_labels
552 text.attributes["transform"] =
552 text.attributes["transform"] =
553 "rotate( 90 #{x} #{y-x_label_font_size} )"+
553 "rotate( 90 #{x} #{y-x_label_font_size} )"+
554 " translate( 0 -#{x_label_font_size/4} )"
554 " translate( 0 -#{x_label_font_size/4} )"
555 text.attributes["style"] = "text-anchor: start"
555 text.attributes["style"] = "text-anchor: start"
556 else
556 else
557 text.attributes["style"] = "text-anchor: middle"
557 text.attributes["style"] = "text-anchor: middle"
558 end
558 end
559 end
559 end
560
560
561 draw_x_guidelines( label_width, count ) if show_x_guidelines
561 draw_x_guidelines( label_width, count ) if show_x_guidelines
562 count += 1
562 count += 1
563 end
563 end
564 end
564 end
565 end
565 end
566
566
567
567
568 # Where in the Y area the label is drawn
568 # Where in the Y area the label is drawn
569 # Centered in the field, should be width/2. Start, 0.
569 # Centered in the field, should be width/2. Start, 0.
570 def y_label_offset( height )
570 def y_label_offset( height )
571 0
571 0
572 end
572 end
573
573
574
574
575 def field_width
575 def field_width
576 (@graph_width.to_f - font_size*2*right_font) /
576 (@graph_width.to_f - font_size*2*right_font) /
577 (get_x_labels.length - right_align)
577 (get_x_labels.length - right_align)
578 end
578 end
579
579
580
580
581 def field_height
581 def field_height
582 (@graph_height.to_f - font_size*2*top_font) /
582 (@graph_height.to_f - font_size*2*top_font) /
583 (get_y_labels.length - top_align)
583 (get_y_labels.length - top_align)
584 end
584 end
585
585
586
586
587 # Draws the Y axis labels
587 # Draws the Y axis labels
588 def draw_y_labels
588 def draw_y_labels
589 stagger = y_label_font_size + 5
589 stagger = y_label_font_size + 5
590 if show_y_labels
590 if show_y_labels
591 label_height = field_height
591 label_height = field_height
592
592
593 count = 0
593 count = 0
594 y_offset = @graph_height + y_label_offset( label_height )
594 y_offset = @graph_height + y_label_offset( label_height )
595 y_offset += font_size/1.2 unless rotate_y_labels
595 y_offset += font_size/1.2 unless rotate_y_labels
596 for label in get_y_labels
596 for label in get_y_labels
597 y = y_offset - (label_height * count)
597 y = y_offset - (label_height * count)
598 x = rotate_y_labels ? 0 : -3
598 x = rotate_y_labels ? 0 : -3
599
599
600 if stagger_y_labels and count % 2 == 1
600 if stagger_y_labels and count % 2 == 1
601 x -= stagger
601 x -= stagger
602 @graph.add_element( "path", {
602 @graph.add_element( "path", {
603 "d" => "M#{x} #{y} h#{stagger}",
603 "d" => "M#{x} #{y} h#{stagger}",
604 "class" => "staggerGuideLine"
604 "class" => "staggerGuideLine"
605 })
605 })
606 end
606 end
607
607
608 text = @graph.add_element( "text", {
608 text = @graph.add_element( "text", {
609 "x" => x.to_s,
609 "x" => x.to_s,
610 "y" => y.to_s,
610 "y" => y.to_s,
611 "class" => "yAxisLabels"
611 "class" => "yAxisLabels"
612 })
612 })
613 text.text = label.to_s
613 text.text = label.to_s
614 if rotate_y_labels
614 if rotate_y_labels
615 text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
615 text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
616 "rotate( 90 #{x} #{y} ) "
616 "rotate( 90 #{x} #{y} ) "
617 text.attributes["style"] = "text-anchor: middle"
617 text.attributes["style"] = "text-anchor: middle"
618 else
618 else
619 text.attributes["y"] = (y - (y_label_font_size/2)).to_s
619 text.attributes["y"] = (y - (y_label_font_size/2)).to_s
620 text.attributes["style"] = "text-anchor: end"
620 text.attributes["style"] = "text-anchor: end"
621 end
621 end
622 draw_y_guidelines( label_height, count ) if show_y_guidelines
622 draw_y_guidelines( label_height, count ) if show_y_guidelines
623 count += 1
623 count += 1
624 end
624 end
625 end
625 end
626 end
626 end
627
627
628
628
629 # Draws the X axis guidelines
629 # Draws the X axis guidelines
630 def draw_x_guidelines( label_height, count )
630 def draw_x_guidelines( label_height, count )
631 if count != 0
631 if count != 0
632 @graph.add_element( "path", {
632 @graph.add_element( "path", {
633 "d" => "M#{label_height*count} 0 v#@graph_height",
633 "d" => "M#{label_height*count} 0 v#@graph_height",
634 "class" => "guideLines"
634 "class" => "guideLines"
635 })
635 })
636 end
636 end
637 end
637 end
638
638
639
639
640 # Draws the Y axis guidelines
640 # Draws the Y axis guidelines
641 def draw_y_guidelines( label_height, count )
641 def draw_y_guidelines( label_height, count )
642 if count != 0
642 if count != 0
643 @graph.add_element( "path", {
643 @graph.add_element( "path", {
644 "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
644 "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
645 "class" => "guideLines"
645 "class" => "guideLines"
646 })
646 })
647 end
647 end
648 end
648 end
649
649
650
650
651 # Draws the graph title and subtitle
651 # Draws the graph title and subtitle
652 def draw_titles
652 def draw_titles
653 if show_graph_title
653 if show_graph_title
654 @root.add_element( "text", {
654 @root.add_element( "text", {
655 "x" => (width / 2).to_s,
655 "x" => (width / 2).to_s,
656 "y" => (title_font_size).to_s,
656 "y" => (title_font_size).to_s,
657 "class" => "mainTitle"
657 "class" => "mainTitle"
658 }).text = graph_title.to_s
658 }).text = graph_title.to_s
659 end
659 end
660
660
661 if show_graph_subtitle
661 if show_graph_subtitle
662 y_subtitle = show_graph_title ?
662 y_subtitle = show_graph_title ?
663 title_font_size + 10 :
663 title_font_size + 10 :
664 subtitle_font_size
664 subtitle_font_size
665 @root.add_element("text", {
665 @root.add_element("text", {
666 "x" => (width / 2).to_s,
666 "x" => (width / 2).to_s,
667 "y" => (y_subtitle).to_s,
667 "y" => (y_subtitle).to_s,
668 "class" => "subTitle"
668 "class" => "subTitle"
669 }).text = graph_subtitle.to_s
669 }).text = graph_subtitle.to_s
670 end
670 end
671
671
672 if show_x_title
672 if show_x_title
673 y = @graph_height + @border_top + x_title_font_size
673 y = @graph_height + @border_top + x_title_font_size
674 if show_x_labels
674 if show_x_labels
675 y += x_label_font_size + 5 if stagger_x_labels
675 y += x_label_font_size + 5 if stagger_x_labels
676 y += x_label_font_size + 5
676 y += x_label_font_size + 5
677 end
677 end
678 x = width / 2
678 x = width / 2
679
679
680 @root.add_element("text", {
680 @root.add_element("text", {
681 "x" => x.to_s,
681 "x" => x.to_s,
682 "y" => y.to_s,
682 "y" => y.to_s,
683 "class" => "xAxisTitle",
683 "class" => "xAxisTitle",
684 }).text = x_title.to_s
684 }).text = x_title.to_s
685 end
685 end
686
686
687 if show_y_title
687 if show_y_title
688 x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
688 x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
689 y = height / 2
689 y = height / 2
690
690
691 text = @root.add_element("text", {
691 text = @root.add_element("text", {
692 "x" => x.to_s,
692 "x" => x.to_s,
693 "y" => y.to_s,
693 "y" => y.to_s,
694 "class" => "yAxisTitle",
694 "class" => "yAxisTitle",
695 })
695 })
696 text.text = y_title.to_s
696 text.text = y_title.to_s
697 if y_title_text_direction == :bt
697 if y_title_text_direction == :bt
698 text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
698 text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
699 else
699 else
700 text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
700 text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
701 end
701 end
702 end
702 end
703 end
703 end
704
704
705 def keys
705 def keys
706 return @data.collect{ |d| d[:title] }
706 return @data.collect{ |d| d[:title] }
707 end
707 end
708
708
709 # Draws the legend on the graph
709 # Draws the legend on the graph
710 def draw_legend
710 def draw_legend
711 if key
711 if key
712 group = @root.add_element( "g" )
712 group = @root.add_element( "g" )
713
713
714 key_count = 0
714 key_count = 0
715 for key_name in keys
715 for key_name in keys
716 y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
716 y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
717 group.add_element( "rect", {
717 group.add_element( "rect", {
718 "x" => 0.to_s,
718 "x" => 0.to_s,
719 "y" => y_offset.to_s,
719 "y" => y_offset.to_s,
720 "width" => KEY_BOX_SIZE.to_s,
720 "width" => KEY_BOX_SIZE.to_s,
721 "height" => KEY_BOX_SIZE.to_s,
721 "height" => KEY_BOX_SIZE.to_s,
722 "class" => "key#{key_count+1}"
722 "class" => "key#{key_count+1}"
723 })
723 })
724 group.add_element( "text", {
724 group.add_element( "text", {
725 "x" => (KEY_BOX_SIZE + 5).to_s,
725 "x" => (KEY_BOX_SIZE + 5).to_s,
726 "y" => (y_offset + KEY_BOX_SIZE - 2).to_s,
726 "y" => (y_offset + KEY_BOX_SIZE - 2).to_s,
727 "class" => "keyText"
727 "class" => "keyText"
728 }).text = key_name.to_s
728 }).text = key_name.to_s
729 key_count += 1
729 key_count += 1
730 end
730 end
731
731
732 case key_position
732 case key_position
733 when :right
733 when :right
734 x_offset = @graph_width + @border_left + 10
734 x_offset = @graph_width + @border_left + 10
735 y_offset = @border_top + 20
735 y_offset = @border_top + 20
736 when :bottom
736 when :bottom
737 x_offset = @border_left + 20
737 x_offset = @border_left + 20
738 y_offset = @border_top + @graph_height + 5
738 y_offset = @border_top + @graph_height + 5
739 if show_x_labels
739 if show_x_labels
740 max_x_label_height_px = rotate_x_labels ?
740 max_x_label_height_px = rotate_x_labels ?
741 get_x_labels.max{|a,b|
741 get_x_labels.max{|a,b|
742 a.length<=>b.length
742 a.length<=>b.length
743 }.length * x_label_font_size :
743 }.length * x_label_font_size :
744 x_label_font_size
744 x_label_font_size
745 y_offset += max_x_label_height_px
745 y_offset += max_x_label_height_px
746 y_offset += max_x_label_height_px + 5 if stagger_x_labels
746 y_offset += max_x_label_height_px + 5 if stagger_x_labels
747 end
747 end
748 y_offset += x_title_font_size + 5 if show_x_title
748 y_offset += x_title_font_size + 5 if show_x_title
749 end
749 end
750 group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
750 group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
751 end
751 end
752 end
752 end
753
753
754
754
755 private
755 private
756
756
757 def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
757 def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
758 if lo < hi
758 if lo < hi
759 p = partition(arrys,lo,hi)
759 p = partition(arrys,lo,hi)
760 sort_multiple(arrys, lo, p-1)
760 sort_multiple(arrys, lo, p-1)
761 sort_multiple(arrys, p+1, hi)
761 sort_multiple(arrys, p+1, hi)
762 end
762 end
763 arrys
763 arrys
764 end
764 end
765
765
766 def partition( arrys, lo, hi )
766 def partition( arrys, lo, hi )
767 p = arrys[0][lo]
767 p = arrys[0][lo]
768 l = lo
768 l = lo
769 z = lo+1
769 z = lo+1
770 while z <= hi
770 while z <= hi
771 if arrys[0][z] < p
771 if arrys[0][z] < p
772 l += 1
772 l += 1
773 arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
773 arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
774 end
774 end
775 z += 1
775 z += 1
776 end
776 end
777 arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
777 arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
778 l
778 l
779 end
779 end
780
780
781 def style
781 def style
782 if no_css
782 if no_css
783 styles = parse_css
783 styles = parse_css
784 @root.elements.each("//*[@class]") { |el|
784 @root.elements.each("//*[@class]") { |el|
785 cl = el.attributes["class"]
785 cl = el.attributes["class"]
786 style = styles[cl]
786 style = styles[cl]
787 style += el.attributes["style"] if el.attributes["style"]
787 style += el.attributes["style"] if el.attributes["style"]
788 el.attributes["style"] = style
788 el.attributes["style"] = style
789 }
789 }
790 end
790 end
791 end
791 end
792
792
793 def parse_css
793 def parse_css
794 css = get_style
794 css = get_style
795 rv = {}
795 rv = {}
796 while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
796 while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
797 names_orig = names = $1
797 names_orig = names = $1
798 css = $'
798 css = $'
799 css =~ /([^}]+)\}/m
799 css =~ /([^}]+)\}/m
800 content = $1
800 content = $1
801 css = $'
801 css = $'
802
802
803 nms = []
803 nms = []
804 while names =~ /^\s*,?\s*\.(\w+)/
804 while names =~ /^\s*,?\s*\.(\w+)/
805 nms << $1
805 nms << $1
806 names = $'
806 names = $'
807 end
807 end
808
808
809 content = content.tr( "\n\t", " ")
809 content = content.tr( "\n\t", " ")
810 for name in nms
810 for name in nms
811 current = rv[name]
811 current = rv[name]
812 current = current ? current+"; "+content : content
812 current = current ? current+"; "+content : content
813 rv[name] = current.strip.squeeze(" ")
813 rv[name] = current.strip.squeeze(" ")
814 end
814 end
815 end
815 end
816 return rv
816 return rv
817 end
817 end
818
818
819
819
820 # Override and place code to add defs here
820 # Override and place code to add defs here
821 def add_defs defs
821 def add_defs defs
822 end
822 end
823
823
824
824
825 def start_svg
825 def start_svg
826 # Base document
826 # Base document
827 @doc = Document.new
827 @doc = Document.new
828 @doc << XMLDecl.new
828 @doc << XMLDecl.new
829 @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
829 @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
830 %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
830 %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
831 if style_sheet && style_sheet != ''
831 if style_sheet && style_sheet != ''
832 @doc << ProcessingInstruction.new( "xml-stylesheet",
832 @doc << Instruction.new( "xml-stylesheet",
833 %Q{href="#{style_sheet}" type="text/css"} )
833 %Q{href="#{style_sheet}" type="text/css"} )
834 end
834 end
835 @root = @doc.add_element( "svg", {
835 @root = @doc.add_element( "svg", {
836 "width" => width.to_s,
836 "width" => width.to_s,
837 "height" => height.to_s,
837 "height" => height.to_s,
838 "viewBox" => "0 0 #{width} #{height}",
838 "viewBox" => "0 0 #{width} #{height}",
839 "xmlns" => "http://www.w3.org/2000/svg",
839 "xmlns" => "http://www.w3.org/2000/svg",
840 "xmlns:xlink" => "http://www.w3.org/1999/xlink",
840 "xmlns:xlink" => "http://www.w3.org/1999/xlink",
841 "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
841 "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
842 "a3:scriptImplementation" => "Adobe"
842 "a3:scriptImplementation" => "Adobe"
843 })
843 })
844 @root << Comment.new( " "+"\\"*66 )
844 @root << Comment.new( " "+"\\"*66 )
845 @root << Comment.new( " Created with SVG::Graph " )
845 @root << Comment.new( " Created with SVG::Graph " )
846 @root << Comment.new( " SVG::Graph by Sean E. Russell " )
846 @root << Comment.new( " SVG::Graph by Sean E. Russell " )
847 @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
847 @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
848 " Leo Lapworth & Stephan Morgan " )
848 " Leo Lapworth & Stephan Morgan " )
849 @root << Comment.new( " "+"/"*66 )
849 @root << Comment.new( " "+"/"*66 )
850
850
851 defs = @root.add_element( "defs" )
851 defs = @root.add_element( "defs" )
852 add_defs defs
852 add_defs defs
853 if not(style_sheet && style_sheet != '') and !no_css
853 if not(style_sheet && style_sheet != '') and !no_css
854 @root << Comment.new(" include default stylesheet if none specified ")
854 @root << Comment.new(" include default stylesheet if none specified ")
855 style = defs.add_element( "style", {"type"=>"text/css"} )
855 style = defs.add_element( "style", {"type"=>"text/css"} )
856 style << CData.new( get_style )
856 style << CData.new( get_style )
857 end
857 end
858
858
859 @root << Comment.new( "SVG Background" )
859 @root << Comment.new( "SVG Background" )
860 @root.add_element( "rect", {
860 @root.add_element( "rect", {
861 "width" => width.to_s,
861 "width" => width.to_s,
862 "height" => height.to_s,
862 "height" => height.to_s,
863 "x" => "0",
863 "x" => "0",
864 "y" => "0",
864 "y" => "0",
865 "class" => "svgBackground"
865 "class" => "svgBackground"
866 })
866 })
867 end
867 end
868
868
869
869
870 def calculate_graph_dimensions
870 def calculate_graph_dimensions
871 calculate_left_margin
871 calculate_left_margin
872 calculate_right_margin
872 calculate_right_margin
873 calculate_bottom_margin
873 calculate_bottom_margin
874 calculate_top_margin
874 calculate_top_margin
875 @graph_width = width - @border_left - @border_right
875 @graph_width = width - @border_left - @border_right
876 @graph_height = height - @border_top - @border_bottom
876 @graph_height = height - @border_top - @border_bottom
877 end
877 end
878
878
879 def get_style
879 def get_style
880 return <<EOL
880 return <<EOL
881 /* Copy from here for external style sheet */
881 /* Copy from here for external style sheet */
882 .svgBackground{
882 .svgBackground{
883 fill:#ffffff;
883 fill:#ffffff;
884 }
884 }
885 .graphBackground{
885 .graphBackground{
886 fill:#f5f5f5;
886 fill:#f5f5f5;
887 }
887 }
888
888
889 /* graphs titles */
889 /* graphs titles */
890 .mainTitle{
890 .mainTitle{
891 text-anchor: middle;
891 text-anchor: middle;
892 fill: #555555;
892 fill: #555555;
893 font-size: #{title_font_size}px;
893 font-size: #{title_font_size}px;
894 font-family: "Verdana", sans-serif;
894 font-family: "Verdana", sans-serif;
895 font-weight: bold;
895 font-weight: bold;
896 }
896 }
897 .subTitle{
897 .subTitle{
898 text-anchor: middle;
898 text-anchor: middle;
899 fill: #999999;
899 fill: #999999;
900 font-size: #{subtitle_font_size}px;
900 font-size: #{subtitle_font_size}px;
901 font-family: "Verdana", sans-serif;
901 font-family: "Verdana", sans-serif;
902 font-weight: normal;
902 font-weight: normal;
903 }
903 }
904
904
905 .axis{
905 .axis{
906 stroke: #666666;
906 stroke: #666666;
907 stroke-width: 1px;
907 stroke-width: 1px;
908 }
908 }
909
909
910 .guideLines{
910 .guideLines{
911 stroke: #666666;
911 stroke: #666666;
912 stroke-width: 1px;
912 stroke-width: 1px;
913 stroke-dasharray:2,2,2;
913 stroke-dasharray:2,2,2;
914 }
914 }
915
915
916 .xAxisLabels{
916 .xAxisLabels{
917 text-anchor: middle;
917 text-anchor: middle;
918 fill: #000000;
918 fill: #000000;
919 font-size: #{x_label_font_size}px;
919 font-size: #{x_label_font_size}px;
920 font-family: "Verdana", sans-serif;
920 font-family: "Verdana", sans-serif;
921 font-weight: normal;
921 font-weight: normal;
922 }
922 }
923
923
924 .yAxisLabels{
924 .yAxisLabels{
925 text-anchor: end;
925 text-anchor: end;
926 fill: #000000;
926 fill: #000000;
927 font-size: #{y_label_font_size}px;
927 font-size: #{y_label_font_size}px;
928 font-family: "Verdana", sans-serif;
928 font-family: "Verdana", sans-serif;
929 font-weight: normal;
929 font-weight: normal;
930 }
930 }
931
931
932 .xAxisTitle{
932 .xAxisTitle{
933 text-anchor: middle;
933 text-anchor: middle;
934 fill: #ff0000;
934 fill: #ff0000;
935 font-size: #{x_title_font_size}px;
935 font-size: #{x_title_font_size}px;
936 font-family: "Verdana", sans-serif;
936 font-family: "Verdana", sans-serif;
937 font-weight: normal;
937 font-weight: normal;
938 }
938 }
939
939
940 .yAxisTitle{
940 .yAxisTitle{
941 fill: #ff0000;
941 fill: #ff0000;
942 text-anchor: middle;
942 text-anchor: middle;
943 font-size: #{y_title_font_size}px;
943 font-size: #{y_title_font_size}px;
944 font-family: "Verdana", sans-serif;
944 font-family: "Verdana", sans-serif;
945 font-weight: normal;
945 font-weight: normal;
946 }
946 }
947
947
948 .dataPointLabel{
948 .dataPointLabel{
949 fill: #000000;
949 fill: #000000;
950 text-anchor:middle;
950 text-anchor:middle;
951 font-size: 10px;
951 font-size: 10px;
952 font-family: "Verdana", sans-serif;
952 font-family: "Verdana", sans-serif;
953 font-weight: normal;
953 font-weight: normal;
954 }
954 }
955
955
956 .staggerGuideLine{
956 .staggerGuideLine{
957 fill: none;
957 fill: none;
958 stroke: #000000;
958 stroke: #000000;
959 stroke-width: 0.5px;
959 stroke-width: 0.5px;
960 }
960 }
961
961
962 #{get_css}
962 #{get_css}
963
963
964 .keyText{
964 .keyText{
965 fill: #000000;
965 fill: #000000;
966 text-anchor:start;
966 text-anchor:start;
967 font-size: #{key_font_size}px;
967 font-size: #{key_font_size}px;
968 font-family: "Verdana", sans-serif;
968 font-family: "Verdana", sans-serif;
969 font-weight: normal;
969 font-weight: normal;
970 }
970 }
971 /* End copy for external style sheet */
971 /* End copy for external style sheet */
972 EOL
972 EOL
973 end
973 end
974
974
975 end
975 end
976 end
976 end
977 end
977 end
@@ -1,1140 +1,1140
1 # vim:ts=4:sw=4:
1 # vim:ts=4:sw=4:
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
3 #
3 #
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
7 # License:: BSD
7 # License:: BSD
8 #
8 #
9 # (see http://hobix.com/textile/ for a Textile Reference.)
9 # (see http://hobix.com/textile/ for a Textile Reference.)
10 #
10 #
11 # Based on (and also inspired by) both:
11 # Based on (and also inspired by) both:
12 #
12 #
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
14 # Textism for PHP: http://www.textism.com/tools/textile/
14 # Textism for PHP: http://www.textism.com/tools/textile/
15 #
15 #
16 #
16 #
17
17
18 # = RedCloth
18 # = RedCloth
19 #
19 #
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
21 # into HTML. You can use either format, intermingled or separately.
21 # into HTML. You can use either format, intermingled or separately.
22 # You can also extend RedCloth to honor your own custom text stylings.
22 # You can also extend RedCloth to honor your own custom text stylings.
23 #
23 #
24 # RedCloth users are encouraged to use Textile if they are generating
24 # RedCloth users are encouraged to use Textile if they are generating
25 # HTML and to use Markdown if others will be viewing the plain text.
25 # HTML and to use Markdown if others will be viewing the plain text.
26 #
26 #
27 # == What is Textile?
27 # == What is Textile?
28 #
28 #
29 # Textile is a simple formatting style for text
29 # Textile is a simple formatting style for text
30 # documents, loosely based on some HTML conventions.
30 # documents, loosely based on some HTML conventions.
31 #
31 #
32 # == Sample Textile Text
32 # == Sample Textile Text
33 #
33 #
34 # h2. This is a title
34 # h2. This is a title
35 #
35 #
36 # h3. This is a subhead
36 # h3. This is a subhead
37 #
37 #
38 # This is a bit of paragraph.
38 # This is a bit of paragraph.
39 #
39 #
40 # bq. This is a blockquote.
40 # bq. This is a blockquote.
41 #
41 #
42 # = Writing Textile
42 # = Writing Textile
43 #
43 #
44 # A Textile document consists of paragraphs. Paragraphs
44 # A Textile document consists of paragraphs. Paragraphs
45 # can be specially formatted by adding a small instruction
45 # can be specially formatted by adding a small instruction
46 # to the beginning of the paragraph.
46 # to the beginning of the paragraph.
47 #
47 #
48 # h[n]. Header of size [n].
48 # h[n]. Header of size [n].
49 # bq. Blockquote.
49 # bq. Blockquote.
50 # # Numeric list.
50 # # Numeric list.
51 # * Bulleted list.
51 # * Bulleted list.
52 #
52 #
53 # == Quick Phrase Modifiers
53 # == Quick Phrase Modifiers
54 #
54 #
55 # Quick phrase modifiers are also included, to allow formatting
55 # Quick phrase modifiers are also included, to allow formatting
56 # of small portions of text within a paragraph.
56 # of small portions of text within a paragraph.
57 #
57 #
58 # \_emphasis\_
58 # \_emphasis\_
59 # \_\_italicized\_\_
59 # \_\_italicized\_\_
60 # \*strong\*
60 # \*strong\*
61 # \*\*bold\*\*
61 # \*\*bold\*\*
62 # ??citation??
62 # ??citation??
63 # -deleted text-
63 # -deleted text-
64 # +inserted text+
64 # +inserted text+
65 # ^superscript^
65 # ^superscript^
66 # ~subscript~
66 # ~subscript~
67 # @code@
67 # @code@
68 # %(classname)span%
68 # %(classname)span%
69 #
69 #
70 # ==notextile== (leave text alone)
70 # ==notextile== (leave text alone)
71 #
71 #
72 # == Links
72 # == Links
73 #
73 #
74 # To make a hypertext link, put the link text in "quotation
74 # To make a hypertext link, put the link text in "quotation
75 # marks" followed immediately by a colon and the URL of the link.
75 # marks" followed immediately by a colon and the URL of the link.
76 #
76 #
77 # Optional: text in (parentheses) following the link text,
77 # Optional: text in (parentheses) following the link text,
78 # but before the closing quotation mark, will become a Title
78 # but before the closing quotation mark, will become a Title
79 # attribute for the link, visible as a tool tip when a cursor is above it.
79 # attribute for the link, visible as a tool tip when a cursor is above it.
80 #
80 #
81 # Example:
81 # Example:
82 #
82 #
83 # "This is a link (This is a title) ":http://www.textism.com
83 # "This is a link (This is a title) ":http://www.textism.com
84 #
84 #
85 # Will become:
85 # Will become:
86 #
86 #
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
88 #
88 #
89 # == Images
89 # == Images
90 #
90 #
91 # To insert an image, put the URL for the image inside exclamation marks.
91 # To insert an image, put the URL for the image inside exclamation marks.
92 #
92 #
93 # Optional: text that immediately follows the URL in (parentheses) will
93 # Optional: text that immediately follows the URL in (parentheses) will
94 # be used as the Alt text for the image. Images on the web should always
94 # be used as the Alt text for the image. Images on the web should always
95 # have descriptive Alt text for the benefit of readers using non-graphical
95 # have descriptive Alt text for the benefit of readers using non-graphical
96 # browsers.
96 # browsers.
97 #
97 #
98 # Optional: place a colon followed by a URL immediately after the
98 # Optional: place a colon followed by a URL immediately after the
99 # closing ! to make the image into a link.
99 # closing ! to make the image into a link.
100 #
100 #
101 # Example:
101 # Example:
102 #
102 #
103 # !http://www.textism.com/common/textist.gif(Textist)!
103 # !http://www.textism.com/common/textist.gif(Textist)!
104 #
104 #
105 # Will become:
105 # Will become:
106 #
106 #
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
108 #
108 #
109 # With a link:
109 # With a link:
110 #
110 #
111 # !/common/textist.gif(Textist)!:http://textism.com
111 # !/common/textist.gif(Textist)!:http://textism.com
112 #
112 #
113 # Will become:
113 # Will become:
114 #
114 #
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
116 #
116 #
117 # == Defining Acronyms
117 # == Defining Acronyms
118 #
118 #
119 # HTML allows authors to define acronyms via the tag. The definition appears as a
119 # HTML allows authors to define acronyms via the tag. The definition appears as a
120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
121 # this should be used at least once for each acronym in documents where they appear.
121 # this should be used at least once for each acronym in documents where they appear.
122 #
122 #
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
124 # immediately following the acronym.
124 # immediately following the acronym.
125 #
125 #
126 # Example:
126 # Example:
127 #
127 #
128 # ACLU(American Civil Liberties Union)
128 # ACLU(American Civil Liberties Union)
129 #
129 #
130 # Will become:
130 # Will become:
131 #
131 #
132 # <acronym title="American Civil Liberties Union">ACLU</acronym>
132 # <acronym title="American Civil Liberties Union">ACLU</acronym>
133 #
133 #
134 # == Adding Tables
134 # == Adding Tables
135 #
135 #
136 # In Textile, simple tables can be added by seperating each column by
136 # In Textile, simple tables can be added by seperating each column by
137 # a pipe.
137 # a pipe.
138 #
138 #
139 # |a|simple|table|row|
139 # |a|simple|table|row|
140 # |And|Another|table|row|
140 # |And|Another|table|row|
141 #
141 #
142 # Attributes are defined by style definitions in parentheses.
142 # Attributes are defined by style definitions in parentheses.
143 #
143 #
144 # table(border:1px solid black).
144 # table(border:1px solid black).
145 # (background:#ddd;color:red). |{}| | | |
145 # (background:#ddd;color:red). |{}| | | |
146 #
146 #
147 # == Using RedCloth
147 # == Using RedCloth
148 #
148 #
149 # RedCloth is simply an extension of the String class, which can handle
149 # RedCloth is simply an extension of the String class, which can handle
150 # Textile formatting. Use it like a String and output HTML with its
150 # Textile formatting. Use it like a String and output HTML with its
151 # RedCloth#to_html method.
151 # RedCloth#to_html method.
152 #
152 #
153 # doc = RedCloth.new "
153 # doc = RedCloth.new "
154 #
154 #
155 # h2. Test document
155 # h2. Test document
156 #
156 #
157 # Just a simple test."
157 # Just a simple test."
158 #
158 #
159 # puts doc.to_html
159 # puts doc.to_html
160 #
160 #
161 # By default, RedCloth uses both Textile and Markdown formatting, with
161 # By default, RedCloth uses both Textile and Markdown formatting, with
162 # Textile formatting taking precedence. If you want to turn off Markdown
162 # Textile formatting taking precedence. If you want to turn off Markdown
163 # formatting, to boost speed and limit the processor:
163 # formatting, to boost speed and limit the processor:
164 #
164 #
165 # class RedCloth::Textile.new( str )
165 # class RedCloth::Textile.new( str )
166
166
167 class RedCloth < String
167 class RedCloth < String
168
168
169 VERSION = '3.0.4'
169 VERSION = '3.0.4'
170 DEFAULT_RULES = [:textile, :markdown]
170 DEFAULT_RULES = [:textile, :markdown]
171
171
172 #
172 #
173 # Two accessor for setting security restrictions.
173 # Two accessor for setting security restrictions.
174 #
174 #
175 # This is a nice thing if you're using RedCloth for
175 # This is a nice thing if you're using RedCloth for
176 # formatting in public places (e.g. Wikis) where you
176 # formatting in public places (e.g. Wikis) where you
177 # don't want users to abuse HTML for bad things.
177 # don't want users to abuse HTML for bad things.
178 #
178 #
179 # If +:filter_html+ is set, HTML which wasn't
179 # If +:filter_html+ is set, HTML which wasn't
180 # created by the Textile processor will be escaped.
180 # created by the Textile processor will be escaped.
181 #
181 #
182 # If +:filter_styles+ is set, it will also disable
182 # If +:filter_styles+ is set, it will also disable
183 # the style markup specifier. ('{color: red}')
183 # the style markup specifier. ('{color: red}')
184 #
184 #
185 attr_accessor :filter_html, :filter_styles
185 attr_accessor :filter_html, :filter_styles
186
186
187 #
187 #
188 # Accessor for toggling hard breaks.
188 # Accessor for toggling hard breaks.
189 #
189 #
190 # If +:hard_breaks+ is set, single newlines will
190 # If +:hard_breaks+ is set, single newlines will
191 # be converted to HTML break tags. This is the
191 # be converted to HTML break tags. This is the
192 # default behavior for traditional RedCloth.
192 # default behavior for traditional RedCloth.
193 #
193 #
194 attr_accessor :hard_breaks
194 attr_accessor :hard_breaks
195
195
196 # Accessor for toggling lite mode.
196 # Accessor for toggling lite mode.
197 #
197 #
198 # In lite mode, block-level rules are ignored. This means
198 # In lite mode, block-level rules are ignored. This means
199 # that tables, paragraphs, lists, and such aren't available.
199 # that tables, paragraphs, lists, and such aren't available.
200 # Only the inline markup for bold, italics, entities and so on.
200 # Only the inline markup for bold, italics, entities and so on.
201 #
201 #
202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 # r.to_html
203 # r.to_html
204 # #=> "And then? She <strong>fell</strong>!"
204 # #=> "And then? She <strong>fell</strong>!"
205 #
205 #
206 attr_accessor :lite_mode
206 attr_accessor :lite_mode
207
207
208 #
208 #
209 # Accessor for toggling span caps.
209 # Accessor for toggling span caps.
210 #
210 #
211 # Textile places `span' tags around capitalized
211 # Textile places `span' tags around capitalized
212 # words by default, but this wreaks havoc on Wikis.
212 # words by default, but this wreaks havoc on Wikis.
213 # If +:no_span_caps+ is set, this will be
213 # If +:no_span_caps+ is set, this will be
214 # suppressed.
214 # suppressed.
215 #
215 #
216 attr_accessor :no_span_caps
216 attr_accessor :no_span_caps
217
217
218 #
218 #
219 # Establishes the markup predence. Available rules include:
219 # Establishes the markup predence. Available rules include:
220 #
220 #
221 # == Textile Rules
221 # == Textile Rules
222 #
222 #
223 # The following textile rules can be set individually. Or add the complete
223 # The following textile rules can be set individually. Or add the complete
224 # set of rules with the single :textile rule, which supplies the rule set in
224 # set of rules with the single :textile rule, which supplies the rule set in
225 # the following precedence:
225 # the following precedence:
226 #
226 #
227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 # block_textile_table:: Textile table block structures
228 # block_textile_table:: Textile table block structures
229 # block_textile_lists:: Textile list structures
229 # block_textile_lists:: Textile list structures
230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 # inline_textile_image:: Textile inline images
231 # inline_textile_image:: Textile inline images
232 # inline_textile_link:: Textile inline links
232 # inline_textile_link:: Textile inline links
233 # inline_textile_span:: Textile inline spans
233 # inline_textile_span:: Textile inline spans
234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 #
235 #
236 # == Markdown
236 # == Markdown
237 #
237 #
238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 # block_markdown_setext:: Markdown setext headers
239 # block_markdown_setext:: Markdown setext headers
240 # block_markdown_atx:: Markdown atx headers
240 # block_markdown_atx:: Markdown atx headers
241 # block_markdown_rule:: Markdown horizontal rules
241 # block_markdown_rule:: Markdown horizontal rules
242 # block_markdown_bq:: Markdown blockquotes
242 # block_markdown_bq:: Markdown blockquotes
243 # block_markdown_lists:: Markdown lists
243 # block_markdown_lists:: Markdown lists
244 # inline_markdown_link:: Markdown links
244 # inline_markdown_link:: Markdown links
245 attr_accessor :rules
245 attr_accessor :rules
246
246
247 # Returns a new RedCloth object, based on _string_ and
247 # Returns a new RedCloth object, based on _string_ and
248 # enforcing all the included _restrictions_.
248 # enforcing all the included _restrictions_.
249 #
249 #
250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
251 # r.to_html
251 # r.to_html
252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
253 #
253 #
254 def initialize( string, restrictions = [] )
254 def initialize( string, restrictions = [] )
255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 super( string )
256 super( string )
257 end
257 end
258
258
259 #
259 #
260 # Generates HTML from the Textile contents.
260 # Generates HTML from the Textile contents.
261 #
261 #
262 # r = RedCloth.new( "And then? She *fell*!" )
262 # r = RedCloth.new( "And then? She *fell*!" )
263 # r.to_html( true )
263 # r.to_html( true )
264 # #=>"And then? She <strong>fell</strong>!"
264 # #=>"And then? She <strong>fell</strong>!"
265 #
265 #
266 def to_html( *rules )
266 def to_html( *rules )
267 rules = DEFAULT_RULES if rules.empty?
267 rules = DEFAULT_RULES if rules.empty?
268 # make our working copy
268 # make our working copy
269 text = self.dup
269 text = self.dup
270
270
271 @urlrefs = {}
271 @urlrefs = {}
272 @shelf = []
272 @shelf = []
273 textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
273 textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
274 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
274 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
275 :inline_textile_code, :inline_textile_span]
275 :inline_textile_code, :inline_textile_span]
276 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
276 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
277 :block_markdown_bq, :block_markdown_lists,
277 :block_markdown_bq, :block_markdown_lists,
278 :inline_markdown_reflink, :inline_markdown_link]
278 :inline_markdown_reflink, :inline_markdown_link]
279 @rules = rules.collect do |rule|
279 @rules = rules.collect do |rule|
280 case rule
280 case rule
281 when :markdown
281 when :markdown
282 markdown_rules
282 markdown_rules
283 when :textile
283 when :textile
284 textile_rules
284 textile_rules
285 else
285 else
286 rule
286 rule
287 end
287 end
288 end.flatten
288 end.flatten
289
289
290 # standard clean up
290 # standard clean up
291 incoming_entities text
291 incoming_entities text
292 clean_white_space text
292 clean_white_space text
293
293
294 # start processor
294 # start processor
295 @pre_list = []
295 @pre_list = []
296 rip_offtags text
296 rip_offtags text
297 no_textile text
297 no_textile text
298 escape_html_tags text
298 escape_html_tags text
299 hard_break text
299 hard_break text
300 unless @lite_mode
300 unless @lite_mode
301 refs text
301 refs text
302 blocks text
302 blocks text
303 end
303 end
304 inline text
304 inline text
305 smooth_offtags text
305 smooth_offtags text
306
306
307 retrieve text
307 retrieve text
308
308
309 text.gsub!( /<\/?notextile>/, '' )
309 text.gsub!( /<\/?notextile>/, '' )
310 text.gsub!( /x%x%/, '&#38;' )
310 text.gsub!( /x%x%/, '&#38;' )
311 clean_html text if filter_html
311 clean_html text if filter_html
312 text.strip!
312 text.strip!
313 text
313 text
314
314
315 end
315 end
316
316
317 #######
317 #######
318 private
318 private
319 #######
319 #######
320 #
320 #
321 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
321 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
322 # (from PyTextile)
322 # (from PyTextile)
323 #
323 #
324 TEXTILE_TAGS =
324 TEXTILE_TAGS =
325
325
326 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
326 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
327 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
327 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
328 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
328 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
329 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
329 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
330 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
330 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
331
331
332 collect! do |a, b|
332 collect! do |a, b|
333 [a.chr, ( b.zero? and "" or "&#{ b };" )]
333 [a.chr, ( b.zero? and "" or "&#{ b };" )]
334 end
334 end
335
335
336 #
336 #
337 # Regular expressions to convert to HTML.
337 # Regular expressions to convert to HTML.
338 #
338 #
339 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
339 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
340 A_VLGN = /[\-^~]/
340 A_VLGN = /[\-^~]/
341 C_CLAS = '(?:\([^)]+\))'
341 C_CLAS = '(?:\([^)]+\))'
342 C_LNGE = '(?:\[[^\]]+\])'
342 C_LNGE = '(?:\[[^\]]+\])'
343 C_STYL = '(?:\{[^}]+\})'
343 C_STYL = '(?:\{[^}]+\})'
344 S_CSPN = '(?:\\\\\d+)'
344 S_CSPN = '(?:\\\\\d+)'
345 S_RSPN = '(?:/\d+)'
345 S_RSPN = '(?:/\d+)'
346 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
346 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
347 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
347 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
348 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
348 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
349 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
349 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
350 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
350 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
351 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
351 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
352 PUNCT_Q = Regexp::quote( '*-_+^~%' )
352 PUNCT_Q = Regexp::quote( '*-_+^~%' )
353 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
353 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
354
354
355 # Text markup tags, don't conflict with block tags
355 # Text markup tags, don't conflict with block tags
356 SIMPLE_HTML_TAGS = [
356 SIMPLE_HTML_TAGS = [
357 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
357 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
358 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
358 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
359 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
359 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
360 ]
360 ]
361
361
362 QTAGS = [
362 QTAGS = [
363 ['**', 'b', :limit],
363 ['**', 'b', :limit],
364 ['*', 'strong', :limit],
364 ['*', 'strong', :limit],
365 ['??', 'cite', :limit],
365 ['??', 'cite', :limit],
366 ['-', 'del', :limit],
366 ['-', 'del', :limit],
367 ['__', 'i', :limit],
367 ['__', 'i', :limit],
368 ['_', 'em', :limit],
368 ['_', 'em', :limit],
369 ['%', 'span', :limit],
369 ['%', 'span', :limit],
370 ['+', 'ins', :limit],
370 ['+', 'ins', :limit],
371 ['^', 'sup', :limit],
371 ['^', 'sup', :limit],
372 ['~', 'sub', :limit]
372 ['~', 'sub', :limit]
373 ]
373 ]
374 QTAGS.collect! do |rc, ht, rtype|
374 QTAGS.collect! do |rc, ht, rtype|
375 rcq = Regexp::quote rc
375 rcq = Regexp::quote rc
376 re =
376 re =
377 case rtype
377 case rtype
378 when :limit
378 when :limit
379 /(^|[>\s\(])
379 /(^|[>\s\(])
380 (#{rcq})
380 (#{rcq})
381 (#{C})
381 (#{C})
382 (?::(\S+?))?
382 (?::(\S+?))?
383 ([^\s\-].*?[^\s\-]|\w)
383 ([^\s\-].*?[^\s\-]|\w)
384 #{rcq}
384 #{rcq}
385 (?=[[:punct:]]|\s|\)|$)/x
385 (?=[[:punct:]]|\s|\)|$)/x
386 else
386 else
387 /(#{rcq})
387 /(#{rcq})
388 (#{C})
388 (#{C})
389 (?::(\S+))?
389 (?::(\S+))?
390 ([^\s\-].*?[^\s\-]|\w)
390 ([^\s\-].*?[^\s\-]|\w)
391 #{rcq}/xm
391 #{rcq}/xm
392 end
392 end
393 [rc, ht, re, rtype]
393 [rc, ht, re, rtype]
394 end
394 end
395
395
396 # Elements to handle
396 # Elements to handle
397 GLYPHS = [
397 GLYPHS = [
398 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
398 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
399 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
399 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
400 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
400 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
401 # [ /\'/, '&#8216;' ], # single opening
401 # [ /\'/, '&#8216;' ], # single opening
402 [ /</, '&lt;' ], # less-than
402 [ /</, '&lt;' ], # less-than
403 [ />/, '&gt;' ], # greater-than
403 [ />/, '&gt;' ], # greater-than
404 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
404 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
405 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
405 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
406 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
406 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
407 # [ /"/, '&#8220;' ], # double opening
407 # [ /"/, '&#8220;' ], # double opening
408 [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
408 [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
409 [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
409 [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
410 [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
410 [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
411 [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
411 [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
412 [ /\s->\s/, ' &rarr; ' ], # right arrow
412 [ /\s->\s/, ' &rarr; ' ], # right arrow
413 [ /\s-\s/, ' &#8211; ' ], # en dash
413 [ /\s-\s/, ' &#8211; ' ], # en dash
414 [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
414 [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
415 [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
415 [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
416 [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
416 [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
417 [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
417 [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
418 ]
418 ]
419
419
420 H_ALGN_VALS = {
420 H_ALGN_VALS = {
421 '<' => 'left',
421 '<' => 'left',
422 '=' => 'center',
422 '=' => 'center',
423 '>' => 'right',
423 '>' => 'right',
424 '<>' => 'justify'
424 '<>' => 'justify'
425 }
425 }
426
426
427 V_ALGN_VALS = {
427 V_ALGN_VALS = {
428 '^' => 'top',
428 '^' => 'top',
429 '-' => 'middle',
429 '-' => 'middle',
430 '~' => 'bottom'
430 '~' => 'bottom'
431 }
431 }
432
432
433 #
433 #
434 # Flexible HTML escaping
434 # Flexible HTML escaping
435 #
435 #
436 def htmlesc( str, mode )
436 def htmlesc( str, mode )
437 str.gsub!( '&', '&amp;' )
437 str.gsub!( '&', '&amp;' )
438 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
438 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
439 str.gsub!( "'", '&#039;' ) if mode == :Quotes
439 str.gsub!( "'", '&#039;' ) if mode == :Quotes
440 str.gsub!( '<', '&lt;')
440 str.gsub!( '<', '&lt;')
441 str.gsub!( '>', '&gt;')
441 str.gsub!( '>', '&gt;')
442 end
442 end
443
443
444 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
444 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
445 def pgl( text )
445 def pgl( text )
446 GLYPHS.each do |re, resub, tog|
446 GLYPHS.each do |re, resub, tog|
447 next if tog and method( tog ).call
447 next if tog and method( tog ).call
448 text.gsub! re, resub
448 text.gsub! re, resub
449 end
449 end
450 end
450 end
451
451
452 # Parses Textile attribute lists and builds an HTML attribute string
452 # Parses Textile attribute lists and builds an HTML attribute string
453 def pba( text_in, element = "" )
453 def pba( text_in, element = "" )
454
454
455 return '' unless text_in
455 return '' unless text_in
456
456
457 style = []
457 style = []
458 text = text_in.dup
458 text = text_in.dup
459 if element == 'td'
459 if element == 'td'
460 colspan = $1 if text =~ /\\(\d+)/
460 colspan = $1 if text =~ /\\(\d+)/
461 rowspan = $1 if text =~ /\/(\d+)/
461 rowspan = $1 if text =~ /\/(\d+)/
462 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
462 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
463 end
463 end
464
464
465 style << "#{ $1 };" if not filter_styles and
465 style << "#{ $1 };" if not filter_styles and
466 text.sub!( /\{([^}]*)\}/, '' )
466 text.sub!( /\{([^}]*)\}/, '' )
467
467
468 lang = $1 if
468 lang = $1 if
469 text.sub!( /\[([^)]+?)\]/, '' )
469 text.sub!( /\[([^)]+?)\]/, '' )
470
470
471 cls = $1 if
471 cls = $1 if
472 text.sub!( /\(([^()]+?)\)/, '' )
472 text.sub!( /\(([^()]+?)\)/, '' )
473
473
474 style << "padding-left:#{ $1.length }em;" if
474 style << "padding-left:#{ $1.length }em;" if
475 text.sub!( /([(]+)/, '' )
475 text.sub!( /([(]+)/, '' )
476
476
477 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
477 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
478
478
479 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
479 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
480
480
481 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
481 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
482
482
483 atts = ''
483 atts = ''
484 atts << " style=\"#{ style.join }\"" unless style.empty?
484 atts << " style=\"#{ style.join }\"" unless style.empty?
485 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
485 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
486 atts << " lang=\"#{ lang }\"" if lang
486 atts << " lang=\"#{ lang }\"" if lang
487 atts << " id=\"#{ id }\"" if id
487 atts << " id=\"#{ id }\"" if id
488 atts << " colspan=\"#{ colspan }\"" if colspan
488 atts << " colspan=\"#{ colspan }\"" if colspan
489 atts << " rowspan=\"#{ rowspan }\"" if rowspan
489 atts << " rowspan=\"#{ rowspan }\"" if rowspan
490
490
491 atts
491 atts
492 end
492 end
493
493
494 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
494 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
495
495
496 # Parses a Textile table block, building HTML from the result.
496 # Parses a Textile table block, building HTML from the result.
497 def block_textile_table( text )
497 def block_textile_table( text )
498 text.gsub!( TABLE_RE ) do |matches|
498 text.gsub!( TABLE_RE ) do |matches|
499
499
500 tatts, fullrow = $~[1..2]
500 tatts, fullrow = $~[1..2]
501 tatts = pba( tatts, 'table' )
501 tatts = pba( tatts, 'table' )
502 tatts = shelve( tatts ) if tatts
502 tatts = shelve( tatts ) if tatts
503 rows = []
503 rows = []
504
504
505 fullrow.
505 fullrow.
506 split( /\|$/m ).
506 split( /\|$/m ).
507 delete_if { |x| x.empty? }.
507 delete_if { |x| x.empty? }.
508 each do |row|
508 each do |row|
509
509
510 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
510 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
511
511
512 cells = []
512 cells = []
513 #row.split( /\(?!\[\[[^\]])|(?![^\[]\]\])/ ).each do |cell|
513 #row.split( /\(?!\[\[[^\]])|(?![^\[]\]\])/ ).each do |cell|
514 row.split( /\|(?![^\[\|]*\]\])/ ).each do |cell|
514 row.split( /\|(?![^\[\|]*\]\])/ ).each do |cell|
515 ctyp = 'd'
515 ctyp = 'd'
516 ctyp = 'h' if cell =~ /^_/
516 ctyp = 'h' if cell =~ /^_/
517
517
518 catts = ''
518 catts = ''
519 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
519 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
520
520
521 unless cell.strip.empty?
521 unless cell.strip.empty?
522 catts = shelve( catts ) if catts
522 catts = shelve( catts ) if catts
523 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
523 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
524 end
524 end
525 end
525 end
526 ratts = shelve( ratts ) if ratts
526 ratts = shelve( ratts ) if ratts
527 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
527 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
528 end
528 end
529 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
529 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
530 end
530 end
531 end
531 end
532
532
533 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
533 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
534 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
534 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
535
535
536 # Parses Textile lists and generates HTML
536 # Parses Textile lists and generates HTML
537 def block_textile_lists( text )
537 def block_textile_lists( text )
538 text.gsub!( LISTS_RE ) do |match|
538 text.gsub!( LISTS_RE ) do |match|
539 lines = match.split( /\n/ )
539 lines = match.split( /\n/ )
540 last_line = -1
540 last_line = -1
541 depth = []
541 depth = []
542 lines.each_with_index do |line, line_id|
542 lines.each_with_index do |line, line_id|
543 if line =~ LISTS_CONTENT_RE
543 if line =~ LISTS_CONTENT_RE
544 tl,atts,content = $~[1..3]
544 tl,atts,content = $~[1..3]
545 if depth.last
545 if depth.last
546 if depth.last.length > tl.length
546 if depth.last.length > tl.length
547 (depth.length - 1).downto(0) do |i|
547 (depth.length - 1).downto(0) do |i|
548 break if depth[i].length == tl.length
548 break if depth[i].length == tl.length
549 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
549 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
550 depth.pop
550 depth.pop
551 end
551 end
552 end
552 end
553 if depth.last and depth.last.length == tl.length
553 if depth.last and depth.last.length == tl.length
554 lines[line_id - 1] << '</li>'
554 lines[line_id - 1] << '</li>'
555 end
555 end
556 end
556 end
557 unless depth.last == tl
557 unless depth.last == tl
558 depth << tl
558 depth << tl
559 atts = pba( atts )
559 atts = pba( atts )
560 atts = shelve( atts ) if atts
560 atts = shelve( atts ) if atts
561 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
561 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
562 else
562 else
563 lines[line_id] = "\t\t<li>#{ content }"
563 lines[line_id] = "\t\t<li>#{ content }"
564 end
564 end
565 last_line = line_id
565 last_line = line_id
566
566
567 else
567 else
568 last_line = line_id
568 last_line = line_id
569 end
569 end
570 if line_id - last_line > 1 or line_id == lines.length - 1
570 if line_id - last_line > 1 or line_id == lines.length - 1
571 depth.delete_if do |v|
571 depth.delete_if do |v|
572 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
572 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
573 end
573 end
574 end
574 end
575 end
575 end
576 lines.join( "\n" )
576 lines.join( "\n" )
577 end
577 end
578 end
578 end
579
579
580 CODE_RE = /(\W)
580 CODE_RE = /(\W)
581 @
581 @
582 (?:\|(\w+?)\|)?
582 (?:\|(\w+?)\|)?
583 (.+?)
583 (.+?)
584 @
584 @
585 (?=\W)/x
585 (?=\W)/x
586
586
587 def inline_textile_code( text )
587 def inline_textile_code( text )
588 text.gsub!( CODE_RE ) do |m|
588 text.gsub!( CODE_RE ) do |m|
589 before,lang,code,after = $~[1..4]
589 before,lang,code,after = $~[1..4]
590 lang = " lang=\"#{ lang }\"" if lang
590 lang = " lang=\"#{ lang }\"" if lang
591 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }" )
591 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }" )
592 end
592 end
593 end
593 end
594
594
595 def lT( text )
595 def lT( text )
596 text =~ /\#$/ ? 'o' : 'u'
596 text =~ /\#$/ ? 'o' : 'u'
597 end
597 end
598
598
599 def hard_break( text )
599 def hard_break( text )
600 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
600 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
601 end
601 end
602
602
603 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
603 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
604
604
605 def blocks( text, deep_code = false )
605 def blocks( text, deep_code = false )
606 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
606 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
607 plain = blk !~ /\A[#*> ]/
607 plain = blk !~ /\A[#*> ]/
608
608
609 # skip blocks that are complex HTML
609 # skip blocks that are complex HTML
610 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
610 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
611 blk
611 blk
612 else
612 else
613 # search for indentation levels
613 # search for indentation levels
614 blk.strip!
614 blk.strip!
615 if blk.empty?
615 if blk.empty?
616 blk
616 blk
617 else
617 else
618 code_blk = nil
618 code_blk = nil
619 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
619 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
620 flush_left iblk
620 flush_left iblk
621 blocks iblk, plain
621 blocks iblk, plain
622 iblk.gsub( /^(\S)/, "\t\\1" )
622 iblk.gsub( /^(\S)/, "\t\\1" )
623 if plain
623 if plain
624 code_blk = iblk; ""
624 code_blk = iblk; ""
625 else
625 else
626 iblk
626 iblk
627 end
627 end
628 end
628 end
629
629
630 block_applied = 0
630 block_applied = 0
631 @rules.each do |rule_name|
631 @rules.each do |rule_name|
632 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
632 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
633 end
633 end
634 if block_applied.zero?
634 if block_applied.zero?
635 if deep_code
635 if deep_code
636 blk = "\t<pre><code>#{ blk }</code></pre>"
636 blk = "\t<pre><code>#{ blk }</code></pre>"
637 else
637 else
638 blk = "\t<p>#{ blk }</p>"
638 blk = "\t<p>#{ blk }</p>"
639 end
639 end
640 end
640 end
641 # hard_break blk
641 # hard_break blk
642 blk + "\n#{ code_blk }"
642 blk + "\n#{ code_blk }"
643 end
643 end
644 end
644 end
645
645
646 end.join( "\n\n" ) )
646 end.join( "\n\n" ) )
647 end
647 end
648
648
649 def textile_bq( tag, atts, cite, content )
649 def textile_bq( tag, atts, cite, content )
650 cite, cite_title = check_refs( cite )
650 cite, cite_title = check_refs( cite )
651 cite = " cite=\"#{ cite }\"" if cite
651 cite = " cite=\"#{ cite }\"" if cite
652 atts = shelve( atts ) if atts
652 atts = shelve( atts ) if atts
653 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
653 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
654 end
654 end
655
655
656 def textile_p( tag, atts, cite, content )
656 def textile_p( tag, atts, cite, content )
657 atts = shelve( atts ) if atts
657 atts = shelve( atts ) if atts
658 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
658 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
659 end
659 end
660
660
661 alias textile_h1 textile_p
661 alias textile_h1 textile_p
662 alias textile_h2 textile_p
662 alias textile_h2 textile_p
663 alias textile_h3 textile_p
663 alias textile_h3 textile_p
664 alias textile_h4 textile_p
664 alias textile_h4 textile_p
665 alias textile_h5 textile_p
665 alias textile_h5 textile_p
666 alias textile_h6 textile_p
666 alias textile_h6 textile_p
667
667
668 def textile_fn_( tag, num, atts, cite, content )
668 def textile_fn_( tag, num, atts, cite, content )
669 atts << " id=\"fn#{ num }\""
669 atts << " id=\"fn#{ num }\""
670 content = "<sup>#{ num }</sup> #{ content }"
670 content = "<sup>#{ num }</sup> #{ content }"
671 atts = shelve( atts ) if atts
671 atts = shelve( atts ) if atts
672 "\t<p#{ atts }>#{ content }</p>"
672 "\t<p#{ atts }>#{ content }</p>"
673 end
673 end
674
674
675 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
675 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
676
676
677 def block_textile_prefix( text )
677 def block_textile_prefix( text )
678 if text =~ BLOCK_RE
678 if text =~ BLOCK_RE
679 tag,tagpre,num,atts,cite,content = $~[1..6]
679 tag,tagpre,num,atts,cite,content = $~[1..6]
680 atts = pba( atts )
680 atts = pba( atts )
681
681
682 # pass to prefix handler
682 # pass to prefix handler
683 if respond_to? "textile_#{ tag }", true
683 if respond_to? "textile_#{ tag }", true
684 text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
684 text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
685 elsif respond_to? "textile_#{ tagpre }_", true
685 elsif respond_to? "textile_#{ tagpre }_", true
686 text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
686 text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
687 end
687 end
688 end
688 end
689 end
689 end
690
690
691 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
691 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
692 def block_markdown_setext( text )
692 def block_markdown_setext( text )
693 if text =~ SETEXT_RE
693 if text =~ SETEXT_RE
694 tag = if $2 == "="; "h1"; else; "h2"; end
694 tag = if $2 == "="; "h1"; else; "h2"; end
695 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
695 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
696 blocks cont
696 blocks cont
697 text.replace( blk + cont )
697 text.replace( blk + cont )
698 end
698 end
699 end
699 end
700
700
701 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
701 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
702 [ ]*
702 [ ]*
703 (.+?) # $2 = Header text
703 (.+?) # $2 = Header text
704 [ ]*
704 [ ]*
705 \#* # optional closing #'s (not counted)
705 \#* # optional closing #'s (not counted)
706 $/x
706 $/x
707 def block_markdown_atx( text )
707 def block_markdown_atx( text )
708 if text =~ ATX_RE
708 if text =~ ATX_RE
709 tag = "h#{ $1.length }"
709 tag = "h#{ $1.length }"
710 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
710 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
711 blocks cont
711 blocks cont
712 text.replace( blk + cont )
712 text.replace( blk + cont )
713 end
713 end
714 end
714 end
715
715
716 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
716 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
717
717
718 def block_markdown_bq( text )
718 def block_markdown_bq( text )
719 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
719 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
720 blk.gsub!( /^ *> ?/, '' )
720 blk.gsub!( /^ *> ?/, '' )
721 flush_left blk
721 flush_left blk
722 blocks blk
722 blocks blk
723 blk.gsub!( /^(\S)/, "\t\\1" )
723 blk.gsub!( /^(\S)/, "\t\\1" )
724 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
724 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
725 end
725 end
726 end
726 end
727
727
728 MARKDOWN_RULE_RE = /^(#{
728 MARKDOWN_RULE_RE = /^(#{
729 ['*', '-', '_'].collect { |ch| '( ?' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
729 ['*', '-', '_'].collect { |ch| '( ?' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
730 })$/
730 })$/
731
731
732 def block_markdown_rule( text )
732 def block_markdown_rule( text )
733 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
733 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
734 "<hr />"
734 "<hr />"
735 end
735 end
736 end
736 end
737
737
738 # XXX TODO XXX
738 # XXX TODO XXX
739 def block_markdown_lists( text )
739 def block_markdown_lists( text )
740 end
740 end
741
741
742 def inline_textile_span( text )
742 def inline_textile_span( text )
743 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
743 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
744 text.gsub!( qtag_re ) do |m|
744 text.gsub!( qtag_re ) do |m|
745
745
746 case rtype
746 case rtype
747 when :limit
747 when :limit
748 sta,qtag,atts,cite,content = $~[1..5]
748 sta,qtag,atts,cite,content = $~[1..5]
749 else
749 else
750 qtag,atts,cite,content = $~[1..4]
750 qtag,atts,cite,content = $~[1..4]
751 sta = ''
751 sta = ''
752 end
752 end
753 atts = pba( atts )
753 atts = pba( atts )
754 atts << " cite=\"#{ cite }\"" if cite
754 atts << " cite=\"#{ cite }\"" if cite
755 atts = shelve( atts ) if atts
755 atts = shelve( atts ) if atts
756
756
757 "#{ sta }<#{ ht }#{ atts }>#{ content }</#{ ht }>"
757 "#{ sta }<#{ ht }#{ atts }>#{ content }</#{ ht }>"
758
758
759 end
759 end
760 end
760 end
761 end
761 end
762
762
763 LINK_RE = /
763 LINK_RE = /
764 ([\s\[{(]|[#{PUNCT}])? # $pre
764 ([\s\[{(]|[#{PUNCT}])? # $pre
765 " # start
765 " # start
766 (#{C}) # $atts
766 (#{C}) # $atts
767 ([^"\n]+?) # $text
767 ([^"\n]+?) # $text
768 \s?
768 \s?
769 (?:\(([^)]+?)\)(?="))? # $title
769 (?:\(([^)]+?)\)(?="))? # $title
770 ":
770 ":
771 (\S+?) # $url
771 (\S+?) # $url
772 (\/)? # $slash
772 (\/)? # $slash
773 ([^\w\/;]*?) # $post
773 ([^\w\/;]*?) # $post
774 (?=<|\s|$)
774 (?=<|\s|$)
775 /x
775 /x
776
776
777 def inline_textile_link( text )
777 def inline_textile_link( text )
778 text.gsub!( LINK_RE ) do |m|
778 text.gsub!( LINK_RE ) do |m|
779 pre,atts,text,title,url,slash,post = $~[1..7]
779 pre,atts,text,title,url,slash,post = $~[1..7]
780
780
781 url, url_title = check_refs( url )
781 url, url_title = check_refs( url )
782 title ||= url_title
782 title ||= url_title
783
783
784 atts = pba( atts )
784 atts = pba( atts )
785 atts = " href=\"#{ url }#{ slash }\"#{ atts }"
785 atts = " href=\"#{ url }#{ slash }\"#{ atts }"
786 atts << " title=\"#{ title }\"" if title
786 atts << " title=\"#{ title }\"" if title
787 atts = shelve( atts ) if atts
787 atts = shelve( atts ) if atts
788
788
789 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
789 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
790
790
791 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
791 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
792 end
792 end
793 end
793 end
794
794
795 MARKDOWN_REFLINK_RE = /
795 MARKDOWN_REFLINK_RE = /
796 \[([^\[\]]+)\] # $text
796 \[([^\[\]]+)\] # $text
797 [ ]? # opt. space
797 [ ]? # opt. space
798 (?:\n[ ]*)? # one optional newline followed by spaces
798 (?:\n[ ]*)? # one optional newline followed by spaces
799 \[(.*?)\] # $id
799 \[(.*?)\] # $id
800 /x
800 /x
801
801
802 def inline_markdown_reflink( text )
802 def inline_markdown_reflink( text )
803 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
803 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
804 text, id = $~[1..2]
804 text, id = $~[1..2]
805
805
806 if id.empty?
806 if id.empty?
807 url, title = check_refs( text )
807 url, title = check_refs( text )
808 else
808 else
809 url, title = check_refs( id )
809 url, title = check_refs( id )
810 end
810 end
811
811
812 atts = " href=\"#{ url }\""
812 atts = " href=\"#{ url }\""
813 atts << " title=\"#{ title }\"" if title
813 atts << " title=\"#{ title }\"" if title
814 atts = shelve( atts )
814 atts = shelve( atts )
815
815
816 "<a#{ atts }>#{ text }</a>"
816 "<a#{ atts }>#{ text }</a>"
817 end
817 end
818 end
818 end
819
819
820 MARKDOWN_LINK_RE = /
820 MARKDOWN_LINK_RE = /
821 \[([^\[\]]+)\] # $text
821 \[([^\[\]]+)\] # $text
822 \( # open paren
822 \( # open paren
823 [ \t]* # opt space
823 [ \t]* # opt space
824 <?(.+?)>? # $href
824 <?(.+?)>? # $href
825 [ \t]* # opt space
825 [ \t]* # opt space
826 (?: # whole title
826 (?: # whole title
827 (['"]) # $quote
827 (['"]) # $quote
828 (.*?) # $title
828 (.*?) # $title
829 \3 # matching quote
829 \3 # matching quote
830 )? # title is optional
830 )? # title is optional
831 \)
831 \)
832 /x
832 /x
833
833
834 def inline_markdown_link( text )
834 def inline_markdown_link( text )
835 text.gsub!( MARKDOWN_LINK_RE ) do |m|
835 text.gsub!( MARKDOWN_LINK_RE ) do |m|
836 text, url, quote, title = $~[1..4]
836 text, url, quote, title = $~[1..4]
837
837
838 atts = " href=\"#{ url }\""
838 atts = " href=\"#{ url }\""
839 atts << " title=\"#{ title }\"" if title
839 atts << " title=\"#{ title }\"" if title
840 atts = shelve( atts )
840 atts = shelve( atts )
841
841
842 "<a#{ atts }>#{ text }</a>"
842 "<a#{ atts }>#{ text }</a>"
843 end
843 end
844 end
844 end
845
845
846 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
846 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
847 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
847 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
848
848
849 def refs( text )
849 def refs( text )
850 @rules.each do |rule_name|
850 @rules.each do |rule_name|
851 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
851 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
852 end
852 end
853 end
853 end
854
854
855 def refs_textile( text )
855 def refs_textile( text )
856 text.gsub!( TEXTILE_REFS_RE ) do |m|
856 text.gsub!( TEXTILE_REFS_RE ) do |m|
857 flag, url = $~[2..3]
857 flag, url = $~[2..3]
858 @urlrefs[flag.downcase] = [url, nil]
858 @urlrefs[flag.downcase] = [url, nil]
859 nil
859 nil
860 end
860 end
861 end
861 end
862
862
863 def refs_markdown( text )
863 def refs_markdown( text )
864 text.gsub!( MARKDOWN_REFS_RE ) do |m|
864 text.gsub!( MARKDOWN_REFS_RE ) do |m|
865 flag, url = $~[2..3]
865 flag, url = $~[2..3]
866 title = $~[6]
866 title = $~[6]
867 @urlrefs[flag.downcase] = [url, title]
867 @urlrefs[flag.downcase] = [url, title]
868 nil
868 nil
869 end
869 end
870 end
870 end
871
871
872 def check_refs( text )
872 def check_refs( text )
873 ret = @urlrefs[text.downcase] if text
873 ret = @urlrefs[text.downcase] if text
874 ret || [text, nil]
874 ret || [text, nil]
875 end
875 end
876
876
877 IMAGE_RE = /
877 IMAGE_RE = /
878 (<p>|.|^) # start of line?
878 (<p>|.|^) # start of line?
879 \! # opening
879 \! # opening
880 (\<|\=|\>)? # optional alignment atts
880 (\<|\=|\>)? # optional alignment atts
881 (#{C}) # optional style,class atts
881 (#{C}) # optional style,class atts
882 (?:\. )? # optional dot-space
882 (?:\. )? # optional dot-space
883 ([^\s(!]+?) # presume this is the src
883 ([^\s(!]+?) # presume this is the src
884 \s? # optional space
884 \s? # optional space
885 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
885 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
886 \! # closing
886 \! # closing
887 (?::#{ HYPERLINK })? # optional href
887 (?::#{ HYPERLINK })? # optional href
888 /x
888 /x
889
889
890 def inline_textile_image( text )
890 def inline_textile_image( text )
891 text.gsub!( IMAGE_RE ) do |m|
891 text.gsub!( IMAGE_RE ) do |m|
892 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
892 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
893 atts = pba( atts )
893 atts = pba( atts )
894 atts = " src=\"#{ url }\"#{ atts }"
894 atts = " src=\"#{ url }\"#{ atts }"
895 atts << " title=\"#{ title }\"" if title
895 atts << " title=\"#{ title }\"" if title
896 atts << " alt=\"#{ title }\""
896 atts << " alt=\"#{ title }\""
897 # size = @getimagesize($url);
897 # size = @getimagesize($url);
898 # if($size) $atts.= " $size[3]";
898 # if($size) $atts.= " $size[3]";
899
899
900 href, alt_title = check_refs( href ) if href
900 href, alt_title = check_refs( href ) if href
901 url, url_title = check_refs( url )
901 url, url_title = check_refs( url )
902
902
903 out = ''
903 out = ''
904 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
904 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
905 out << "<img#{ shelve( atts ) } />"
905 out << "<img#{ shelve( atts ) } />"
906 out << "</a>#{ href_a1 }#{ href_a2 }" if href
906 out << "</a>#{ href_a1 }#{ href_a2 }" if href
907
907
908 if algn
908 if algn
909 algn = h_align( algn )
909 algn = h_align( algn )
910 if stln == "<p>"
910 if stln == "<p>"
911 out = "<p style=\"float:#{ algn }\">#{ out }"
911 out = "<p style=\"float:#{ algn }\">#{ out }"
912 else
912 else
913 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
913 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
914 end
914 end
915 else
915 else
916 out = stln + out
916 out = stln + out
917 end
917 end
918
918
919 out
919 out
920 end
920 end
921 end
921 end
922
922
923 def shelve( val )
923 def shelve( val )
924 @shelf << val
924 @shelf << val
925 " :redsh##{ @shelf.length }:"
925 " :redsh##{ @shelf.length }:"
926 end
926 end
927
927
928 def retrieve( text )
928 def retrieve( text )
929 @shelf.each_with_index do |r, i|
929 @shelf.each_with_index do |r, i|
930 text.gsub!( " :redsh##{ i + 1 }:", r )
930 text.gsub!( " :redsh##{ i + 1 }:", r )
931 end
931 end
932 end
932 end
933
933
934 def incoming_entities( text )
934 def incoming_entities( text )
935 ## turn any incoming ampersands into a dummy character for now.
935 ## turn any incoming ampersands into a dummy character for now.
936 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
936 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
937 ## implying an incoming html entity, to be skipped
937 ## implying an incoming html entity, to be skipped
938
938
939 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
939 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
940 end
940 end
941
941
942 def no_textile( text )
942 def no_textile( text )
943 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
943 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
944 '\1<notextile>\2</notextile>\3' )
944 '\1<notextile>\2</notextile>\3' )
945 text.gsub!( /^ *==([^=]+.*?)==/m,
945 text.gsub!( /^ *==([^=]+.*?)==/m,
946 '\1<notextile>\2</notextile>\3' )
946 '\1<notextile>\2</notextile>\3' )
947 end
947 end
948
948
949 def clean_white_space( text )
949 def clean_white_space( text )
950 # normalize line breaks
950 # normalize line breaks
951 text.gsub!( /\r\n/, "\n" )
951 text.gsub!( /\r\n/, "\n" )
952 text.gsub!( /\r/, "\n" )
952 text.gsub!( /\r/, "\n" )
953 text.gsub!( /\t/, ' ' )
953 text.gsub!( /\t/, ' ' )
954 text.gsub!( /^ +$/, '' )
954 text.gsub!( /^ +$/, '' )
955 text.gsub!( /\n{3,}/, "\n\n" )
955 text.gsub!( /\n{3,}/, "\n\n" )
956 text.gsub!( /"$/, "\" " )
956 text.gsub!( /"$/, "\" " )
957
957
958 # if entire document is indented, flush
958 # if entire document is indented, flush
959 # to the left side
959 # to the left side
960 flush_left text
960 flush_left text
961 end
961 end
962
962
963 def flush_left( text )
963 def flush_left( text )
964 indt = 0
964 indt = 0
965 if text =~ /^ /
965 if text =~ /^ /
966 while text !~ /^ {#{indt}}\S/
966 while text !~ /^ {#{indt}}\S/
967 indt += 1
967 indt += 1
968 end unless text.empty?
968 end unless text.empty?
969 if indt.nonzero?
969 if indt.nonzero?
970 text.gsub!( /^ {#{indt}}/, '' )
970 text.gsub!( /^ {#{indt}}/, '' )
971 end
971 end
972 end
972 end
973 end
973 end
974
974
975 def footnote_ref( text )
975 def footnote_ref( text )
976 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
976 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
977 '<sup><a href="#fn\1">\1</a></sup>\2' )
977 '<sup><a href="#fn\1">\1</a></sup>\2' )
978 end
978 end
979
979
980 OFFTAGS = /(code|pre|kbd|notextile)/
980 OFFTAGS = /(code|pre|kbd|notextile)/
981 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi
981 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi
982 OFFTAG_OPEN = /<#{ OFFTAGS }/
982 OFFTAG_OPEN = /<#{ OFFTAGS }/
983 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
983 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
984 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
984 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
985 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
985 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
986
986
987 def glyphs_textile( text, level = 0 )
987 def glyphs_textile( text, level = 0 )
988 if text !~ HASTAG_MATCH
988 if text !~ HASTAG_MATCH
989 pgl text
989 pgl text
990 footnote_ref text
990 footnote_ref text
991 else
991 else
992 codepre = 0
992 codepre = 0
993 text.gsub!( ALLTAG_MATCH ) do |line|
993 text.gsub!( ALLTAG_MATCH ) do |line|
994 ## matches are off if we're between <code>, <pre> etc.
994 ## matches are off if we're between <code>, <pre> etc.
995 if $1
995 if $1
996 if line =~ OFFTAG_OPEN
996 if line =~ OFFTAG_OPEN
997 codepre += 1
997 codepre += 1
998 elsif line =~ OFFTAG_CLOSE
998 elsif line =~ OFFTAG_CLOSE
999 codepre -= 1
999 codepre -= 1
1000 codepre = 0 if codepre < 0
1000 codepre = 0 if codepre < 0
1001 end
1001 end
1002 elsif codepre.zero?
1002 elsif codepre.zero?
1003 glyphs_textile( line, level + 1 )
1003 glyphs_textile( line, level + 1 )
1004 else
1004 else
1005 htmlesc( line, :NoQuotes )
1005 htmlesc( line, :NoQuotes )
1006 end
1006 end
1007 # p [level, codepre, line]
1007 # p [level, codepre, line]
1008
1008
1009 line
1009 line
1010 end
1010 end
1011 end
1011 end
1012 end
1012 end
1013
1013
1014 def rip_offtags( text )
1014 def rip_offtags( text )
1015 if text =~ /<.*>/
1015 if text =~ /<.*>/
1016 ## strip and encode <pre> content
1016 ## strip and encode <pre> content
1017 codepre, used_offtags = 0, {}
1017 codepre, used_offtags = 0, {}
1018 text.gsub!( OFFTAG_MATCH ) do |line|
1018 text.gsub!( OFFTAG_MATCH ) do |line|
1019 if $3
1019 if $3
1020 offtag, aftertag = $4, $5
1020 offtag, aftertag = $4, $5
1021 codepre += 1
1021 codepre += 1
1022 used_offtags[offtag] = true
1022 used_offtags[offtag] = true
1023 if codepre - used_offtags.length > 0
1023 if codepre - used_offtags.length > 0
1024 htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
1024 htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
1025 @pre_list.last << line
1025 @pre_list.last << line
1026 line = ""
1026 line = ""
1027 else
1027 else
1028 htmlesc( aftertag, :NoQuotes ) if aftertag and not used_offtags['notextile']
1028 htmlesc( aftertag, :NoQuotes ) if aftertag and not used_offtags['notextile']
1029 line = "<redpre##{ @pre_list.length }>"
1029 line = "<redpre##{ @pre_list.length }>"
1030 @pre_list << "#{ $3 }#{ aftertag }"
1030 @pre_list << "#{ $3 }#{ aftertag }"
1031 end
1031 end
1032 elsif $1 and codepre > 0
1032 elsif $1 and codepre > 0
1033 if codepre - used_offtags.length > 0
1033 if codepre - used_offtags.length > 0
1034 htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
1034 htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
1035 @pre_list.last << line
1035 @pre_list.last << line
1036 line = ""
1036 line = ""
1037 end
1037 end
1038 codepre -= 1 unless codepre.zero?
1038 codepre -= 1 unless codepre.zero?
1039 used_offtags = {} if codepre.zero?
1039 used_offtags = {} if codepre.zero?
1040 end
1040 end
1041 line
1041 line
1042 end
1042 end
1043 end
1043 end
1044 text
1044 text
1045 end
1045 end
1046
1046
1047 def smooth_offtags( text )
1047 def smooth_offtags( text )
1048 unless @pre_list.empty?
1048 unless @pre_list.empty?
1049 ## replace <pre> content
1049 ## replace <pre> content
1050 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1050 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1051 end
1051 end
1052 end
1052 end
1053
1053
1054 def inline( text )
1054 def inline( text )
1055 [/^inline_/, /^glyphs_/].each do |meth_re|
1055 [/^inline_/, /^glyphs_/].each do |meth_re|
1056 @rules.each do |rule_name|
1056 @rules.each do |rule_name|
1057 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1057 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1058 end
1058 end
1059 end
1059 end
1060 end
1060 end
1061
1061
1062 def h_align( text )
1062 def h_align( text )
1063 H_ALGN_VALS[text]
1063 H_ALGN_VALS[text]
1064 end
1064 end
1065
1065
1066 def v_align( text )
1066 def v_align( text )
1067 V_ALGN_VALS[text]
1067 V_ALGN_VALS[text]
1068 end
1068 end
1069
1069
1070 def textile_popup_help( name, windowW, windowH )
1070 def textile_popup_help( name, windowW, windowH )
1071 ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
1071 ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
1072 end
1072 end
1073
1073
1074 # HTML cleansing stuff
1074 # HTML cleansing stuff
1075 BASIC_TAGS = {
1075 BASIC_TAGS = {
1076 'a' => ['href', 'title'],
1076 'a' => ['href', 'title'],
1077 'img' => ['src', 'alt', 'title'],
1077 'img' => ['src', 'alt', 'title'],
1078 'br' => [],
1078 'br' => [],
1079 'i' => nil,
1079 'i' => nil,
1080 'u' => nil,
1080 'u' => nil,
1081 'b' => nil,
1081 'b' => nil,
1082 'pre' => nil,
1082 'pre' => nil,
1083 'kbd' => nil,
1083 'kbd' => nil,
1084 'code' => ['lang'],
1084 'code' => ['lang'],
1085 'cite' => nil,
1085 'cite' => nil,
1086 'strong' => nil,
1086 'strong' => nil,
1087 'em' => nil,
1087 'em' => nil,
1088 'ins' => nil,
1088 'ins' => nil,
1089 'sup' => nil,
1089 'sup' => nil,
1090 'sub' => nil,
1090 'sub' => nil,
1091 'del' => nil,
1091 'del' => nil,
1092 'table' => nil,
1092 'table' => nil,
1093 'tr' => nil,
1093 'tr' => nil,
1094 'td' => ['colspan', 'rowspan'],
1094 'td' => ['colspan', 'rowspan'],
1095 'th' => nil,
1095 'th' => nil,
1096 'ol' => nil,
1096 'ol' => nil,
1097 'ul' => nil,
1097 'ul' => nil,
1098 'li' => nil,
1098 'li' => nil,
1099 'p' => nil,
1099 'p' => nil,
1100 'h1' => nil,
1100 'h1' => nil,
1101 'h2' => nil,
1101 'h2' => nil,
1102 'h3' => nil,
1102 'h3' => nil,
1103 'h4' => nil,
1103 'h4' => nil,
1104 'h5' => nil,
1104 'h5' => nil,
1105 'h6' => nil,
1105 'h6' => nil,
1106 'blockquote' => ['cite']
1106 'blockquote' => ['cite']
1107 }
1107 }
1108
1108
1109 def clean_html( text, tags = BASIC_TAGS )
1109 def clean_html( text, tags = BASIC_TAGS )
1110 text.gsub!( /<!\[CDATA\[/, '' )
1110 text.gsub!( /<!\[CDATA\[/, '' )
1111 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1111 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1112 raw = $~
1112 raw = $~
1113 tag = raw[2].downcase
1113 tag = raw[2].downcase
1114 if tags.has_key? tag
1114 if tags.has_key? tag
1115 pcs = [tag]
1115 pcs = [tag]
1116 tags[tag].each do |prop|
1116 tags[tag].each do |prop|
1117 ['"', "'", ''].each do |q|
1117 ['"', "'", ''].each do |q|
1118 q2 = ( q != '' ? q : '\s' )
1118 q2 = ( q != '' ? q : '\s' )
1119 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1119 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1120 attrv = $1
1120 attrv = $1
1121 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1121 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1122 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1122 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1123 break
1123 break
1124 end
1124 end
1125 end
1125 end
1126 end if tags[tag]
1126 end if tags[tag]
1127 "<#{raw[1]}#{pcs.join " "}>"
1127 "<#{raw[1]}#{pcs.join " "}>"
1128 else
1128 else
1129 " "
1129 " "
1130 end
1130 end
1131 end
1131 end
1132 end
1132 end
1133
1133
1134 ALLOWED_TAGS = %w(redpre pre code)
1134 ALLOWED_TAGS = %w(redpre pre code notextile)
1135
1135
1136 def escape_html_tags(text)
1136 def escape_html_tags(text)
1137 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1137 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1138 end
1138 end
1139 end
1139 end
1140
1140
@@ -1,598 +1,598
1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2
2
3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 h1 {margin:0; padding:0; font-size: 24px;}
4 h1 {margin:0; padding:0; font-size: 24px;}
5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8
8
9 /***** Layout *****/
9 /***** Layout *****/
10 #wrapper {background: white;}
10 #wrapper {background: white;}
11
11
12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 #top-menu ul {margin: 0; padding: 0;}
13 #top-menu ul {margin: 0; padding: 0;}
14 #top-menu li {
14 #top-menu li {
15 float:left;
15 float:left;
16 list-style-type:none;
16 list-style-type:none;
17 margin: 0px 0px 0px 0px;
17 margin: 0px 0px 0px 0px;
18 padding: 0px 0px 0px 0px;
18 padding: 0px 0px 0px 0px;
19 white-space:nowrap;
19 white-space:nowrap;
20 }
20 }
21 #top-menu a {color: #fff; padding-right: 8px; font-weight: bold;}
21 #top-menu a {color: #fff; padding-right: 8px; font-weight: bold;}
22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23
23
24 #account {float:right;}
24 #account {float:right;}
25
25
26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 #header a {color:#f8f8f8;}
27 #header a {color:#f8f8f8;}
28 #quick-search {float:right;}
28 #quick-search {float:right;}
29
29
30 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
30 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
31 #main-menu ul {margin: 0; padding: 0;}
31 #main-menu ul {margin: 0; padding: 0;}
32 #main-menu li {
32 #main-menu li {
33 float:left;
33 float:left;
34 list-style-type:none;
34 list-style-type:none;
35 margin: 0px 2px 0px 0px;
35 margin: 0px 2px 0px 0px;
36 padding: 0px 0px 0px 0px;
36 padding: 0px 0px 0px 0px;
37 white-space:nowrap;
37 white-space:nowrap;
38 }
38 }
39 #main-menu li a {
39 #main-menu li a {
40 display: block;
40 display: block;
41 color: #fff;
41 color: #fff;
42 text-decoration: none;
42 text-decoration: none;
43 font-weight: bold;
43 font-weight: bold;
44 margin: 0;
44 margin: 0;
45 padding: 4px 10px 4px 10px;
45 padding: 4px 10px 4px 10px;
46 }
46 }
47 #main-menu li a:hover {background:#759FCF; color:#fff;}
47 #main-menu li a:hover {background:#759FCF; color:#fff;}
48 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
48 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
49
49
50 #main {background-color:#EEEEEE;}
50 #main {background-color:#EEEEEE;}
51
51
52 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
52 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
53 * html #sidebar{ width: 17%; }
53 * html #sidebar{ width: 17%; }
54 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
54 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
55 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
55 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
56 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
56 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
57
57
58 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
58 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
59 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
59 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
60 html>body #content { height: auto; min-height: 600px; overflow: auto; }
60 html>body #content { height: auto; min-height: 600px; overflow: auto; }
61
61
62 #main.nosidebar #sidebar{ display: none; }
62 #main.nosidebar #sidebar{ display: none; }
63 #main.nosidebar #content{ width: auto; border-right: 0; }
63 #main.nosidebar #content{ width: auto; border-right: 0; }
64
64
65 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
65 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
66
66
67 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
67 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
68 #login-form table td {padding: 6px;}
68 #login-form table td {padding: 6px;}
69 #login-form label {font-weight: bold;}
69 #login-form label {font-weight: bold;}
70
70
71 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
71 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
72
72
73 /***** Links *****/
73 /***** Links *****/
74 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
74 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
75 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
75 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
76 a img{ border: 0; }
76 a img{ border: 0; }
77
77
78 a.issue.closed, .issue.closed a { text-decoration: line-through; }
78 a.issue.closed, .issue.closed a { text-decoration: line-through; }
79
79
80 /***** Tables *****/
80 /***** Tables *****/
81 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
81 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
82 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
82 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
83 table.list td { vertical-align: top; }
83 table.list td { vertical-align: top; }
84 table.list td.id { width: 2%; text-align: center;}
84 table.list td.id { width: 2%; text-align: center;}
85 table.list td.checkbox { width: 15px; padding: 0px;}
85 table.list td.checkbox { width: 15px; padding: 0px;}
86
86
87 table.list.issues { margin-top: 10px; }
87 table.list.issues { margin-top: 10px; }
88 tr.issue { text-align: center; white-space: nowrap; }
88 tr.issue { text-align: center; white-space: nowrap; }
89 tr.issue td.subject, tr.issue td.category { white-space: normal; }
89 tr.issue td.subject, tr.issue td.category { white-space: normal; }
90 tr.issue td.subject { text-align: left; }
90 tr.issue td.subject { text-align: left; }
91 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
91 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
92
92
93 tr.entry { border: 1px solid #f8f8f8; }
93 tr.entry { border: 1px solid #f8f8f8; }
94 tr.entry td { white-space: nowrap; }
94 tr.entry td { white-space: nowrap; }
95 tr.entry td.filename { width: 30%; }
95 tr.entry td.filename { width: 30%; }
96 tr.entry td.size { text-align: right; font-size: 90%; }
96 tr.entry td.size { text-align: right; font-size: 90%; }
97 tr.entry td.revision, tr.entry td.author { text-align: center; }
97 tr.entry td.revision, tr.entry td.author { text-align: center; }
98 tr.entry td.age { text-align: right; }
98 tr.entry td.age { text-align: right; }
99
99
100 tr.changeset td.author { text-align: center; width: 15%; }
100 tr.changeset td.author { text-align: center; width: 15%; }
101 tr.changeset td.committed_on { text-align: center; width: 15%; }
101 tr.changeset td.committed_on { text-align: center; width: 15%; }
102
102
103 tr.message { height: 2.6em; }
103 tr.message { height: 2.6em; }
104 tr.message td.last_message { font-size: 80%; }
104 tr.message td.last_message { font-size: 80%; }
105 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
105 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
106 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
106 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
107
107
108 tr.user td { width:13%; }
108 tr.user td { width:13%; }
109 tr.user td.email { width:18%; }
109 tr.user td.email { width:18%; }
110 tr.user td { white-space: nowrap; }
110 tr.user td { white-space: nowrap; }
111 tr.user.locked, tr.user.registered { color: #aaa; }
111 tr.user.locked, tr.user.registered { color: #aaa; }
112 tr.user.locked a, tr.user.registered a { color: #aaa; }
112 tr.user.locked a, tr.user.registered a { color: #aaa; }
113
113
114 tr.time-entry { text-align: center; white-space: nowrap; }
114 tr.time-entry { text-align: center; white-space: nowrap; }
115 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
115 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
116 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
116 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
117 td.hours .hours-dec { font-size: 0.9em; }
117 td.hours .hours-dec { font-size: 0.9em; }
118
118
119 table.list tbody tr:hover { background-color:#ffffdd; }
119 table.list tbody tr:hover { background-color:#ffffdd; }
120 table td {padding:2px;}
120 table td {padding:2px;}
121 table p {margin:0;}
121 table p {margin:0;}
122 .odd {background-color:#f6f7f8;}
122 .odd {background-color:#f6f7f8;}
123 .even {background-color: #fff;}
123 .even {background-color: #fff;}
124
124
125 .highlight { background-color: #FCFD8D;}
125 .highlight { background-color: #FCFD8D;}
126 .highlight.token-1 { background-color: #faa;}
126 .highlight.token-1 { background-color: #faa;}
127 .highlight.token-2 { background-color: #afa;}
127 .highlight.token-2 { background-color: #afa;}
128 .highlight.token-3 { background-color: #aaf;}
128 .highlight.token-3 { background-color: #aaf;}
129
129
130 .box{
130 .box{
131 padding:6px;
131 padding:6px;
132 margin-bottom: 10px;
132 margin-bottom: 10px;
133 background-color:#f6f6f6;
133 background-color:#f6f6f6;
134 color:#505050;
134 color:#505050;
135 line-height:1.5em;
135 line-height:1.5em;
136 border: 1px solid #e4e4e4;
136 border: 1px solid #e4e4e4;
137 }
137 }
138
138
139 div.square {
139 div.square {
140 border: 1px solid #999;
140 border: 1px solid #999;
141 float: left;
141 float: left;
142 margin: .3em .4em 0 .4em;
142 margin: .3em .4em 0 .4em;
143 overflow: hidden;
143 overflow: hidden;
144 width: .6em; height: .6em;
144 width: .6em; height: .6em;
145 }
145 }
146 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
146 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
147 .contextual input {font-size:0.9em;}
147 .contextual input {font-size:0.9em;}
148
148
149 .splitcontentleft{float:left; width:49%;}
149 .splitcontentleft{float:left; width:49%;}
150 .splitcontentright{float:right; width:49%;}
150 .splitcontentright{float:right; width:49%;}
151 form {display: inline;}
151 form {display: inline;}
152 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
152 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
153 fieldset {border: 1px solid #e4e4e4; margin:0;}
153 fieldset {border: 1px solid #e4e4e4; margin:0;}
154 legend {color: #484848;}
154 legend {color: #484848;}
155 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
155 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
156 textarea.wiki-edit { width: 99%; }
156 textarea.wiki-edit { width: 99%; }
157 li p {margin-top: 0;}
157 li p {margin-top: 0;}
158 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
158 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
159 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
159 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
160 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
160 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
161
161
162 fieldset#filters { padding: 0.7em; }
162 fieldset#filters { padding: 0.7em; }
163 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
163 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
164 fieldset#filters .buttons { font-size: 0.9em; }
164 fieldset#filters .buttons { font-size: 0.9em; }
165 fieldset#filters table { border-collapse: collapse; }
165 fieldset#filters table { border-collapse: collapse; }
166 fieldset#filters table td { padding: 0; vertical-align: middle; }
166 fieldset#filters table td { padding: 0; vertical-align: middle; }
167 fieldset#filters tr.filter { height: 2em; }
167 fieldset#filters tr.filter { height: 2em; }
168 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
168 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
169
169
170 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
170 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
171 div#issue-changesets .changeset { padding: 4px;}
171 div#issue-changesets .changeset { padding: 4px;}
172 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
172 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
173 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
173 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
174
174
175 div#activity dl { margin-left: 2em; }
175 div#activity dl { margin-left: 2em; }
176 div#activity dd { margin-bottom: 1em; padding-left: 18px; }
176 div#activity dd { margin-bottom: 1em; padding-left: 18px; }
177 div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
177 div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
178 div#activity dt .time { color: #777; font-size: 80%; }
178 div#activity dt .time { color: #777; font-size: 80%; }
179 div#activity dd .description { font-style: italic; }
179 div#activity dd .description { font-style: italic; }
180 div#activity span.project:after { content: " -"; }
180 div#activity span.project:after { content: " -"; }
181 div#activity dt.issue { background-image: url(../images/ticket.png); }
181 div#activity dt.issue { background-image: url(../images/ticket.png); }
182 div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); }
182 div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); }
183 div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); }
183 div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); }
184 div#activity dt.changeset { background-image: url(../images/changeset.png); }
184 div#activity dt.changeset { background-image: url(../images/changeset.png); }
185 div#activity dt.news { background-image: url(../images/news.png); }
185 div#activity dt.news { background-image: url(../images/news.png); }
186 div#activity dt.message { background-image: url(../images/message.png); }
186 div#activity dt.message { background-image: url(../images/message.png); }
187 div#activity dt.reply { background-image: url(../images/comments.png); }
187 div#activity dt.reply { background-image: url(../images/comments.png); }
188 div#activity dt.wiki-page { background-image: url(../images/wiki_edit.png); }
188 div#activity dt.wiki-page { background-image: url(../images/wiki_edit.png); }
189 div#activity dt.attachment { background-image: url(../images/attachment.png); }
189 div#activity dt.attachment { background-image: url(../images/attachment.png); }
190 div#activity dt.document { background-image: url(../images/document.png); }
190 div#activity dt.document { background-image: url(../images/document.png); }
191
191
192 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
192 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
193 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
193 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
194 div#roadmap .wiki h1:first-child { display: none; }
194 div#roadmap .wiki h1:first-child { display: none; }
195 div#roadmap .wiki h1 { font-size: 120%; }
195 div#roadmap .wiki h1 { font-size: 120%; }
196 div#roadmap .wiki h2 { font-size: 110%; }
196 div#roadmap .wiki h2 { font-size: 110%; }
197
197
198 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
198 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
199 div#version-summary fieldset { margin-bottom: 1em; }
199 div#version-summary fieldset { margin-bottom: 1em; }
200 div#version-summary .total-hours { text-align: right; }
200 div#version-summary .total-hours { text-align: right; }
201
201
202 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
202 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
203 table#time-report tbody tr { font-style: italic; color: #777; }
203 table#time-report tbody tr { font-style: italic; color: #777; }
204 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
204 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
205 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
205 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
206 table#time-report .hours-dec { font-size: 0.9em; }
206 table#time-report .hours-dec { font-size: 0.9em; }
207
207
208 .total-hours { font-size: 110%; font-weight: bold; }
208 .total-hours { font-size: 110%; font-weight: bold; }
209 .total-hours span.hours-int { font-size: 120%; }
209 .total-hours span.hours-int { font-size: 120%; }
210
210
211 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
211 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
212 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
212 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
213
213
214 .pagination {font-size: 90%}
214 .pagination {font-size: 90%}
215 p.pagination {margin-top:8px;}
215 p.pagination {margin-top:8px;}
216
216
217 /***** Tabular forms ******/
217 /***** Tabular forms ******/
218 .tabular p{
218 .tabular p{
219 margin: 0;
219 margin: 0;
220 padding: 5px 0 8px 0;
220 padding: 5px 0 8px 0;
221 padding-left: 180px; /*width of left column containing the label elements*/
221 padding-left: 180px; /*width of left column containing the label elements*/
222 height: 1%;
222 height: 1%;
223 clear:left;
223 clear:left;
224 }
224 }
225
225
226 html>body .tabular p {overflow:auto;}
226 html>body .tabular p {overflow:hidden;}
227
227
228 .tabular label{
228 .tabular label{
229 font-weight: bold;
229 font-weight: bold;
230 float: left;
230 float: left;
231 text-align: right;
231 text-align: right;
232 margin-left: -180px; /*width of left column*/
232 margin-left: -180px; /*width of left column*/
233 width: 175px; /*width of labels. Should be smaller than left column to create some right
233 width: 175px; /*width of labels. Should be smaller than left column to create some right
234 margin*/
234 margin*/
235 }
235 }
236
236
237 .tabular label.floating{
237 .tabular label.floating{
238 font-weight: normal;
238 font-weight: normal;
239 margin-left: 0px;
239 margin-left: 0px;
240 text-align: left;
240 text-align: left;
241 width: 200px;
241 width: 200px;
242 }
242 }
243
243
244 input#time_entry_comments { width: 90%;}
244 input#time_entry_comments { width: 90%;}
245
245
246 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
246 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
247
247
248 .tabular.settings p{ padding-left: 300px; }
248 .tabular.settings p{ padding-left: 300px; }
249 .tabular.settings label{ margin-left: -300px; width: 295px; }
249 .tabular.settings label{ margin-left: -300px; width: 295px; }
250
250
251 .required {color: #bb0000;}
251 .required {color: #bb0000;}
252 .summary {font-style: italic;}
252 .summary {font-style: italic;}
253
253
254 #attachments_fields input[type=text] {margin-left: 8px; }
254 #attachments_fields input[type=text] {margin-left: 8px; }
255
255
256 div.attachments p { margin:4px 0 2px 0; }
256 div.attachments p { margin:4px 0 2px 0; }
257 div.attachments img { vertical-align: middle; }
257 div.attachments img { vertical-align: middle; }
258 div.attachments span.author { font-size: 0.9em; color: #888; }
258 div.attachments span.author { font-size: 0.9em; color: #888; }
259
259
260 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
260 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
261 .other-formats span + span:before { content: "| "; }
261 .other-formats span + span:before { content: "| "; }
262
262
263 a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
263 a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
264
264
265 /***** Flash & error messages ****/
265 /***** Flash & error messages ****/
266 #errorExplanation, div.flash, .nodata, .warning {
266 #errorExplanation, div.flash, .nodata, .warning {
267 padding: 4px 4px 4px 30px;
267 padding: 4px 4px 4px 30px;
268 margin-bottom: 12px;
268 margin-bottom: 12px;
269 font-size: 1.1em;
269 font-size: 1.1em;
270 border: 2px solid;
270 border: 2px solid;
271 }
271 }
272
272
273 div.flash {margin-top: 8px;}
273 div.flash {margin-top: 8px;}
274
274
275 div.flash.error, #errorExplanation {
275 div.flash.error, #errorExplanation {
276 background: url(../images/false.png) 8px 5px no-repeat;
276 background: url(../images/false.png) 8px 5px no-repeat;
277 background-color: #ffe3e3;
277 background-color: #ffe3e3;
278 border-color: #dd0000;
278 border-color: #dd0000;
279 color: #550000;
279 color: #550000;
280 }
280 }
281
281
282 div.flash.notice {
282 div.flash.notice {
283 background: url(../images/true.png) 8px 5px no-repeat;
283 background: url(../images/true.png) 8px 5px no-repeat;
284 background-color: #dfffdf;
284 background-color: #dfffdf;
285 border-color: #9fcf9f;
285 border-color: #9fcf9f;
286 color: #005f00;
286 color: #005f00;
287 }
287 }
288
288
289 .nodata, .warning {
289 .nodata, .warning {
290 text-align: center;
290 text-align: center;
291 background-color: #FFEBC1;
291 background-color: #FFEBC1;
292 border-color: #FDBF3B;
292 border-color: #FDBF3B;
293 color: #A6750C;
293 color: #A6750C;
294 }
294 }
295
295
296 #errorExplanation ul { font-size: 0.9em;}
296 #errorExplanation ul { font-size: 0.9em;}
297
297
298 /***** Ajax indicator ******/
298 /***** Ajax indicator ******/
299 #ajax-indicator {
299 #ajax-indicator {
300 position: absolute; /* fixed not supported by IE */
300 position: absolute; /* fixed not supported by IE */
301 background-color:#eee;
301 background-color:#eee;
302 border: 1px solid #bbb;
302 border: 1px solid #bbb;
303 top:35%;
303 top:35%;
304 left:40%;
304 left:40%;
305 width:20%;
305 width:20%;
306 font-weight:bold;
306 font-weight:bold;
307 text-align:center;
307 text-align:center;
308 padding:0.6em;
308 padding:0.6em;
309 z-index:100;
309 z-index:100;
310 filter:alpha(opacity=50);
310 filter:alpha(opacity=50);
311 opacity: 0.5;
311 opacity: 0.5;
312 }
312 }
313
313
314 html>body #ajax-indicator { position: fixed; }
314 html>body #ajax-indicator { position: fixed; }
315
315
316 #ajax-indicator span {
316 #ajax-indicator span {
317 background-position: 0% 40%;
317 background-position: 0% 40%;
318 background-repeat: no-repeat;
318 background-repeat: no-repeat;
319 background-image: url(../images/loading.gif);
319 background-image: url(../images/loading.gif);
320 padding-left: 26px;
320 padding-left: 26px;
321 vertical-align: bottom;
321 vertical-align: bottom;
322 }
322 }
323
323
324 /***** Calendar *****/
324 /***** Calendar *****/
325 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
325 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
326 table.cal thead th {width: 14%;}
326 table.cal thead th {width: 14%;}
327 table.cal tbody tr {height: 100px;}
327 table.cal tbody tr {height: 100px;}
328 table.cal th { background-color:#EEEEEE; padding: 4px; }
328 table.cal th { background-color:#EEEEEE; padding: 4px; }
329 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
329 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
330 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
330 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
331 table.cal td.odd p.day-num {color: #bbb;}
331 table.cal td.odd p.day-num {color: #bbb;}
332 table.cal td.today {background:#ffffdd;}
332 table.cal td.today {background:#ffffdd;}
333 table.cal td.today p.day-num {font-weight: bold;}
333 table.cal td.today p.day-num {font-weight: bold;}
334
334
335 /***** Tooltips ******/
335 /***** Tooltips ******/
336 .tooltip{position:relative;z-index:24;}
336 .tooltip{position:relative;z-index:24;}
337 .tooltip:hover{z-index:25;color:#000;}
337 .tooltip:hover{z-index:25;color:#000;}
338 .tooltip span.tip{display: none; text-align:left;}
338 .tooltip span.tip{display: none; text-align:left;}
339
339
340 div.tooltip:hover span.tip{
340 div.tooltip:hover span.tip{
341 display:block;
341 display:block;
342 position:absolute;
342 position:absolute;
343 top:12px; left:24px; width:270px;
343 top:12px; left:24px; width:270px;
344 border:1px solid #555;
344 border:1px solid #555;
345 background-color:#fff;
345 background-color:#fff;
346 padding: 4px;
346 padding: 4px;
347 font-size: 0.8em;
347 font-size: 0.8em;
348 color:#505050;
348 color:#505050;
349 }
349 }
350
350
351 /***** Progress bar *****/
351 /***** Progress bar *****/
352 table.progress {
352 table.progress {
353 border: 1px solid #D7D7D7;
353 border: 1px solid #D7D7D7;
354 border-collapse: collapse;
354 border-collapse: collapse;
355 border-spacing: 0pt;
355 border-spacing: 0pt;
356 empty-cells: show;
356 empty-cells: show;
357 text-align: center;
357 text-align: center;
358 float:left;
358 float:left;
359 margin: 1px 6px 1px 0px;
359 margin: 1px 6px 1px 0px;
360 }
360 }
361
361
362 table.progress td { height: 0.9em; }
362 table.progress td { height: 0.9em; }
363 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
363 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
364 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
364 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
365 table.progress td.open { background: #FFF none repeat scroll 0%; }
365 table.progress td.open { background: #FFF none repeat scroll 0%; }
366 p.pourcent {font-size: 80%;}
366 p.pourcent {font-size: 80%;}
367 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
367 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
368
368
369 /***** Tabs *****/
369 /***** Tabs *****/
370 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
370 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
371 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
371 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
372 #content .tabs>ul { bottom:-1px; } /* others */
372 #content .tabs>ul { bottom:-1px; } /* others */
373 #content .tabs ul li {
373 #content .tabs ul li {
374 float:left;
374 float:left;
375 list-style-type:none;
375 list-style-type:none;
376 white-space:nowrap;
376 white-space:nowrap;
377 margin-right:8px;
377 margin-right:8px;
378 background:#fff;
378 background:#fff;
379 }
379 }
380 #content .tabs ul li a{
380 #content .tabs ul li a{
381 display:block;
381 display:block;
382 font-size: 0.9em;
382 font-size: 0.9em;
383 text-decoration:none;
383 text-decoration:none;
384 line-height:1.3em;
384 line-height:1.3em;
385 padding:4px 6px 4px 6px;
385 padding:4px 6px 4px 6px;
386 border: 1px solid #ccc;
386 border: 1px solid #ccc;
387 border-bottom: 1px solid #bbbbbb;
387 border-bottom: 1px solid #bbbbbb;
388 background-color: #eeeeee;
388 background-color: #eeeeee;
389 color:#777;
389 color:#777;
390 font-weight:bold;
390 font-weight:bold;
391 }
391 }
392
392
393 #content .tabs ul li a:hover {
393 #content .tabs ul li a:hover {
394 background-color: #ffffdd;
394 background-color: #ffffdd;
395 text-decoration:none;
395 text-decoration:none;
396 }
396 }
397
397
398 #content .tabs ul li a.selected {
398 #content .tabs ul li a.selected {
399 background-color: #fff;
399 background-color: #fff;
400 border: 1px solid #bbbbbb;
400 border: 1px solid #bbbbbb;
401 border-bottom: 1px solid #fff;
401 border-bottom: 1px solid #fff;
402 }
402 }
403
403
404 #content .tabs ul li a.selected:hover {
404 #content .tabs ul li a.selected:hover {
405 background-color: #fff;
405 background-color: #fff;
406 }
406 }
407
407
408 /***** Diff *****/
408 /***** Diff *****/
409 .diff_out { background: #fcc; }
409 .diff_out { background: #fcc; }
410 .diff_in { background: #cfc; }
410 .diff_in { background: #cfc; }
411
411
412 /***** Wiki *****/
412 /***** Wiki *****/
413 div.wiki table {
413 div.wiki table {
414 border: 1px solid #505050;
414 border: 1px solid #505050;
415 border-collapse: collapse;
415 border-collapse: collapse;
416 margin-bottom: 1em;
416 margin-bottom: 1em;
417 }
417 }
418
418
419 div.wiki table, div.wiki td, div.wiki th {
419 div.wiki table, div.wiki td, div.wiki th {
420 border: 1px solid #bbb;
420 border: 1px solid #bbb;
421 padding: 4px;
421 padding: 4px;
422 }
422 }
423
423
424 div.wiki .external {
424 div.wiki .external {
425 background-position: 0% 60%;
425 background-position: 0% 60%;
426 background-repeat: no-repeat;
426 background-repeat: no-repeat;
427 padding-left: 12px;
427 padding-left: 12px;
428 background-image: url(../images/external.png);
428 background-image: url(../images/external.png);
429 }
429 }
430
430
431 div.wiki a.new {
431 div.wiki a.new {
432 color: #b73535;
432 color: #b73535;
433 }
433 }
434
434
435 div.wiki pre {
435 div.wiki pre {
436 margin: 1em 1em 1em 1.6em;
436 margin: 1em 1em 1em 1.6em;
437 padding: 2px;
437 padding: 2px;
438 background-color: #fafafa;
438 background-color: #fafafa;
439 border: 1px solid #dadada;
439 border: 1px solid #dadada;
440 width:95%;
440 width:95%;
441 overflow-x: auto;
441 overflow-x: auto;
442 }
442 }
443
443
444 div.wiki div.toc {
444 div.wiki div.toc {
445 background-color: #ffffdd;
445 background-color: #ffffdd;
446 border: 1px solid #e4e4e4;
446 border: 1px solid #e4e4e4;
447 padding: 4px;
447 padding: 4px;
448 line-height: 1.2em;
448 line-height: 1.2em;
449 margin-bottom: 12px;
449 margin-bottom: 12px;
450 margin-right: 12px;
450 margin-right: 12px;
451 display: table
451 display: table
452 }
452 }
453 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
453 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
454
454
455 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
455 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
456 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
456 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
457
457
458 div.wiki div.toc a {
458 div.wiki div.toc a {
459 display: block;
459 display: block;
460 font-size: 0.9em;
460 font-size: 0.9em;
461 font-weight: normal;
461 font-weight: normal;
462 text-decoration: none;
462 text-decoration: none;
463 color: #606060;
463 color: #606060;
464 }
464 }
465 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
465 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
466
466
467 div.wiki div.toc a.heading2 { margin-left: 6px; }
467 div.wiki div.toc a.heading2 { margin-left: 6px; }
468 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
468 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
469
469
470 /***** My page layout *****/
470 /***** My page layout *****/
471 .block-receiver {
471 .block-receiver {
472 border:1px dashed #c0c0c0;
472 border:1px dashed #c0c0c0;
473 margin-bottom: 20px;
473 margin-bottom: 20px;
474 padding: 15px 0 15px 0;
474 padding: 15px 0 15px 0;
475 }
475 }
476
476
477 .mypage-box {
477 .mypage-box {
478 margin:0 0 20px 0;
478 margin:0 0 20px 0;
479 color:#505050;
479 color:#505050;
480 line-height:1.5em;
480 line-height:1.5em;
481 }
481 }
482
482
483 .handle {
483 .handle {
484 cursor: move;
484 cursor: move;
485 }
485 }
486
486
487 a.close-icon {
487 a.close-icon {
488 display:block;
488 display:block;
489 margin-top:3px;
489 margin-top:3px;
490 overflow:hidden;
490 overflow:hidden;
491 width:12px;
491 width:12px;
492 height:12px;
492 height:12px;
493 background-repeat: no-repeat;
493 background-repeat: no-repeat;
494 cursor:pointer;
494 cursor:pointer;
495 background-image:url('../images/close.png');
495 background-image:url('../images/close.png');
496 }
496 }
497
497
498 a.close-icon:hover {
498 a.close-icon:hover {
499 background-image:url('../images/close_hl.png');
499 background-image:url('../images/close_hl.png');
500 }
500 }
501
501
502 /***** Gantt chart *****/
502 /***** Gantt chart *****/
503 .gantt_hdr {
503 .gantt_hdr {
504 position:absolute;
504 position:absolute;
505 top:0;
505 top:0;
506 height:16px;
506 height:16px;
507 border-top: 1px solid #c0c0c0;
507 border-top: 1px solid #c0c0c0;
508 border-bottom: 1px solid #c0c0c0;
508 border-bottom: 1px solid #c0c0c0;
509 border-right: 1px solid #c0c0c0;
509 border-right: 1px solid #c0c0c0;
510 text-align: center;
510 text-align: center;
511 overflow: hidden;
511 overflow: hidden;
512 }
512 }
513
513
514 .task {
514 .task {
515 position: absolute;
515 position: absolute;
516 height:8px;
516 height:8px;
517 font-size:0.8em;
517 font-size:0.8em;
518 color:#888;
518 color:#888;
519 padding:0;
519 padding:0;
520 margin:0;
520 margin:0;
521 line-height:0.8em;
521 line-height:0.8em;
522 }
522 }
523
523
524 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
524 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
525 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
525 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
526 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
526 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
527 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
527 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
528
528
529 /***** Icons *****/
529 /***** Icons *****/
530 .icon {
530 .icon {
531 background-position: 0% 40%;
531 background-position: 0% 40%;
532 background-repeat: no-repeat;
532 background-repeat: no-repeat;
533 padding-left: 20px;
533 padding-left: 20px;
534 padding-top: 2px;
534 padding-top: 2px;
535 padding-bottom: 3px;
535 padding-bottom: 3px;
536 }
536 }
537
537
538 .icon22 {
538 .icon22 {
539 background-position: 0% 40%;
539 background-position: 0% 40%;
540 background-repeat: no-repeat;
540 background-repeat: no-repeat;
541 padding-left: 26px;
541 padding-left: 26px;
542 line-height: 22px;
542 line-height: 22px;
543 vertical-align: middle;
543 vertical-align: middle;
544 }
544 }
545
545
546 .icon-add { background-image: url(../images/add.png); }
546 .icon-add { background-image: url(../images/add.png); }
547 .icon-edit { background-image: url(../images/edit.png); }
547 .icon-edit { background-image: url(../images/edit.png); }
548 .icon-copy { background-image: url(../images/copy.png); }
548 .icon-copy { background-image: url(../images/copy.png); }
549 .icon-del { background-image: url(../images/delete.png); }
549 .icon-del { background-image: url(../images/delete.png); }
550 .icon-move { background-image: url(../images/move.png); }
550 .icon-move { background-image: url(../images/move.png); }
551 .icon-save { background-image: url(../images/save.png); }
551 .icon-save { background-image: url(../images/save.png); }
552 .icon-cancel { background-image: url(../images/cancel.png); }
552 .icon-cancel { background-image: url(../images/cancel.png); }
553 .icon-file { background-image: url(../images/file.png); }
553 .icon-file { background-image: url(../images/file.png); }
554 .icon-folder { background-image: url(../images/folder.png); }
554 .icon-folder { background-image: url(../images/folder.png); }
555 .open .icon-folder { background-image: url(../images/folder_open.png); }
555 .open .icon-folder { background-image: url(../images/folder_open.png); }
556 .icon-package { background-image: url(../images/package.png); }
556 .icon-package { background-image: url(../images/package.png); }
557 .icon-home { background-image: url(../images/home.png); }
557 .icon-home { background-image: url(../images/home.png); }
558 .icon-user { background-image: url(../images/user.png); }
558 .icon-user { background-image: url(../images/user.png); }
559 .icon-mypage { background-image: url(../images/user_page.png); }
559 .icon-mypage { background-image: url(../images/user_page.png); }
560 .icon-admin { background-image: url(../images/admin.png); }
560 .icon-admin { background-image: url(../images/admin.png); }
561 .icon-projects { background-image: url(../images/projects.png); }
561 .icon-projects { background-image: url(../images/projects.png); }
562 .icon-logout { background-image: url(../images/logout.png); }
562 .icon-logout { background-image: url(../images/logout.png); }
563 .icon-help { background-image: url(../images/help.png); }
563 .icon-help { background-image: url(../images/help.png); }
564 .icon-attachment { background-image: url(../images/attachment.png); }
564 .icon-attachment { background-image: url(../images/attachment.png); }
565 .icon-index { background-image: url(../images/index.png); }
565 .icon-index { background-image: url(../images/index.png); }
566 .icon-history { background-image: url(../images/history.png); }
566 .icon-history { background-image: url(../images/history.png); }
567 .icon-time { background-image: url(../images/time.png); }
567 .icon-time { background-image: url(../images/time.png); }
568 .icon-stats { background-image: url(../images/stats.png); }
568 .icon-stats { background-image: url(../images/stats.png); }
569 .icon-warning { background-image: url(../images/warning.png); }
569 .icon-warning { background-image: url(../images/warning.png); }
570 .icon-fav { background-image: url(../images/fav.png); }
570 .icon-fav { background-image: url(../images/fav.png); }
571 .icon-fav-off { background-image: url(../images/fav_off.png); }
571 .icon-fav-off { background-image: url(../images/fav_off.png); }
572 .icon-reload { background-image: url(../images/reload.png); }
572 .icon-reload { background-image: url(../images/reload.png); }
573 .icon-lock { background-image: url(../images/locked.png); }
573 .icon-lock { background-image: url(../images/locked.png); }
574 .icon-unlock { background-image: url(../images/unlock.png); }
574 .icon-unlock { background-image: url(../images/unlock.png); }
575 .icon-checked { background-image: url(../images/true.png); }
575 .icon-checked { background-image: url(../images/true.png); }
576 .icon-details { background-image: url(../images/zoom_in.png); }
576 .icon-details { background-image: url(../images/zoom_in.png); }
577 .icon-report { background-image: url(../images/report.png); }
577 .icon-report { background-image: url(../images/report.png); }
578
578
579 .icon22-projects { background-image: url(../images/22x22/projects.png); }
579 .icon22-projects { background-image: url(../images/22x22/projects.png); }
580 .icon22-users { background-image: url(../images/22x22/users.png); }
580 .icon22-users { background-image: url(../images/22x22/users.png); }
581 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
581 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
582 .icon22-role { background-image: url(../images/22x22/role.png); }
582 .icon22-role { background-image: url(../images/22x22/role.png); }
583 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
583 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
584 .icon22-options { background-image: url(../images/22x22/options.png); }
584 .icon22-options { background-image: url(../images/22x22/options.png); }
585 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
585 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
586 .icon22-authent { background-image: url(../images/22x22/authent.png); }
586 .icon22-authent { background-image: url(../images/22x22/authent.png); }
587 .icon22-info { background-image: url(../images/22x22/info.png); }
587 .icon22-info { background-image: url(../images/22x22/info.png); }
588 .icon22-comment { background-image: url(../images/22x22/comment.png); }
588 .icon22-comment { background-image: url(../images/22x22/comment.png); }
589 .icon22-package { background-image: url(../images/22x22/package.png); }
589 .icon22-package { background-image: url(../images/22x22/package.png); }
590 .icon22-settings { background-image: url(../images/22x22/settings.png); }
590 .icon22-settings { background-image: url(../images/22x22/settings.png); }
591 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
591 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
592
592
593 /***** Media print specific styles *****/
593 /***** Media print specific styles *****/
594 @media print {
594 @media print {
595 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
595 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
596 #main { background: #fff; }
596 #main { background: #fff; }
597 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
597 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
598 }
598 }
@@ -1,234 +1,242
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../../test_helper'
18 require File.dirname(__FILE__) + '/../../test_helper'
19
19
20 class ApplicationHelperTest < HelperTestCase
20 class ApplicationHelperTest < HelperTestCase
21 include ApplicationHelper
21 include ApplicationHelper
22 include ActionView::Helpers::TextHelper
22 include ActionView::Helpers::TextHelper
23 fixtures :projects, :repositories, :changesets, :trackers, :issue_statuses, :issues, :documents, :versions, :wikis, :wiki_pages, :wiki_contents, :roles, :enabled_modules
23 fixtures :projects, :repositories, :changesets, :trackers, :issue_statuses, :issues, :documents, :versions, :wikis, :wiki_pages, :wiki_contents, :roles, :enabled_modules
24
24
25 def setup
25 def setup
26 super
26 super
27 end
27 end
28
28
29 def test_auto_links
29 def test_auto_links
30 to_test = {
30 to_test = {
31 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
31 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
32 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
32 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
33 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
33 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
34 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
34 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
35 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
35 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
36 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
36 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
37 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>'
37 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>'
38 }
38 }
39 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
39 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
40 end
40 end
41
41
42 def test_auto_mailto
42 def test_auto_mailto
43 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
43 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
44 textilizable('test@foo.bar')
44 textilizable('test@foo.bar')
45 end
45 end
46
46
47 def test_inline_images
47 def test_inline_images
48 to_test = {
48 to_test = {
49 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
49 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
50 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
50 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
51 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
51 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
52 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
52 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
53 }
53 }
54 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
54 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
55 end
55 end
56
56
57 def test_textile_external_links
57 def test_textile_external_links
58 to_test = {
58 to_test = {
59 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
59 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
60 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
60 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
61 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
61 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
62 # no multiline link text
62 # no multiline link text
63 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test"
63 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test"
64 }
64 }
65 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
65 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
66 end
66 end
67
67
68 def test_redmine_links
68 def test_redmine_links
69 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
69 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
70 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
70 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
71
71
72 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
72 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
73 :class => 'changeset', :title => 'My very first commit')
73 :class => 'changeset', :title => 'My very first commit')
74
74
75 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
75 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
76 :class => 'document')
76 :class => 'document')
77
77
78 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
78 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
79 :class => 'version')
79 :class => 'version')
80
80
81 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => 'some/file'}
81 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => 'some/file'}
82
82
83 to_test = {
83 to_test = {
84 # tickets
84 # tickets
85 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
85 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
86 # changesets
86 # changesets
87 'r1' => changeset_link,
87 'r1' => changeset_link,
88 # documents
88 # documents
89 'document#1' => document_link,
89 'document#1' => document_link,
90 'document:"Test document"' => document_link,
90 'document:"Test document"' => document_link,
91 # versions
91 # versions
92 'version#2' => version_link,
92 'version#2' => version_link,
93 'version:1.0' => version_link,
93 'version:1.0' => version_link,
94 'version:"1.0"' => version_link,
94 'version:"1.0"' => version_link,
95 # source
95 # source
96 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
96 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
97 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
97 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
98 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
98 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
99 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
99 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
100 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
100 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
101 # escaping
101 # escaping
102 '!#3.' => '#3.',
102 '!#3.' => '#3.',
103 '!r1' => 'r1',
103 '!r1' => 'r1',
104 '!document#1' => 'document#1',
104 '!document#1' => 'document#1',
105 '!document:"Test document"' => 'document:"Test document"',
105 '!document:"Test document"' => 'document:"Test document"',
106 '!version#2' => 'version#2',
106 '!version#2' => 'version#2',
107 '!version:1.0' => 'version:1.0',
107 '!version:1.0' => 'version:1.0',
108 '!version:"1.0"' => 'version:"1.0"',
108 '!version:"1.0"' => 'version:"1.0"',
109 '!source:/some/file' => 'source:/some/file',
109 '!source:/some/file' => 'source:/some/file',
110 # invalid expressions
110 # invalid expressions
111 'source:' => 'source:',
111 'source:' => 'source:',
112 # url hash
112 # url hash
113 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
113 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
114 }
114 }
115 @project = Project.find(1)
115 @project = Project.find(1)
116 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
116 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
117 end
117 end
118
118
119 def test_wiki_links
119 def test_wiki_links
120 to_test = {
120 to_test = {
121 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
121 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
122 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
122 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
123 # page that doesn't exist
123 # page that doesn't exist
124 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
124 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
125 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
125 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
126 # link to another project wiki
126 # link to another project wiki
127 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
127 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
128 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
128 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
129 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
129 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
130 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
130 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
131 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
131 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
132 # escaping
132 # escaping
133 '![[Another page|Page]]' => '[[Another page|Page]]',
133 '![[Another page|Page]]' => '[[Another page|Page]]',
134 }
134 }
135 @project = Project.find(1)
135 @project = Project.find(1)
136 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
136 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
137 end
137 end
138
138
139 def test_html_tags
139 def test_html_tags
140 to_test = {
140 to_test = {
141 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
141 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
142 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
142 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
143 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
143 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
144 # do not escape pre/code tags
144 # do not escape pre/code tags
145 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
145 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
146 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
146 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
147 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
147 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
148 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
148 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
149 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>"
149 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>"
150 }
150 }
151 to_test.each { |text, result| assert_equal result, textilizable(text) }
151 to_test.each { |text, result| assert_equal result, textilizable(text) }
152 end
152 end
153
153
154 def test_allowed_html_tags
155 to_test = {
156 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
157 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
158 }
159 to_test.each { |text, result| assert_equal result, textilizable(text) }
160 end
161
154 def test_wiki_links_in_tables
162 def test_wiki_links_in_tables
155 to_test = {"|Cell 11|Cell 12|Cell 13|\n|Cell 21|Cell 22||\n|Cell 31||Cell 33|" =>
163 to_test = {"|Cell 11|Cell 12|Cell 13|\n|Cell 21|Cell 22||\n|Cell 31||Cell 33|" =>
156 '<tr><td>Cell 11</td><td>Cell 12</td><td>Cell 13</td></tr>' +
164 '<tr><td>Cell 11</td><td>Cell 12</td><td>Cell 13</td></tr>' +
157 '<tr><td>Cell 21</td><td>Cell 22</td></tr>' +
165 '<tr><td>Cell 21</td><td>Cell 22</td></tr>' +
158 '<tr><td>Cell 31</td><td>Cell 33</td></tr>',
166 '<tr><td>Cell 31</td><td>Cell 33</td></tr>',
159
167
160 "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
168 "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
161 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
169 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
162 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
170 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
163 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
171 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
164 }
172 }
165 @project = Project.find(1)
173 @project = Project.find(1)
166 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
174 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
167 end
175 end
168
176
169 def test_text_formatting
177 def test_text_formatting
170 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
178 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
171 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
179 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
172 }
180 }
173 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
181 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
174 end
182 end
175
183
176 def test_wiki_horizontal_rule
184 def test_wiki_horizontal_rule
177 assert_equal '<hr />', textilizable('---')
185 assert_equal '<hr />', textilizable('---')
178 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
186 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
179 end
187 end
180
188
181 def test_macro_hello_world
189 def test_macro_hello_world
182 text = "{{hello_world}}"
190 text = "{{hello_world}}"
183 assert textilizable(text).match(/Hello world!/)
191 assert textilizable(text).match(/Hello world!/)
184 # escaping
192 # escaping
185 text = "!{{hello_world}}"
193 text = "!{{hello_world}}"
186 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
194 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
187 end
195 end
188
196
189 def test_macro_include
197 def test_macro_include
190 @project = Project.find(1)
198 @project = Project.find(1)
191 # include a page of the current project wiki
199 # include a page of the current project wiki
192 text = "{{include(Another page)}}"
200 text = "{{include(Another page)}}"
193 assert textilizable(text).match(/This is a link to a ticket/)
201 assert textilizable(text).match(/This is a link to a ticket/)
194
202
195 @project = nil
203 @project = nil
196 # include a page of a specific project wiki
204 # include a page of a specific project wiki
197 text = "{{include(ecookbook:Another page)}}"
205 text = "{{include(ecookbook:Another page)}}"
198 assert textilizable(text).match(/This is a link to a ticket/)
206 assert textilizable(text).match(/This is a link to a ticket/)
199
207
200 text = "{{include(ecookbook:)}}"
208 text = "{{include(ecookbook:)}}"
201 assert textilizable(text).match(/CookBook documentation/)
209 assert textilizable(text).match(/CookBook documentation/)
202
210
203 text = "{{include(unknowidentifier:somepage)}}"
211 text = "{{include(unknowidentifier:somepage)}}"
204 assert textilizable(text).match(/Unknow project/)
212 assert textilizable(text).match(/Unknow project/)
205 end
213 end
206
214
207 def test_date_format_default
215 def test_date_format_default
208 today = Date.today
216 today = Date.today
209 Setting.date_format = ''
217 Setting.date_format = ''
210 assert_equal l_date(today), format_date(today)
218 assert_equal l_date(today), format_date(today)
211 end
219 end
212
220
213 def test_date_format
221 def test_date_format
214 today = Date.today
222 today = Date.today
215 Setting.date_format = '%d %m %Y'
223 Setting.date_format = '%d %m %Y'
216 assert_equal today.strftime('%d %m %Y'), format_date(today)
224 assert_equal today.strftime('%d %m %Y'), format_date(today)
217 end
225 end
218
226
219 def test_time_format_default
227 def test_time_format_default
220 now = Time.now
228 now = Time.now
221 Setting.date_format = ''
229 Setting.date_format = ''
222 Setting.time_format = ''
230 Setting.time_format = ''
223 assert_equal l_datetime(now), format_time(now)
231 assert_equal l_datetime(now), format_time(now)
224 assert_equal l_time(now), format_time(now, false)
232 assert_equal l_time(now), format_time(now, false)
225 end
233 end
226
234
227 def test_time_format
235 def test_time_format
228 now = Time.now
236 now = Time.now
229 Setting.date_format = '%d %m %Y'
237 Setting.date_format = '%d %m %Y'
230 Setting.time_format = '%H %M'
238 Setting.time_format = '%H %M'
231 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
239 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
232 assert_equal now.strftime('%H %M'), format_time(now, false)
240 assert_equal now.strftime('%H %M'), format_time(now, false)
233 end
241 end
234 end
242 end
General Comments 0
You need to be logged in to leave comments. Login now