Project

General

Profile

Defect #44623 » 0001-fix.patch

Marius BĂLTEANU, 2026-10-21 22:21

View differences:

app/helpers/application_helper.rb
990 990

  
991 991
    @parsed_headings = []
992 992
    @heading_anchors = {}
993
    @current_section = 0 if options[:edit_section_links]
993
    if options[:edit_section_links]
994
      @current_section = 0
995
      # Output of macros whose headings are not sections of the page (eg. include)
996
      # is hidden while parsing sections so that it does not get edit links
997
      hidden_macros = @hidden_macros = []
998
    end
994 999

  
995
    text = parse_non_pre_blocks(text, obj, macros, options) do |txt|
996
      [:parse_wiki_links, :parse_redmine_links].each do |method_name|
997
        send method_name, txt, project, obj, attr, only_path, options
1000
    begin
1001
      text = parse_non_pre_blocks(text, obj, macros, options) do |txt|
1002
        [:parse_wiki_links, :parse_redmine_links].each do |method_name|
1003
          send method_name, txt, project, obj, attr, only_path, options
1004
        end
998 1005
      end
1006
      parse_sections(text, project, obj, attr, only_path, options)
1007
    ensure
1008
      @hidden_macros = nil if hidden_macros
999 1009
    end
1000
    parse_sections(text, project, obj, attr, only_path, options)
1010
    text = restore_hidden_macros(text, hidden_macros) if hidden_macros
1001 1011
    parse_headings(text, project, obj, attr, only_path, options)
1002 1012

  
1003 1013
    if @parsed_headings.any?
......
1507 1517
      if execute && orig && orig =~ MACROS_RE
1508 1518
        esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
1509 1519
        if esc.nil?
1510
          h(exec_macro(macro, obj, args, block, options) || all)
1520
          hide_macro_output(macro) {h(exec_macro(macro, obj, args, block, options) || all)}
1511 1521
        else
1512 1522
          h(all)
1513 1523
        end
......
1519 1529
    end
1520 1530
  end
1521 1531

  
1532
  HIDDEN_MACRO_RE = /\0(\d+)\0/ unless const_defined?(:HIDDEN_MACRO_RE)
1533

  
1534
  # Replaces the output of a macro with a placeholder while sections are parsed,
1535
  # unless the headings it contains are sections of the page (eg. collapse)
1536
  def hide_macro_output(macro)
1537
    hidden_macros = @hidden_macros
1538
    if hidden_macros.nil? || Redmine::WikiFormatting::Macros.available_macros.dig(macro.to_sym, :sections)
1539
      return yield
1540
    end
1541

  
1542
    # The whole output is hidden, macros nested in this one don't need to be
1543
    @hidden_macros = nil
1544
    hidden_macros << yield.to_str
1545
    "\0#{hidden_macros.size - 1}\0"
1546
  ensure
1547
    @hidden_macros = hidden_macros
1548
  end
1549

  
1550
  # Puts back the output of the macros hidden by hide_macro_output
1551
  def restore_hidden_macros(text, outputs)
1552
    text.gsub(HIDDEN_MACRO_RE) {outputs[$1.to_i] || $&}
1553
  end
1554

  
1522 1555
  TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
1523 1556

  
1524 1557
  # Renders the TOC with given headings
lib/redmine/wiki_formatting/macros.rb
100 100
        # * :desc - A description of the macro
101 101
        # * :parse_args => false - Disables arguments parsing (the whole arguments
102 102
        #   string is passed to the macro)
103
        # * :sections => true - Headings in the macro output are sections of the
104
        #   page and get section edit links (eg. the block of text is part of the page)
103 105
        #
104 106
        # Macro blocks accept 2 or 3 arguments:
105 107
        # * obj: the object that is rendered (eg. an Issue, a WikiContent...)
......
157 159
        #
158 160
        # If a block of text is given, the closing tag }} must be at the start of a new line.
159 161
        def macro(name, options={}, &block)
160
          options.assert_valid_keys(:desc, :parse_args)
162
          options.assert_valid_keys(:desc, :parse_args, :sections)
161 163
          unless /\A\w+\z/.match?(name.to_s)
162 164
            raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)"
163 165
          end
......
297 299
      desc "Inserts of collapsed block of text. Examples:\n\n" +
298 300
             "{{collapse\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}\n\n" +
299 301
             "{{collapse(View details...)\nWith custom link text.\n}}"
