Project

General

Profile

Feature #2222 » redmine-0.9-r3357-wiki_section-v1.patch

Wiki section edit patch for redmine 0.9 trunk r3357. - Manuel Studer, 2010-02-01 15:43

View differences:

app/helpers/wiki_helper.rb (working copy)
66 66
    end
67 67
    simple_format_without_paragraph(words.join(' '))
68 68
  end
69
  
70
  # Manuel Studer: parse text to find sections and numerate them
71
  def parse_sections(text)
72

  
73
    # scan text and replace line
74
    section_index = 1
75
    doc = Nokogiri::HTML.parse(text,nil,'utf-8')
76

  
77
    doc.xpath('//h1 | //h2 | //h3').each do |node|
78

  
79
      span_node = Nokogiri::XML::Node.new('span',doc)
80
      span_node['class'] = 'edit-section'
81
      bracket_open = Nokogiri::XML::Text.new('[',doc)
82
      bracket_close = Nokogiri::XML::Text.new(']',doc)
83
      span_node.add_child(bracket_open)
84

  
85
      link_node = Nokogiri::XML::Node.new('a',doc)
86
      #link_node['href'] = "#{@page.title}/edit?section=#{section_index}"
87
      # ok, this is not very nice, I know.
88
      link_node['href'] = @controller.send(:url_for,{:action => "edit", :page => @page.title}) << "?section=#{section_index}"
89
      #link_node['class'] = 'icon icon-edit'
90
      link_node.content = l(:button_edit)
91
      #link_node['title'] = l(:button_edit) << ": #{node.text.chop.chop}"
92

  
93
      span_node.add_child(link_node)
94
      span_node.add_child(bracket_close)
95

  
96
      node.children.first.add_next_sibling(span_node)
97

  
98
      section_index += 1
99
    end
100

  
101
    return doc.xpath('//body/.').to_xhtml(:encoding => 'utf8')
102
  end
103

  
104
  # Manuel Studer: Collect content between two sections
105
  # section start included, section end removed
106
  def collect_between(first,last)
107
    result = ''
108
    until first == last
109
      result << first.to_s
110
      first = first.next_sibling
111
    end
112
    result
113
  end
114

  
115
  def get_section(html_text,section_number)
116
    doc = Nokogiri::HTML.parse(html_text)
117
    sections = doc.xpath('//h1 | //h2 | //h3')
118
    return collect_between(sections[section_number-1],sections[section_number])
119
  end
120

  
69 121
end
app/helpers/application_helper.rb (working copy)
402 402
    end
403 403
    return '' if text.blank?
404 404

  
405
    textile_text = text
406

  
405 407
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
408

  
409
    # Manuel Studer: wiki edit links for sections h1, h2, and h3
410
    # small hack, so that the content is being parsed for editing sections if allowed.
411
    # Do not apply the hack if there is an "include" which means externally added content from other wikis
412

  
413
    if @content.type == WikiContent && @editable && !textile_text.include?("{{include(")
414
      if authorize_for(:wiki, :edit) && @content.version == @page.content.version
415
        text = parse_sections(text)
416
      end
417
    end
406 418
    
407 419
    only_path = options.delete(:only_path) == false ? false : true
408 420

  
app/controllers/application_controller.rb (working copy)
17 17

  
18 18
require 'uri'
19 19
require 'cgi'
20
require 'nokogiri'
20 21

  
21 22
class ApplicationController < ActionController::Base
22 23
  include Redmine::I18n
app/controllers/wiki_controller.rb (working copy)
61 61
  
62 62
  # edit an existing page or a new one
63 63
  def edit
64
    @page = @wiki.find_or_new_page(params[:page])    
65
    return render_403 unless editable?
66
    @page.content = WikiContent.new(:page => @page) if @page.new_record?
67
    
68
    @content = @page.content_for_version(params[:version])
69
    @content.text = initial_page_content(@page) if @content.text.blank?
70
    # don't keep previous comment
71
    @content.comments = nil
72
    if request.get?
64
    # Manuel Studer: check if it contains a section number as parameter
65
    if params[:section]
66
      # do special treatment to edit section
67
      @page = @wiki.find_or_new_page(params[:page])
68
      return render_403 unless editable?
69
      @page.content = WikiContent.new(:page => @page) if @page.new_record?
