From 0dfb7a6850ed96cc1f082e133e4077fcb2bbd390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C4=82LTEANU?= Date: Wed, 21 Oct 2026 20:42:31 +0300 Subject: [PATCH] fix --- app/helpers/application_helper.rb | 45 ++++++++++-- lib/redmine/wiki_formatting/macros.rb | 6 +- test/helpers/application_helper_test.rb | 97 +++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 8 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f906d0ddf..f23de8abc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -990,14 +990,24 @@ module ApplicationHelper @parsed_headings = [] @heading_anchors = {} - @current_section = 0 if options[:edit_section_links] + if options[:edit_section_links] + @current_section = 0 + # Output of macros whose headings are not sections of the page (eg. include) + # is hidden while parsing sections so that it does not get edit links + hidden_macros = @hidden_macros = [] + end - text = parse_non_pre_blocks(text, obj, macros, options) do |txt| - [:parse_wiki_links, :parse_redmine_links].each do |method_name| - send method_name, txt, project, obj, attr, only_path, options + begin + text = parse_non_pre_blocks(text, obj, macros, options) do |txt| + [:parse_wiki_links, :parse_redmine_links].each do |method_name| + send method_name, txt, project, obj, attr, only_path, options + end end + parse_sections(text, project, obj, attr, only_path, options) + ensure + @hidden_macros = nil if hidden_macros end - parse_sections(text, project, obj, attr, only_path, options) + text = restore_hidden_macros(text, hidden_macros) if hidden_macros parse_headings(text, project, obj, attr, only_path, options) if @parsed_headings.any? @@ -1507,7 +1517,7 @@ module ApplicationHelper if execute && orig && orig =~ MACROS_RE esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) if esc.nil? - h(exec_macro(macro, obj, args, block, options) || all) + hide_macro_output(macro) {h(exec_macro(macro, obj, args, block, options) || all)} else h(all) end @@ -1519,6 +1529,29 @@ module ApplicationHelper end end + HIDDEN_MACRO_RE = /\0(\d+)\0/ unless const_defined?(:HIDDEN_MACRO_RE) + + # Replaces the output of a macro with a placeholder while sections are parsed, + # unless the headings it contains are sections of the page (eg. collapse) + def hide_macro_output(macro) + hidden_macros = @hidden_macros + if hidden_macros.nil? || Redmine::WikiFormatting::Macros.available_macros.dig(macro.to_sym, :sections) + return yield + end + + # The whole output is hidden, macros nested in this one don't need to be + @hidden_macros = nil + hidden_macros << yield.to_str + "\0#{hidden_macros.size - 1}\0" + ensure + @hidden_macros = hidden_macros + end + + # Puts back the output of the macros hidden by hide_macro_output + def restore_hidden_macros(text, outputs) + text.gsub(HIDDEN_MACRO_RE) {outputs[$1.to_i] || $&} + end + TOC_RE = /

\{\{((<|<)|(>|>))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE) # Renders the TOC with given headings diff --git a/lib/redmine/wiki_formatting/macros.rb b/lib/redmine/wiki_formatting/macros.rb index f7e6c5016..8c91f1f94 100644 --- a/lib/redmine/wiki_formatting/macros.rb +++ b/lib/redmine/wiki_formatting/macros.rb @@ -100,6 +100,8 @@ module Redmine # * :desc - A description of the macro # * :parse_args => false - Disables arguments parsing (the whole arguments # string is passed to the macro) + # * :sections => true - Headings in the macro output are sections of the + # page and get section edit links (eg. the block of text is part of the page) # # Macro blocks accept 2 or 3 arguments: # * obj: the object that is rendered (eg. an Issue, a WikiContent...) @@ -157,7 +159,7 @@ module Redmine # # If a block of text is given, the closing tag }} must be at the start of a new line. def macro(name, options={}, &block) - options.assert_valid_keys(:desc, :parse_args) + options.assert_valid_keys(:desc, :parse_args, :sections) unless /\A\w+\z/.match?(name.to_s) raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)" end @@ -297,7 +299,7 @@ module Redmine desc "Inserts of collapsed block of text. Examples:\n\n" + "{{collapse\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}\n\n" + "{{collapse(View details...)\nWith custom link text.\n}}" - macro :collapse do |obj, args, text| + macro :collapse, :sections => true do |obj, args, text| html_id = "collapse-#{Redmine::Utils.random_hex(4)}" show_label = args[0] || l(:button_show) hide_label = args[1] || args[0] || l(:button_hide) diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb index c7b1afa4c..cbac6560b 100644 --- a/test/helpers/application_helper_test.rb +++ b/test/helpers/application_helper_test.rb @@ -1685,6 +1685,103 @@ class ApplicationHelperTest < Redmine::HelperTest end end + def test_section_edit_links_should_ignore_headings_from_include_macro + raw = <<~RAW + # Test Wiki + + ## Some local heading + + Local text + + {{include(Included_page)}} + + ## More local heading + + More local text + RAW + @project = Project.find(1) + set_language_if_valid 'en' + with_settings :text_formatting => 'common_mark' do + page = WikiPage.new(:wiki => Wiki.find(1), :title => 'Included_page') + page.content = WikiContent.new( + :text => "# Included Heading 1\n\n## Included Heading 2\n\nIncluded text\n", + :author => User.find(1)) + page.save! + + result = + textilizable( + raw, + :edit_section_links => + {:controller => 'wiki', :action => 'edit', + :project_id => '1', :id => 'Test'} + ).delete("\n") + + # Headings of the included page are rendered... + assert_include 'Included Heading 2', result + # ...but they are not sections of this page, so they must not get edit links + assert_equal %w(2 3), result.scan(/edit\?section=(\d+)/).flatten + + # More local heading should have section=3 edit link + assert_match( + Regexp.new( + '

' \ + '' \ + '' \ + 'Edit this section' \ + '
' \ + '' \ + '

More local heading

' + ), + result + ) + + # The section number must match the one used when editing the page + section, _hash = Redmine::WikiFormatting.formatter.new(raw).get_section(3) + assert section.start_with?('## More local heading') + end + end + + def test_section_edit_links_should_ignore_headings_from_include_macro_inside_collapse_macro + raw = <<~RAW + # Test Wiki + + ## Section A + + {{collapse + ## Section B + + {{include(Included_page)}} + }} + + ## Section C + RAW + @project = Project.find(1) + set_language_if_valid 'en' + with_settings :text_formatting => 'common_mark' do + page = WikiPage.new(:wiki => Wiki.find(1), :title => 'Included_page') + page.content = WikiContent.new(:text => "## Included Heading\n", :author => User.find(1)) + page.save! + + result = + textilizable( + raw, + :edit_section_links => + {:controller => 'wiki', :action => 'edit', + :project_id => '1', :id => 'Test'} + ).delete("\n") + + assert_include 'Included Heading', result + assert_equal %w(2 3 4), result.scan(/edit\?section=(\d+)/).flatten + assert_match( + /id="section-4">.*?<\/a><\/div><\/a>/, + result + ) + + section, _hash = Redmine::WikiFormatting.formatter.new(raw).get_section(4) + assert section.start_with?('## Section C') + end + end + def test_default_formatter with_settings :text_formatting => 'unknown' do text = 'a *link*: http://www.example.net/' -- 2.54.0 (Apple Git-157)