300
      macro :collapse do |obj, args, text|
302
      macro :collapse, :sections => true do |obj, args, text|
301 303
        html_id = "collapse-#{Redmine::Utils.random_hex(4)}"
302 304
        show_label = args[0] || l(:button_show)
303 305
        hide_label = args[1] || args[0] || l(:button_hide)
test/helpers/application_helper_test.rb
1685 1685
    end
1686 1686
  end
1687 1687

  
1688
  def test_section_edit_links_should_ignore_headings_from_include_macro
1689
    raw = <<~RAW
1690
      # Test Wiki
1691

  
1692
      ## Some local heading
1693

  
1694
      Local text
1695

  
1696
      {{include(Included_page)}}
1697

  
1698
      ## More local heading
1699

  
1700
      More local text
1701
    RAW
1702
    @project = Project.find(1)
1703
    set_language_if_valid 'en'
1704
    with_settings :text_formatting => 'common_mark' do
1705
      page = WikiPage.new(:wiki => Wiki.find(1), :title => 'Included_page')
1706
      page.content = WikiContent.new(
1707
        :text => "# Included Heading 1\n\n## Included Heading 2\n\nIncluded text\n",
1708
        :author => User.find(1))
1709
      page.save!
1710

  
1711
      result =
1712
        textilizable(
1713
          raw,
1714
          :edit_section_links =>
1715
            {:controller => 'wiki', :action => 'edit',
1716
            :project_id => '1', :id => 'Test'}
1717
        ).delete("\n")
1718

  
1719
      # Headings of the included page are rendered...
1720
      assert_include 'Included Heading 2', result
1721
      # ...but they are not sections of this page, so they must not get edit links
1722
      assert_equal %w(2 3), result.scan(/edit\?section=(\d+)/).flatten
1723

  
1724
      # More local heading should have section=3 edit link
1725
      assert_match(
1726
        Regexp.new(
1727
          '<div class="contextual heading-2" title="Edit this section" id="section-3">' \
1728
          '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\\?section=3">' \
1729
          '<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-.*\\.svg#icon--edit"></use></svg>' \
1730
          '<span class="icon-label">Edit this section</span>' \
1731
          '</a></div>' \
1732
          '<a name="More-local-heading"></a>' \
1733
          '<h2 >More local heading<a href="#More-local-heading" class="wiki-anchor">&para;</a></h2>'
1734
        ),
1735
        result
1736
      )
1737

  
1738
      # The section number must match the one used when editing the page
1739
      section, _hash = Redmine::WikiFormatting.formatter.new(raw).get_section(3)
1740
      assert section.start_with?('## More local heading')
1741
    end
1742
  end
1743

  
1744
  def test_section_edit_links_should_ignore_headings_from_include_macro_inside_collapse_macro
1745
    raw = <<~RAW
1746
      # Test Wiki
1747

  
1748
      ## Section A
1749

  
1750
      {{collapse
1751
      ## Section B
1752

  
1753
      {{include(Included_page)}}
1754
      }}
1755

  
1756
      ## Section C
1757
    RAW
1758
    @project = Project.find(1)
1759
    set_language_if_valid 'en'
1760
    with_settings :text_formatting => 'common_mark' do
1761
      page = WikiPage.new(:wiki => Wiki.find(1), :title => 'Included_page')
1762
      page.content = WikiContent.new(:text => "## Included Heading\n", :author => User.find(1))
1763
      page.save!
1764

  
1765
      result =
1766
        textilizable(
1767
          raw,
1768
          :edit_section_links =>
1769
            {:controller => 'wiki', :action => 'edit',
1770
            :project_id => '1', :id => 'Test'}
1771
        ).delete("\n")
1772

  
1773
      assert_include 'Included Heading', result
1774
      assert_equal %w(2 3 4), result.scan(/edit\?section=(\d+)/).flatten
1775
      assert_match(
1776
        /id="section-4"><a class="icon-only icon-edit" href="[^"]*section=4">.*?<\/a><\/div><a name="Section-C"><\/a>/,
1777
        result
1778
      )
1779

  
1780
      section, _hash = Redmine::WikiFormatting.formatter.new(raw).get_section(4)
1781
      assert section.start_with?('## Section C')
1782
    end
1783
  end
1784

  
1688 1785
  def test_default_formatter
1689 1786
    with_settings :text_formatting => 'unknown' do
1690 1787
      text = 'a *link*: http://www.example.net/'
(5-5/5)