mirror of
https://github.com/redmine/redmine.git
synced 2026-03-18 02:20:59 +01:00
Support Loofah for textile (#43643):
* Add TablesortScrubber to both CommonMark and Textile Formatters. It will now be determined on the server-side whether to use tablesort. * Migrate tablesort to Stimulus controller. * Update tablesort to v5.7.0. * Switch tablesort to ESM. Patch by Takashi Kato (user:tohosaku). git-svn-id: https://svn.redmine.org/redmine/trunk@24357 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
@@ -1170,7 +1170,6 @@ $(document).ready(function(){
|
||||
data: "text=" + element + '&' + attachments,
|
||||
success: function(data){
|
||||
jstBlock.find('.wiki-preview').html(data);
|
||||
setupWikiTableSortableHeader();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1439,7 +1438,6 @@ $(document).ready(defaultFocus);
|
||||
$(document).ready(setupAttachmentDetail);
|
||||
$(document).ready(setupTabs);
|
||||
$(document).ready(setupFilePreviewNavigation);
|
||||
$(document).ready(setupWikiTableSortableHeader);
|
||||
$(document).on('focus', '[data-auto-complete=true]', function(event) {
|
||||
inlineAutoComplete(event.target);
|
||||
});
|
||||
|
||||
@@ -1801,9 +1801,6 @@ module ApplicationHelper
|
||||
'rails-ujs',
|
||||
'tribute-5.1.3.min'
|
||||
)
|
||||
if Setting.wiki_tablesort_enabled?
|
||||
tags << javascript_include_tag('tablesort-5.2.1.min.js', 'tablesort-5.2.1.number.min.js')
|
||||
end
|
||||
tags << javascript_include_tag('application-legacy', 'responsive')
|
||||
unless User.current.pref.warn_on_leaving_unsaved == '0'
|
||||
warn_text = escape_javascript(l(:text_warn_on_leaving_unsaved))
|
||||
|
||||
18
app/javascript/controllers/tablesort_controller.js
Normal file
18
app/javascript/controllers/tablesort_controller.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Redmine - project management software
|
||||
* Copyright (C) 2006- Jean-Philippe Lang
|
||||
* This code is released under the GNU General Public License.
|
||||
*/
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import Tablesort from 'tablesort';
|
||||
import numberPlugin from 'tablesort.number';
|
||||
|
||||
// Extensions must be loaded explicitly
|
||||
Tablesort.extend(numberPlugin.name, numberPlugin.pattern, numberPlugin.sort);
|
||||
|
||||
// Connects to data-controller="tablesort"
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
new Tablesort(this.element);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
} else {
|
||||
journal_header.append('<%= escape_javascript(render_journal_update_info(@journal)) %>');
|
||||
}
|
||||
setupWikiTableSortableHeader();
|
||||
setupCopyButtonsToPreElements();
|
||||
setupHoverTooltips();
|
||||
<% end %>
|
||||
|
||||
@@ -7,3 +7,5 @@ pin "@hotwired/stimulus", to: "stimulus.min.js"
|
||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
||||
pin "turndown" # @7.2.0
|
||||
pin_all_from "app/javascript/controllers", under: "controllers"
|
||||
pin "tablesort", to: "tablesort.min.js"
|
||||
pin "tablesort.number", to: "tablesort.number.min.js"
|
||||
|
||||
@@ -55,6 +55,7 @@ module Redmine
|
||||
SANITIZER = SanitizationFilter.new
|
||||
SCRUBBERS = [
|
||||
SyntaxHighlightScrubber.new,
|
||||
Redmine::WikiFormatting::TablesortScrubber.new,
|
||||
FixupAutoLinksScrubber.new,
|
||||
ExternalLinksScrubber.new,
|
||||
AlertsIconsScrubber.new
|
||||
|
||||
27
lib/redmine/wiki_formatting/tablesort_scrubber.rb
Normal file
27
lib/redmine/wiki_formatting/tablesort_scrubber.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006- Jean-Philippe Lang
|
||||
# This code is released under the GNU General Public License.
|
||||
|
||||
module Redmine
|
||||
module WikiFormatting
|
||||
class TablesortScrubber < Loofah::Scrubber
|
||||
def scrub(node)
|
||||
return if !Setting.wiki_tablesort_enabled? || node.name != 'table'
|
||||
|
||||
rows = node.search('tr')
|
||||
return if rows.size < 3
|
||||
|
||||
tr = rows.first
|
||||
if tr.search('th').present?
|
||||
node['data-controller'] = 'tablesort'
|
||||
tr['data-sort-method'] = 'none'
|
||||
tr.search('td').each do |td|
|
||||
td['data-sort-method'] = 'none'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -20,11 +20,33 @@
|
||||
module Redmine
|
||||
module WikiFormatting
|
||||
module Textile
|
||||
class Formatter < RedCloth3
|
||||
include ActionView::Helpers::TagHelper
|
||||
include Redmine::WikiFormatting::LinksHelper
|
||||
SCRUBBERS = [
|
||||
Redmine::WikiFormatting::TablesortScrubber.new
|
||||
]
|
||||
|
||||
class Formatter
|
||||
include Redmine::WikiFormatting::SectionHelper
|
||||
|
||||
extend Forwardable
|
||||
def_delegators :@filter, :extract_sections, :rip_offtags
|
||||
|
||||
def initialize(args)
|
||||
@filter = Filter.new(args)
|
||||
end
|
||||
|
||||
def to_html(*rules)
|
||||
html = @filter.to_html(rules)
|
||||
fragment = Loofah.html5_fragment(html)
|
||||
SCRUBBERS.each do |scrubber|
|
||||
fragment.scrub!(scrubber)
|
||||
end
|
||||
fragment.to_s
|
||||
end
|
||||
end
|
||||
|
||||
class Filter < RedCloth3
|
||||
include Redmine::WikiFormatting::LinksHelper
|
||||
|
||||
alias :inline_auto_link :auto_link!
|
||||
alias :inline_auto_mailto :auto_mailto!
|
||||
alias :inline_restore_redmine_links :restore_redmine_links
|
||||
@@ -41,7 +63,7 @@ module Redmine
|
||||
|
||||
def to_html(*rules)
|
||||
@toc = []
|
||||
super(*RULES).to_s
|
||||
super(*RULES)
|
||||
end
|
||||
|
||||
def extract_sections(index)
|
||||
|
||||
@@ -80,7 +80,7 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
'(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
|
||||
'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
|
||||
'http://foo.bar/page?p=1&t=z&s=' =>
|
||||
'<a class="external" href="http://foo.bar/page?p=1&t=z&s=">http://foo.bar/page?p=1&t=z&s=</a>',
|
||||
'<a class="external" href="http://foo.bar/page?p=1&t=z&s=">http://foo.bar/page?p=1&t=z&s=</a>',
|
||||
'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
|
||||
'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
|
||||
'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
|
||||
@@ -92,7 +92,7 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
'<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">' \
|
||||
'http://example.net/path!602815048C7B5C20!302.html</a>',
|
||||
# escaping
|
||||
'http://foo"bar' => '<a class="external" href="http://foo"bar">http://foo"bar</a>',
|
||||
'http://foo"bar' => '<a class="external" href="http://foo"bar">http://foo"bar</a>',
|
||||
# wrap in angle brackets
|
||||
'<http://foo.bar>' => '<<a class="external" href="http://foo.bar">http://foo.bar</a>>',
|
||||
# invalid urls
|
||||
@@ -130,22 +130,22 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
|
||||
def test_inline_images
|
||||
to_test = {
|
||||
'!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
|
||||
'!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="">',
|
||||
'floating !>http://foo.bar/image.jpg!' =>
|
||||
'floating <span style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></span>',
|
||||
'floating <span style="float:right"><img src="http://foo.bar/image.jpg" alt=""></span>',
|
||||
'with class !(some-class)http://foo.bar/image.jpg!' =>
|
||||
'with class <img src="http://foo.bar/image.jpg" class="wiki-class-some-class" alt="" />',
|
||||
'with class <img src="http://foo.bar/image.jpg" class="wiki-class-some-class" alt="">',
|
||||
'with class !(wiki-class-foo)http://foo.bar/image.jpg!' =>
|
||||
'with class <img src="http://foo.bar/image.jpg" class="wiki-class-foo" alt="" />',
|
||||
'with class <img src="http://foo.bar/image.jpg" class="wiki-class-foo" alt="">',
|
||||
'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' =>
|
||||
'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
|
||||
'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="">',
|
||||
'with title !http://foo.bar/image.jpg(This is a title)!' =>
|
||||
'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
|
||||
'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title">',
|
||||
'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' =>
|
||||
'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted "title"" ' \
|
||||
'alt="This is a double-quoted "title"" />',
|
||||
'alt="This is a double-quoted "title"">',
|
||||
'with query string !http://foo.bar/image.cgi?a=1&b=2!' =>
|
||||
'with query string <img src="http://foo.bar/image.cgi?a=1&b=2" alt="" />'
|
||||
'with query string <img src="http://foo.bar/image.cgi?a=1&b=2" alt="">'
|
||||
}
|
||||
with_settings :text_formatting => 'textile' do
|
||||
to_test.each {|text, result| assert_equal "<p>#{result}</p>", textilizable(text)}
|
||||
@@ -161,24 +161,24 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
p=. !bar.gif!
|
||||
RAW
|
||||
with_settings :text_formatting => 'textile' do
|
||||
assert textilizable(raw).include?('<img src="foo.png" alt="" />')
|
||||
assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
|
||||
assert textilizable(raw).include?('<img src="foo.png" alt="">')
|
||||
assert textilizable(raw).include?('<img src="bar.gif" alt="">')
|
||||
end
|
||||
end
|
||||
|
||||
def test_attached_images
|
||||
to_test = {
|
||||
'Inline image: !logo.gif!' =>
|
||||
'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy" />',
|
||||
'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy">',
|
||||
'Inline image: !logo.GIF!' =>
|
||||
'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy" />',
|
||||
'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy">',
|
||||
'Inline WebP image: !logo.webp!' =>
|
||||
'Inline WebP image: <img src="/attachments/download/24/logo.webp" title="WebP image" alt="WebP image" loading="lazy" />',
|
||||
'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
|
||||
'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
|
||||
'Inline WebP image: <img src="/attachments/download/24/logo.webp" title="WebP image" alt="WebP image" loading="lazy">',
|
||||
'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="">',
|
||||
'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="">',
|
||||
# link image
|
||||
'!logo.gif!:http://foo.bar/' =>
|
||||
'<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy" /></a>',
|
||||
'<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy"></a>',
|
||||
}
|
||||
attachments = Attachment.all
|
||||
with_settings :text_formatting => 'textile' do
|
||||
@@ -190,31 +190,31 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
attachments = Attachment.all
|
||||
with_settings text_formatting: 'textile' do
|
||||
# When alt text is set
|
||||
assert_match %r[<img src=".+?" title="alt text" alt="alt text" loading=".+?" />],
|
||||
assert_match %r[<img src=".+?" title="alt text" alt="alt text" loading=".+?">],
|
||||
textilizable('!logo.gif(alt text)!', attachments: attachments)
|
||||
|
||||
# When alt text and style are set
|
||||
assert_match %r[<img src=".+?" title="alt text" alt="alt text" loading=".+?" style="width:100px;" />],
|
||||
assert_match %r[<img src=".+?" title="alt text" alt="alt text" loading=".+?" style="width:100px;">],
|
||||
textilizable('!{width:100px}logo.gif(alt text)!', attachments: attachments)
|
||||
|
||||
# When alt text is not set
|
||||
assert_match %r[<img src=".+?" title="This is a logo" alt="This is a logo" loading=".+?" />],
|
||||
assert_match %r[<img src=".+?" title="This is a logo" alt="This is a logo" loading=".+?">],
|
||||
textilizable('!logo.gif!', attachments: attachments)
|
||||
|
||||
# When alt text is not set and the attachment has no description
|
||||
assert_match %r[<img src=".+?" alt="" loading=".+?" />],
|
||||
assert_match %r[<img src=".+?" alt="" loading=".+?">],
|
||||
textilizable('!testfile.PNG!', attachments: attachments)
|
||||
|
||||
# When no matching attachments are found
|
||||
assert_match %r[<img src=".+?" alt="" />],
|
||||
assert_match %r[<img src=".+?" alt="">],
|
||||
textilizable('!no-match.jpg!', attachments: attachments)
|
||||
assert_match %r[<img src=".+?" alt="alt text" />],
|
||||
assert_match %r[<img src=".+?" alt="alt text">],
|
||||
textilizable('!no-match.jpg(alt text)!', attachments: attachments)
|
||||
|
||||
# When no attachment is registered
|
||||
assert_match %r[<img src=".+?" alt="" />],
|
||||
assert_match %r[<img src=".+?" alt="">],
|
||||
textilizable('!logo.gif!', attachments: [])
|
||||
assert_match %r[<img src=".+?" alt="alt text" />],
|
||||
assert_match %r[<img src=".+?" alt="alt text">],
|
||||
textilizable('!logo.gif(alt text)!', attachments: [])
|
||||
end
|
||||
end
|
||||
@@ -232,8 +232,8 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
RAW
|
||||
|
||||
with_settings :text_formatting => 'textile' do
|
||||
assert textilizable(raw, :object => journal).include?("<img src=\"/attachments/download/#{attachment_1.id}/attached_on_issue.png\" alt=\"\" loading=\"lazy\" />")
|
||||
assert textilizable(raw, :object => journal).include?("<img src=\"/attachments/download/#{attachment_2.id}/attached_on_journal.png\" alt=\"\" loading=\"lazy\" />")
|
||||
assert textilizable(raw, :object => journal).include?("<img src=\"/attachments/download/#{attachment_1.id}/attached_on_issue.png\" alt=\"\" loading=\"lazy\">")
|
||||
assert textilizable(raw, :object => journal).include?("<img src=\"/attachments/download/#{attachment_2.id}/attached_on_journal.png\" alt=\"\" loading=\"lazy\">")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -245,7 +245,7 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
with_settings :text_formatting => 'textile' do
|
||||
to_test.each do |filename, result|
|
||||
attachment = Attachment.generate!(:filename => filename)
|
||||
assert_include %(<img src="/attachments/download/#{attachment.id}/#{result}" alt="" loading="lazy" />),
|
||||
assert_include %(<img src="/attachments/download/#{attachment.id}/#{result}" alt="" loading="lazy">),
|
||||
textilizable("!#{filename}!", :attachments => [attachment])
|
||||
end
|
||||
end
|
||||
@@ -272,7 +272,7 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
with_settings :text_formatting => 'textile' do
|
||||
assert_equal(
|
||||
%(<p><img src="/attachments/download/#{attachment.id}/image@2x.png" ) +
|
||||
%(srcset="/attachments/download/#{attachment.id}/image@2x.png 2x" alt="" loading="lazy" /></p>),
|
||||
%(srcset="/attachments/download/#{attachment.id}/image@2x.png 2x" alt="" loading="lazy"></p>),
|
||||
textilizable("!image@2x.png!", :attachments => [attachment])
|
||||
)
|
||||
end
|
||||
@@ -325,13 +325,13 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
|
||||
to_test = {
|
||||
'Inline image: !testtest.jpg!' =>
|
||||
'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" loading="lazy" />',
|
||||
'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" loading="lazy">',
|
||||
'Inline image: !testtest.jpeg!' =>
|
||||
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" loading="lazy" />',
|
||||
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" loading="lazy">',
|
||||
'Inline image: !testtest.jpe!' =>
|
||||
'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" loading="lazy" />',
|
||||
'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" loading="lazy">',
|
||||
'Inline image: !testtest.bmp!' =>
|
||||
'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" loading="lazy" />',
|
||||
'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" loading="lazy">',
|
||||
}
|
||||
|
||||
attachments = [a1, a2, a3, a4]
|
||||
@@ -356,9 +356,9 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
|
||||
to_test = {
|
||||
'Inline image: !testfile.png!' =>
|
||||
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" loading="lazy" />',
|
||||
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" loading="lazy">',
|
||||
'Inline image: !Testfile.PNG!' =>
|
||||
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" loading="lazy" />',
|
||||
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" loading="lazy">',
|
||||
}
|
||||
attachments = [a1, a2]
|
||||
with_settings :text_formatting => 'textile' do
|
||||
@@ -378,7 +378,7 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
"This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
|
||||
# no multiline link text
|
||||
"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 />and another on a second line\":test",
|
||||
"This is a double quote \"on the first line<br>and another on a second line\":test",
|
||||
# mailto link
|
||||
"\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" =>
|
||||
"<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
|
||||
@@ -391,7 +391,7 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
'(see "inline link":http://www.foo.bar/Test-)' =>
|
||||
'(see <a href="http://www.foo.bar/Test-" class="external">inline link</a>)',
|
||||
'http://foo.bar/page?p=1&t=z&s=-' =>
|
||||
'<a class="external" href="http://foo.bar/page?p=1&t=z&s=-">http://foo.bar/page?p=1&t=z&s=-</a>',
|
||||
'<a class="external" href="http://foo.bar/page?p=1&t=z&s=-">http://foo.bar/page?p=1&t=z&s=-</a>',
|
||||
'This is an intern "link":/foo/bar-' => 'This is an intern <a href="/foo/bar-">link</a>'
|
||||
}
|
||||
with_settings :text_formatting => 'textile' do
|
||||
@@ -1317,10 +1317,10 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
def test_html_tags
|
||||
to_test = {
|
||||
"<div>content</div>" => "<p><div>content</div></p>",
|
||||
"<div class=\"bold\">content</div>" => "<p><div class="bold">content</div></p>",
|
||||
"<div class=\"bold\">content</div>" => "<p><div class=\"bold\">content</div></p>",
|
||||
"<script>some script;</script>" => "<p><script>some script;</script></p>",
|
||||
# do not escape pre/code tags
|
||||
"<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
|
||||
"<pre>\nline 1\nline2</pre>" => "<pre>line 1\nline2</pre>",
|
||||
"<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
|
||||
"<pre><div class=\"foo\">content</div></pre>" => "<pre><div class=\"foo\">content</div></pre>",
|
||||
"<pre><div class=\"<foo\">content</div></pre>" => "<pre><div class=\"<foo\">content</div></pre>",
|
||||
@@ -1477,7 +1477,7 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
"</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
|
||||
@project = Project.find(1)
|
||||
with_settings :text_formatting => 'textile' do
|
||||
assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
|
||||
assert_equal "<table><tbody>#{result}</tbody></table>", textilizable(text).gsub(/[\t\n]/, '')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1498,7 +1498,7 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
|
||||
def test_wiki_horizontal_rule
|
||||
with_settings :text_formatting => 'textile' do
|
||||
assert_equal '<hr />', textilizable('---')
|
||||
assert_equal '<hr>', textilizable('---')
|
||||
assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006- Jean-Philippe Lang
|
||||
# This code is released under the GNU General Public License.
|
||||
|
||||
require_relative '../../../../test_helper'
|
||||
|
||||
class Redmine::WikiFormatting::TablesortScrubberTest < ActiveSupport::TestCase
|
||||
def filter(html)
|
||||
fragment = Redmine::WikiFormatting::HtmlParser.parse(html)
|
||||
scrubber = Redmine::WikiFormatting::TablesortScrubber.new
|
||||
fragment.scrub!(scrubber)
|
||||
fragment.to_s
|
||||
end
|
||||
|
||||
test 'should not add data-controller attribute by default' do
|
||||
table = <<~HTML
|
||||
<table>
|
||||
<tbody><tr>
|
||||
<th>A</th>
|
||||
<th>B</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
HTML
|
||||
assert_equal table, filter(table)
|
||||
end
|
||||
|
||||
test 'should not add data-controller attribute when the table has less than 3 rows' do
|
||||
table = <<~HTML
|
||||
<table>
|
||||
<tbody><tr>
|
||||
<th>A</th>
|
||||
<th>B</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
HTML
|
||||
with_settings :wiki_tablesort_enabled => 1 do
|
||||
assert_equal table, filter(table)
|
||||
end
|
||||
end
|
||||
|
||||
test 'should add data-controller attribute when the table contains at least 3 rows and enables sorting' do
|
||||
input = <<~HTML
|
||||
<table>
|
||||
<tbody><tr>
|
||||
<th>A</th>
|
||||
<th>B</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
HTML
|
||||
expected = <<~HTML
|
||||
<table data-controller="tablesort">
|
||||
<tbody><tr data-sort-method="none">
|
||||
<th>A</th>
|
||||
<th>B</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
HTML
|
||||
with_settings :wiki_tablesort_enabled => 1 do
|
||||
assert_equal expected, filter(input)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -252,7 +252,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
expected = <<~EXPECTED
|
||||
<p>John said:</p>
|
||||
<blockquote>
|
||||
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br />
|
||||
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br>
|
||||
Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
|
||||
<ul>
|
||||
<li>Donec odio lorem,</li>
|
||||
@@ -282,9 +282,11 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
<p>This is a table with empty cells:</p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>cell11</td><td>cell12</td><td></td></tr>
|
||||
<tr><td>cell21</td><td></td><td>cell23</td></tr>
|
||||
<tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
EXPECTED
|
||||
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
||||
@@ -298,9 +300,11 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
RAW
|
||||
expected = <<~EXPECTED
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td style="text-align:right;">right</td></tr>
|
||||
<tr><td style="text-align:left;">left</td></tr>
|
||||
<tr><td style="text-align:justify;">justify</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
EXPECTED
|
||||
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
||||
@@ -318,9 +322,11 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
<p>This is a table with trailing whitespace in one row:</p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>cell11</td><td>cell12</td></tr>
|
||||
<tr><td>cell21</td><td>cell22</td></tr>
|
||||
<tr><td>cell31</td><td>cell32</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
EXPECTED
|
||||
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
||||
@@ -343,21 +349,23 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
<p>This is a table with line breaks:</p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>cell11<br />continued</td>
|
||||
<td>cell11<br>continued</td>
|
||||
<td>cell12</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><del>cell21</del></td>
|
||||
<td></td>
|
||||
<td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
|
||||
<td>cell23<br>cell23 line2<br>cell23 <strong>line3</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>cell31</td>
|
||||
<td>cell32<br/>cell32 line2</td>
|
||||
<td>cell32<br>cell32 line2</td>
|
||||
<td>cell33</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
EXPECTED
|
||||
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
||||
@@ -380,18 +388,20 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
<p>This is a table with lists:</p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>cell11</td>
|
||||
<td>cell12</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>cell21</td>
|
||||
<td>ordered list<br /># item<br /># item 2</td>
|
||||
<td>ordered list<br># item<br># item 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>cell31</td>
|
||||
<td>unordered list<br />* item<br />* item 2</td>
|
||||
<td>unordered list<br>* item<br>* item 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
EXPECTED
|
||||
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
||||
@@ -408,7 +418,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
expected =
|
||||
'<p><img src="/images/comment.png"onclick=' \
|
||||
'&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;' \
|
||||
'&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;" alt="" /></p>'
|
||||
'&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;" alt=""></p>'
|
||||
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
||||
end
|
||||
|
||||
@@ -635,7 +645,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
"<code class=\"foolang\">unsupported language</code>" =>
|
||||
"<code data-language=\"foolang\">unsupported language</code>",
|
||||
"<code class=\"c-k&r\">special-char language</code>" =>
|
||||
"<code data-language=\"c-k&r\">special-char language</code>",
|
||||
"<code data-language=\"c-k&r\">special-char language</code>",
|
||||
},
|
||||
false
|
||||
)
|
||||
@@ -652,11 +662,11 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
def test_should_prefix_class_attribute_on_tags
|
||||
assert_html_output(
|
||||
{
|
||||
'!(foo)test.png!' => "<p><img src=\"test.png\" class=\"wiki-class-foo\" alt=\"\" /></p>",
|
||||
'!(foo)test.png!' => "<p><img src=\"test.png\" class=\"wiki-class-foo\" alt=\"\"></p>",
|
||||
'%(foo)test%' => "<p><span class=\"wiki-class-foo\">test</span></p>",
|
||||
'p(foo). test' => "<p class=\"wiki-class-foo\">test</p>",
|
||||
'|(foo). test|' =>
|
||||
"<table>\n\t\t<tr>\n\t\t\t<td class=\"wiki-class-foo\">test</td>\n\t\t</tr>\n\t</table>",
|
||||
"<table>\n\t\t<tbody><tr>\n\t\t\t<td class=\"wiki-class-foo\">test</td>\n\t\t</tr>\n\t</tbody></table>",
|
||||
},
|
||||
false
|
||||
)
|
||||
@@ -665,11 +675,11 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
def test_should_prefix_id_attribute_on_tags
|
||||
assert_html_output(
|
||||
{
|
||||
'!(#foo)test.png!' => "<p><img src=\"test.png\" id=\"wiki-id-foo\" alt=\"\" /></p>",
|
||||
'!(#foo)test.png!' => "<p><img src=\"test.png\" id=\"wiki-id-foo\" alt=\"\"></p>",
|
||||
'%(#foo)test%' => "<p><span id=\"wiki-id-foo\">test</span></p>",
|
||||
'p(#foo). test' => "<p id=\"wiki-id-foo\">test</p>",
|
||||
'|(#foo). test|' =>
|
||||
"<table>\n\t\t<tr>\n\t\t\t<td id=\"wiki-id-foo\">test</td>\n\t\t</tr>\n\t</table>",
|
||||
"<table>\n\t\t<tbody><tr>\n\t\t\t<td id=\"wiki-id-foo\">test</td>\n\t\t</tr>\n\t</tbody></table>",
|
||||
},
|
||||
false
|
||||
)
|
||||
@@ -679,7 +689,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
assert_html_output(
|
||||
{
|
||||
'!(wiki-class-foo#wiki-id-bar)test.png!' =>
|
||||
"<p><img src=\"test.png\" class=\"wiki-class-foo\" id=\"wiki-id-bar\" alt=\"\" /></p>",
|
||||
"<p><img src=\"test.png\" class=\"wiki-class-foo\" id=\"wiki-id-bar\" alt=\"\"></p>",
|
||||
},
|
||||
false
|
||||
)
|
||||
@@ -711,8 +721,8 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
|
||||
</pree>
|
||||
STR
|
||||
expected = <<~EXPECTED
|
||||
<p><pree><br />
|
||||
This is some text<br />
|
||||
<p><pree><br>
|
||||
This is some text<br>
|
||||
</pree></p>
|
||||
EXPECTED
|
||||
assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '')
|
||||
|
||||
6
vendor/javascript/tablesort.min.js
vendored
Normal file
6
vendor/javascript/tablesort.min.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* tablesort v5.7.0 (2026-01-03)
|
||||
* http://tristen.ca/tablesort/demo/
|
||||
* Copyright (c) 2026 ; Licensed MIT
|
||||
*/
|
||||
const m=[],v=function(n){if(!window.CustomEvent||typeof window.CustomEvent!="function"){const t=document.createEvent("CustomEvent");return t.initCustomEvent(n,!1,!1,void 0),t}else return new CustomEvent(n)},A=function(n,t){const e=t.sortAttribute||"data-sort";return n.hasAttribute(e)?n.getAttribute(e):n.textContent||n.innerText||""},C=function(n,t){return n=n.trim().toLowerCase(),t=t.trim().toLowerCase(),n===t?0:n<t?1:-1},E=function(n,t){return[].slice.call(n).find(function(e){return e.getAttribute("data-sort-column-key")===t})},x=function(n,t){return function(e,s){const i=n(e.td,s.td);return i===0?t?s.index-e.index:e.index-s.index:i}};class B{static extend(t,e,s){if(typeof e!="function"||typeof s!="function")throw new Error("Pattern and sort must be a function");m.push({name:t,pattern:e,sort:s})}constructor(t,e){if(!t||t.tagName!=="TABLE")throw new Error("Element must be a table");this.table=t,this.thead=!1,this.options=e||{};const s=this.getFirstRow(t);if(!s)return;const i=this.getDefaultSort(s);i&&(this.current=i,this.sortTable(i))}getFirstRow(t){let e;if(t.rows&&t.rows.length>0)if(t.tHead&&t.tHead.rows.length>0){for(let s=0;s<t.tHead.rows.length;s++)if(t.tHead.rows[s].getAttribute("data-sort-method")==="thead"){e=t.tHead.rows[s];break}e||(e=t.tHead.rows[t.tHead.rows.length-1]),this.thead=!0}else e=t.rows[0];return e}getDefaultSort(t){const e=i=>{this.current&&this.current!==i.target&&this.current.removeAttribute("aria-sort"),this.current=i.target,this.sortTable(i.target)};let s;for(let i=0;i<t.cells.length;i++){const l=t.cells[i];l.setAttribute("role","columnheader"),l.getAttribute("data-sort-method")!=="none"&&(l.tabIndex=0,l.addEventListener("click",e,!1),l.addEventListener("keydown",function(r){r.key==="Enter"&&(r.preventDefault(),e(r))}),l.getAttribute("data-sort-default")!==null&&(s=l))}return s}sortTable(t,e){let s=t.getAttribute("data-sort-column-key"),i=t.cellIndex,l=C,r="",f=[],c=this.thead?0:1,b=t.getAttribute("data-sort-method"),y=t.hasAttribute("data-sort-reverse"),u=t.getAttribute("aria-sort");if(this.table.dispatchEvent(v("beforeSort")),e||(u==="ascending"?u="descending":u==="descending"?u="ascending":u=!!this.options.descending!=y?"descending":"ascending",t.setAttribute("aria-sort",u)),!(this.table.rows.length<2)){if(!b){let o;for(;f.length<3&&c<this.table.tBodies[0].rows.length;)s?o=E(this.table.tBodies[0].rows[c].cells,s):o=this.table.tBodies[0].rows[c].cells[i],r=o?A(o,this.options):"",r=r.trim(),r.length>0&&f.push(r),c++;if(!f)return}for(let o=0;o<m.length;o++)if(r=m[o],b){if(r.name===b){l=r.sort;break}}else if(f.every(r.pattern)){l=r.sort;break}this.col=i;for(let o=0;o<this.table.tBodies.length;o++){let d=[],w={},h=0,p=0;if(!(this.table.tBodies[o].rows.length<2)){for(let a=0;a<this.table.tBodies[o].rows.length;a++){let g;r=this.table.tBodies[o].rows[a],r.getAttribute("data-sort-method")==="none"?w[h]=r:(s?g=E(r.cells,s):g=r.cells[this.col],d.push({tr:r,td:g?A(g,this.options):"",index:h})),h++}u==="descending"?d.sort(x(l,!0)):(d.sort(x(l,!1)),d.reverse());for(let a=0;a<h;a++)w[a]?(r=w[a],p++):r=d[a-p].tr,this.table.tBodies[o].appendChild(r)}}this.table.dispatchEvent(v("afterSort"))}}refresh(){this.current!==void 0&&this.sortTable(this.current,!0)}}export{B as default};
|
||||
6
vendor/javascript/tablesort.number.min.js
vendored
Normal file
6
vendor/javascript/tablesort.number.min.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* tablesort v5.7.0 (2026-01-03)
|
||||
* http://tristen.ca/tablesort/demo/
|
||||
* Copyright (c) 2026 ; Licensed MIT
|
||||
*/
|
||||
const r=function(n){return n.replace(/[^\-?0-9.]/g,"")},o=function(n,t){return n=parseFloat(n),t=parseFloat(t),n=isNaN(n)?0:n,t=isNaN(t)?0:t,n-t},e={name:"number",pattern:function(n){return n.match(/^[-+]?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/)||n.match(/^[-+]?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/)||n.match(/^[-+]?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/)},sort:function(n,t){return n=r(n),t=r(t),o(t,n)}};typeof window.Tablesort<"u"&&Tablesort.extend(e.name,e.pattern,e.sort);var u=e;export{u as default};
|
||||
Reference in New Issue
Block a user