70

  
71
      @content = @page.content_for_version(params[:version])
72
      @content.text = initial_page_content(@page) if @content.text.blank?
73
      # don't keep previous comment
74
      @content.comments = nil
75
      @section_id = params[:section]
76
      # change text here according to section
77
      @content.text = get_textsection(@content.text,@section_id.to_i)
73 78
      # To prevent StaleObjectError exception when reverting to a previous version
74 79
      @content.version = @page.content.version
75
    else
76
      if !@page.new_record? && @content.text == params[:content][:text]
80
      # Manuel Studer: this is for saving a section which has been edited
81
    elsif request.post? && params[:content][:section_edit].to_i != 0
82
      section_id = params[:content][:section_edit].to_i
83
      new_section_text = params[:content][:text]
84

  
85
      # get the original content from the db
86
      @page = @wiki.find_or_new_page(params[:page])
87
      return render_403 unless editable?
88
      @content = @page.content_for_version(params[:version])
89

  
90
      # don't keep previous comment
91
      @content.comments = nil
92

  
93
      text_original = @content.text
94
      modify_text_endchars(new_section_text)
95

  
96
      # replace the section
97
      rebuilded_text = text_original.sub(get_textsection(text_original, section_id), new_section_text)
98

  
99
      # create anchor so that after saving a page the browser jumps to the
100
      # edited section
101
      anchor = generate_anchor(rebuilded_text, section_id)
102

  
103
      if !@page.new_record? && @content.text == rebuilded_text
77 104
        # don't save if text wasn't changed
78
        redirect_to :action => 'index', :id => @project, :page => @page.title
105
        redirect_to :action => 'index', :id => @project, :page => @page.title, :anchor => anchor
79 106
        return
80 107
      end
81 108
      #@content.text = params[:content][:text]
82 109
      #@content.comments = params[:content][:comments]
83
      @content.attributes = params[:content]
110
      #@content.attributes = params[:content]
111

  
112
      # manually set the content
113

  
114
      @content.text = rebuilded_text
115
      @content.comments = params[:content][:comments]
116
      @content.version = params[:content][:version]
84 117
      @content.author = User.current
85 118
      # if page is new @page.save will also save content, but not if page isn't a new record
86 119
      if (@page.new_record? ? @page.save : @content.save)
87 120
        call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
88
        redirect_to :action => 'index', :id => @project, :page => @page.title
121
        #redirect_to :action => 'index', :id => @project, :page => @page.title
122
        redirect_to :action => 'index', :id => @project, :page => @page.title, :anchor => anchor
89 123
      end
124

  
125
      # this is invoked if no section has been chosen
126
    else
127
      # Manuel Studer: section id 0 means no section has been chosen, this means the whole
128
      # page is selected
129
      @section_id = 0
130

  
131
      @page = @wiki.find_or_new_page(params[:page])
132
      return render_403 unless editable?
133
      @page.content = WikiContent.new(:page => @page) if @page.new_record?
134

  
135
      @content = @page.content_for_version(params[:version])
136
      @content.text = initial_page_content(@page) if @content.text.blank?
137
      # don't keep previous comment
138
      @content.comments = nil
139
      if request.get?
140
        # To prevent StaleObjectError exception when reverting to a previous version
141
        @content.version = @page.content.version
142
      else
143
        if !@page.new_record? && @content.text == params[:content][:text]
144
          # don't save if text wasn't changed
145
          redirect_to :action => 'index', :id => @project, :page => @page.title
146
          return
147
        end
148
        # Manuel Studer: manually set the content
149
        #@content.attributes = params[:content]
150
        @content.text = params[:content][:text]
151
        @content.comments = params[:content][:comments]
152
        @content.version = params[:content][:version]
153

  
154
        @content.author = User.current
155
        # if page is new @page.save will also save content, but not if page isn't a new record
156
        if (@page.new_record? ? @page.save : @content.save)
157
          redirect_to :action => 'index', :id => @project, :page => @page.title
158
        end
159
      end
90 160
    end
91 161
  rescue ActiveRecord::StaleObjectError
92 162
    # Optimistic locking exception
......
234 304
    extend helper unless self.instance_of?(helper)
235 305
    helper.instance_method(:initial_page_content).bind(self).call(page)
236 306
  end
307

  
308
  
