Index: app/helpers/wiki_helper.rb =================================================================== --- app/helpers/wiki_helper.rb (revision 3357) +++ app/helpers/wiki_helper.rb (working copy) @@ -66,4 +66,56 @@ end simple_format_without_paragraph(words.join(' ')) end + + # Manuel Studer: parse text to find sections and numerate them + def parse_sections(text) + + # scan text and replace line + section_index = 1 + doc = Nokogiri::HTML.parse(text,nil,'utf-8') + + doc.xpath('//h1 | //h2 | //h3').each do |node| + + span_node = Nokogiri::XML::Node.new('span',doc) + span_node['class'] = 'edit-section' + bracket_open = Nokogiri::XML::Text.new('[',doc) + bracket_close = Nokogiri::XML::Text.new(']',doc) + span_node.add_child(bracket_open) + + link_node = Nokogiri::XML::Node.new('a',doc) + #link_node['href'] = "#{@page.title}/edit?section=#{section_index}" + # ok, this is not very nice, I know. + link_node['href'] = @controller.send(:url_for,{:action => "edit", :page => @page.title}) << "?section=#{section_index}" + #link_node['class'] = 'icon icon-edit' + link_node.content = l(:button_edit) + #link_node['title'] = l(:button_edit) << ": #{node.text.chop.chop}" + + span_node.add_child(link_node) + span_node.add_child(bracket_close) + + node.children.first.add_next_sibling(span_node) + + section_index += 1 + end + + return doc.xpath('//body/.').to_xhtml(:encoding => 'utf8') + end + + # Manuel Studer: Collect content between two sections + # section start included, section end removed + def collect_between(first,last) + result = '' + until first == last + result << first.to_s + first = first.next_sibling + end + result + end + + def get_section(html_text,section_number) + doc = Nokogiri::HTML.parse(html_text) + sections = doc.xpath('//h1 | //h2 | //h3') + return collect_between(sections[section_number-1],sections[section_number]) + end + end Index: app/helpers/application_helper.rb =================================================================== --- app/helpers/application_helper.rb (revision 3357) +++ app/helpers/application_helper.rb (working copy) @@ -402,7 +402,19 @@ end return '' if text.blank? + textile_text = text + text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) } + + # Manuel Studer: wiki edit links for sections h1, h2, and h3 + # small hack, so that the content is being parsed for editing sections if allowed. + # Do not apply the hack if there is an "include" which means externally added content from other wikis + + if @content.type == WikiContent && @editable && !textile_text.include?("{{include(") + if authorize_for(:wiki, :edit) && @content.version == @page.content.version + text = parse_sections(text) + end + end only_path = options.delete(:only_path) == false ? false : true Index: app/controllers/application_controller.rb =================================================================== --- app/controllers/application_controller.rb (revision 3357) +++ app/controllers/application_controller.rb (working copy) @@ -17,6 +17,7 @@ require 'uri' require 'cgi' +require 'nokogiri' class ApplicationController < ActionController::Base include Redmine::I18n Index: app/controllers/wiki_controller.rb =================================================================== --- app/controllers/wiki_controller.rb (revision 3357) +++ app/controllers/wiki_controller.rb (working copy) @@ -61,32 +61,102 @@ # edit an existing page or a new one def edit - @page = @wiki.find_or_new_page(params[:page]) - return render_403 unless editable? - @page.content = WikiContent.new(:page => @page) if @page.new_record? - - @content = @page.content_for_version(params[:version]) - @content.text = initial_page_content(@page) if @content.text.blank? - # don't keep previous comment - @content.comments = nil - if request.get? + # Manuel Studer: check if it contains a section number as parameter + if params[:section] + # do special treatment to edit section + @page = @wiki.find_or_new_page(params[:page]) + return render_403 unless editable? + @page.content = WikiContent.new(:page => @page) if @page.new_record? + + @content = @page.content_for_version(params[:version]) + @content.text = initial_page_content(@page) if @content.text.blank? + # don't keep previous comment + @content.comments = nil + @section_id = params[:section] + # change text here according to section + @content.text = get_textsection(@content.text,@section_id.to_i) # To prevent StaleObjectError exception when reverting to a previous version @content.version = @page.content.version - else - if !@page.new_record? && @content.text == params[:content][:text] + # Manuel Studer: this is for saving a section which has been edited + elsif request.post? && params[:content][:section_edit].to_i != 0 + section_id = params[:content][:section_edit].to_i + new_section_text = params[:content][:text] + + # get the original content from the db + @page = @wiki.find_or_new_page(params[:page]) + return render_403 unless editable? + @content = @page.content_for_version(params[:version]) + + # don't keep previous comment + @content.comments = nil + + text_original = @content.text + modify_text_endchars(new_section_text) + + # replace the section + rebuilded_text = text_original.sub(get_textsection(text_original, section_id), new_section_text) + + # create anchor so that after saving a page the browser jumps to the + # edited section + anchor = generate_anchor(rebuilded_text, section_id) + + if !@page.new_record? && @content.text == rebuilded_text # don't save if text wasn't changed - redirect_to :action => 'index', :id => @project, :page => @page.title + redirect_to :action => 'index', :id => @project, :page => @page.title, :anchor => anchor return end #@content.text = params[:content][:text] #@content.comments = params[:content][:comments] - @content.attributes = params[:content] + #@content.attributes = params[:content] + + # manually set the content + + @content.text = rebuilded_text + @content.comments = params[:content][:comments] + @content.version = params[:content][:version] @content.author = User.current # if page is new @page.save will also save content, but not if page isn't a new record if (@page.new_record? ? @page.save : @content.save) call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page}) - redirect_to :action => 'index', :id => @project, :page => @page.title + #redirect_to :action => 'index', :id => @project, :page => @page.title + redirect_to :action => 'index', :id => @project, :page => @page.title, :anchor => anchor end + + # this is invoked if no section has been chosen + else + # Manuel Studer: section id 0 means no section has been chosen, this means the whole + # page is selected + @section_id = 0 + + @page = @wiki.find_or_new_page(params[:page]) + return render_403 unless editable? + @page.content = WikiContent.new(:page => @page) if @page.new_record? + + @content = @page.content_for_version(params[:version]) + @content.text = initial_page_content(@page) if @content.text.blank? + # don't keep previous comment + @content.comments = nil + if request.get? + # To prevent StaleObjectError exception when reverting to a previous version + @content.version = @page.content.version + else + if !@page.new_record? && @content.text == params[:content][:text] + # don't save if text wasn't changed + redirect_to :action => 'index', :id => @project, :page => @page.title + return + end + # Manuel Studer: manually set the content + #@content.attributes = params[:content] + @content.text = params[:content][:text] + @content.comments = params[:content][:comments] + @content.version = params[:content][:version] + + @content.author = User.current + # if page is new @page.save will also save content, but not if page isn't a new record + if (@page.new_record? ? @page.save : @content.save) + redirect_to :action => 'index', :id => @project, :page => @page.title + end + end end rescue ActiveRecord::StaleObjectError # Optimistic locking exception @@ -234,4 +304,103 @@ extend helper unless self.instance_of?(helper) helper.instance_method(:initial_page_content).bind(self).call(page) end + + + # Manuel Studer: get the text of a given section + def get_textsection(textile_text, section_number) + current_section_number = 1 + offset = 0 + current_index = 0 + + # the index where the found section starts + start_section_index = 0 + # the level of the found section + section_level = 0 + + while !current_index.nil? do + current_index = textile_text.index(/h[1|2|3|4|5|6]\. ./i,offset) + if !current_index.nil? then + + # we have found our start section + if current_section_number == section_number + start_section_index = current_index + section_level = textile_text[current_index+1..current_index+1].to_i + end + + # now find the next section which has a smaller OR equal level than + # the found section + if current_section_number > section_number + current_section_level = textile_text[current_index+1..current_index+1].to_i + if current_section_level <= section_level + return textile_text[start_section_index..current_index-1] + end + end + offset = current_index + 3 + current_section_number += 1 + end + end + # return all up to the final char if it was the last h tag + return textile_text[start_section_index..textile_text.size-1] + end + + + # Manuel Studer: check if there is whole empty line at the end: which means + # 2x enter key: one enter key is 13 (\r) + 10 (\n) + def modify_text_endchars(text) + nst_last_char = text[text.size-1] + nst_2last_char = text[text.size-2] + nst_3last_char = text[text.size-3] + nst_4last_char = text[text.size-4] + + if (nst_last_char == 10 && nst_2last_char == 13) && + (nst_3last_char == 10 && nst_4last_char == 13) + # all ok do nothing + elsif nst_last_char == 10 && nst_2last_char == 13 + # add another one + text << "\r\n" + else + text << "\r\n\r\n" + end + return text + end + + + # generate the anchor from a given section + def generate_anchor(textile_text, section_number) + current_section_number = 1 + offset = 0 + current_index = 0 + + anchor = '' + + while !current_index.nil? do + current_index = textile_text.index(/h[1|2|3|4|5|6]\. ./i,offset) + if !current_index.nil? then + + # we have found our start section + if current_section_number == section_number + # find index of next newline + index_next_newline = textile_text.index("\n",current_index) + + if !index_next_newline.nil? then + anchor = textile_text[current_index+3..index_next_newline].strip + # replaces non word caracters by dashes + anchor = anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + return anchor + else + anchor = textile_text[current_index+3..textile_text.size-1].strip + # replaces non word caracters by dashes + anchor = anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + return anchor + end + + end + offset = current_index + 3 + current_section_number += 1 + + else return anchor + end + end + end end + Index: app/views/wiki/edit.rhtml =================================================================== --- app/views/wiki/edit.rhtml (revision 3357) +++ app/views/wiki/edit.rhtml (working copy) @@ -2,6 +2,7 @@ <% form_for :content, @content, :url => {:action => 'edit', :page => @page.title}, :html => {:id => 'wiki_form'} do |f| %> <%= f.hidden_field :version %> +<%= f.hidden_field :section_edit,:value => @section_id%> <%= error_messages_for 'content' %>
<%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %>
Index: public/stylesheets/application.css =================================================================== --- public/stylesheets/application.css (revision 3357) +++ public/stylesheets/application.css (working copy) @@ -670,8 +670,10 @@ } div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;} -a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; } -a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } +/* a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; } + * a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } + */ +a.wiki-anchor {color:white; margin-left: 6px; text-decoration: none; } h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; } div.wiki img { vertical-align: middle; } @@ -708,6 +710,12 @@ background-image:url('../images/close_hl.png'); } +span.edit-section{ + margin-left: 5px; + font-size:11px; + font-weight: normal; +} + /***** Gantt chart *****/ .gantt_hdr { position:absolute;