309
  # Manuel Studer: get the text of a given section
310
  def get_textsection(textile_text, section_number)
311
    current_section_number = 1
312
    offset = 0
313
    current_index = 0
314

  
315
    # the index where the found section starts
316
    start_section_index = 0
317
    # the level of the found section
318
    section_level = 0
319

  
320
    while !current_index.nil? do
321
      current_index =  textile_text.index(/h[1|2|3|4|5|6]\. ./i,offset)
322
      if !current_index.nil? then
323

  
324
        # we have found our start section
325
        if current_section_number == section_number
326
          start_section_index = current_index
327
          section_level = textile_text[current_index+1..current_index+1].to_i
328
        end
329

  
330
        # now find the next section which has a smaller OR equal level than
331
        # the found section
332
        if current_section_number > section_number
333
          current_section_level = textile_text[current_index+1..current_index+1].to_i
334
          if current_section_level <= section_level
335
            return textile_text[start_section_index..current_index-1]
336
          end
337
        end
338
        offset = current_index + 3
339
        current_section_number += 1
340
      end
341
    end
342
    # return all up to the final char if it was the last h tag
343
    return textile_text[start_section_index..textile_text.size-1]
344
  end
345

  
346

  
347
  # Manuel Studer: check if there is whole empty line at the end: which means
348
  # 2x enter key: one enter key is 13 (\r) + 10 (\n)
349
  def modify_text_endchars(text)
350
    nst_last_char = text[text.size-1]
351
    nst_2last_char = text[text.size-2]
352
    nst_3last_char = text[text.size-3]
353
    nst_4last_char = text[text.size-4]
354

  
355
    if (nst_last_char == 10 && nst_2last_char == 13) &&
356
        (nst_3last_char == 10 && nst_4last_char == 13)
357
      # all ok do nothing
358
    elsif nst_last_char == 10 && nst_2last_char == 13
359
      # add another one
360
      text << "\r\n"
361
    else
362
      text << "\r\n\r\n"
363
    end
364
    return text
365
  end
366

  
367

  
368
  # generate the anchor from a given section
369
  def generate_anchor(textile_text, section_number)
370
    current_section_number = 1
371
    offset = 0
372
    current_index = 0
373

  
374
    anchor = ''
375

  
376
    while !current_index.nil? do
377
      current_index =  textile_text.index(/h[1|2|3|4|5|6]\. ./i,offset)
378
      if !current_index.nil? then
379

  
380
        # we have found our start section
381
        if current_section_number == section_number
382
          # find index of next newline
383
          index_next_newline = textile_text.index("\n",current_index)
384

  
385
          if !index_next_newline.nil? then
386
            anchor = textile_text[current_index+3..index_next_newline].strip
387
            # replaces non word caracters by dashes
388
            anchor = anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
389
            return anchor
390
          else
391
            anchor = textile_text[current_index+3..textile_text.size-1].strip
392
            # replaces non word caracters by dashes
393
            anchor = anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
394
            return anchor
395
          end
396

  
397
        end
398
        offset = current_index + 3
399
        current_section_number += 1
400

  
401
      else return anchor
402
      end
403
    end
404
  end
237 405
end
406

  
app/views/wiki/edit.rhtml (working copy)
2 2

  
3 3
<% form_for :content, @content, :url => {:action => 'edit', :page => @page.title}, :html => {:id => 'wiki_form'} do |f| %>
4 4
<%= f.hidden_field :version %>
5
<%= f.hidden_field :section_edit,:value => @section_id%>
5 6
<%= error_messages_for 'content' %>
6 7

  
7 8
<p><%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %></p>
public/stylesheets/application.css (working copy)
670 670
}
671 671
div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
672 672

  
673
a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
674
a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
673
/* a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
674
 * a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
675
 */
676
a.wiki-anchor {color:white; margin-left: 6px; text-decoration: none; }
675 677
h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
676 678

  
677 679
div.wiki img { vertical-align: middle; } 
......
708 710
background-image:url('../images/close_hl.png');
709 711
}
710 712

  
713
span.edit-section{
714
    margin-left: 5px;
715
    font-size:11px;
716
    font-weight: normal;
717
}
718

  
711 719
/***** Gantt chart *****/
712 720
.gantt_hdr {
713 721
  position:absolute;
(3-3/4)