Patch #43643 » 0001-Add-tablesort-controller-Support-loofah-in-textile-m.patch
| app/assets/javascripts/application-legacy.js | ||
|---|---|---|
| 1170 | 1170 |
data: "text=" + element + '&' + attachments, |
| 1171 | 1171 |
success: function(data){
|
| 1172 | 1172 |
jstBlock.find('.wiki-preview').html(data);
|
| 1173 |
setupWikiTableSortableHeader(); |
|
| 1174 | 1173 |
} |
| 1175 | 1174 |
}); |
| 1176 | 1175 |
}); |
| ... | ... | |
| 1439 | 1438 |
$(document).ready(setupAttachmentDetail); |
| 1440 | 1439 |
$(document).ready(setupTabs); |
| 1441 | 1440 |
$(document).ready(setupFilePreviewNavigation); |
| 1442 |
$(document).ready(setupWikiTableSortableHeader); |
|
| 1443 | 1441 |
$(document).on('focus', '[data-auto-complete=true]', function(event) {
|
| 1444 | 1442 |
inlineAutoComplete(event.target); |
| 1445 | 1443 |
}); |
| app/helpers/application_helper.rb | ||
|---|---|---|
| 1806 | 1806 |
'rails-ujs', |
| 1807 | 1807 |
'tribute-5.1.3.min' |
| 1808 | 1808 |
) |
| 1809 |
if Setting.wiki_tablesort_enabled? |
|
| 1810 |
tags << javascript_include_tag('tablesort-5.2.1.min.js', 'tablesort-5.2.1.number.min.js')
|
|
| 1811 |
end |
|
| 1812 | 1809 |
tags << javascript_include_tag('application-legacy', 'responsive')
|
| 1813 | 1810 |
unless User.current.pref.warn_on_leaving_unsaved == '0' |
| 1814 | 1811 |
warn_text = escape_javascript(l(:text_warn_on_leaving_unsaved)) |
| app/javascript/controllers/tablesort_controller.js | ||
|---|---|---|
| 1 |
/** |
|
| 2 |
* Redmine - project management software |
|
| 3 |
* Copyright (C) 2006- Jean-Philippe Lang |
|
| 4 |
* This code is released under the GNU General Public License. |
|
| 5 |
*/ |
|
| 6 |
import { Controller } from "@hotwired/stimulus"
|
|
| 7 |
import Tablesort from 'tablesort'; |
|
| 8 |
import numberPlugin from 'tablesort.number'; |
|
| 9 | ||
| 10 |
// Extensions must be loaded explicitly |
|
| 11 |
Tablesort.extend(numberPlugin.name, numberPlugin.pattern, numberPlugin.sort); |
|
| 12 | ||
| 13 |
// Connects to data-controller="tablesort" |
|
| 14 |
export default class extends Controller {
|
|
| 15 |
connect() {
|
|
| 16 |
new Tablesort(this.element); |
|
| 17 |
} |
|
| 18 |
} |
|
| app/views/journals/update.js.erb | ||
|---|---|---|
| 14 | 14 |
} else {
|
| 15 | 15 |
journal_header.append('<%= escape_javascript(render_journal_update_info(@journal)) %>');
|
| 16 | 16 |
} |
| 17 |
setupWikiTableSortableHeader(); |
|
| 18 | 17 |
setupCopyButtonsToPreElements(); |
| 19 | 18 |
setupHoverTooltips(); |
| 20 | 19 |
<% end %> |
| config/importmap.rb | ||
|---|---|---|
| 7 | 7 |
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" |
| 8 | 8 |
pin "turndown" # @7.2.0 |
| 9 | 9 |
pin_all_from "app/javascript/controllers", under: "controllers" |
| 10 |
pin "tablesort", to: "tablesort.min.js" |
|
| 11 |
pin "tablesort.number", to: "tablesort.number.min.js" |
|
| lib/redmine/wiki_formatting/common_mark/formatter.rb | ||
|---|---|---|
| 55 | 55 |
SANITIZER = SanitizationFilter.new |
| 56 | 56 |
SCRUBBERS = [ |
| 57 | 57 |
SyntaxHighlightScrubber.new, |
| 58 |
Redmine::WikiFormatting::TablesortScrubber.new, |
|
| 58 | 59 |
FixupAutoLinksScrubber.new, |
| 59 | 60 |
ExternalLinksScrubber.new, |
| 60 | 61 |
AlertsIconsScrubber.new |
| lib/redmine/wiki_formatting/tablesort_scrubber.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# This code is released under the GNU General Public License. |
|
| 6 | ||
| 7 |
module Redmine |
|
| 8 |
module WikiFormatting |
|
| 9 |
class TablesortScrubber < Loofah::Scrubber |
|
| 10 |
def scrub(node) |
|
| 11 |
return if !Setting.wiki_tablesort_enabled? || node.name != 'table' |
|
| 12 | ||
| 13 |
rows = node.search('tr')
|
|
| 14 |
return if rows.size < 3 |
|
| 15 | ||
| 16 |
tr = rows.first |
|
| 17 |
if tr.search('th').present?
|
|
| 18 |
node['data-controller'] = 'tablesort' |
|
| 19 |
tr['data-sort-method'] = 'none' |
|
| 20 |
tr.search('td').each do |td|
|
|
| 21 |
td['data-sort-method'] = 'none' |
|
| 22 |
end |
|
| 23 |
end |
|
| 24 |
end |
|
| 25 |
end |
|
| 26 |
end |
|
| 27 |
end |
|
| lib/redmine/wiki_formatting/textile/formatter.rb | ||
|---|---|---|
| 20 | 20 |
module Redmine |
| 21 | 21 |
module WikiFormatting |
| 22 | 22 |
module Textile |
| 23 |
class Formatter < RedCloth3 |
|
| 24 |
include ActionView::Helpers::TagHelper |
|
| 25 |
include Redmine::WikiFormatting::LinksHelper |
|
| 23 |
SCRUBBERS = [ |
|
| 24 |
Redmine::WikiFormatting::TablesortScrubber.new |
|
| 25 |
] |
|
| 26 | ||
| 27 |
class Formatter |
|
| 26 | 28 |
include Redmine::WikiFormatting::SectionHelper |
| 27 | 29 | |
| 30 |
extend Forwardable |
|
| 31 |
def_delegators :@filter, :extract_sections, :rip_offtags |
|
| 32 | ||
| 33 |
def initialize(args) |
|
| 34 |
@filter = Filter.new(args) |
|
| 35 |
end |
|
| 36 | ||
| 37 |
def to_html(*rules) |
|
| 38 |
html = @filter.to_html(rules) |
|
| 39 |
fragment = Loofah.html5_fragment(html) |
|
| 40 |
SCRUBBERS.each do |scrubber| |
|
| 41 |
fragment.scrub!(scrubber) |
|
| 42 |
end |
|
| 43 |
fragment.to_s |
|
| 44 |
end |
|
| 45 |
end |
|
| 46 | ||
| 47 |
class Filter < RedCloth3 |
|
| 48 |
include Redmine::WikiFormatting::LinksHelper |
|
| 49 | ||
| 28 | 50 |
alias :inline_auto_link :auto_link! |
| 29 | 51 |
alias :inline_auto_mailto :auto_mailto! |
| 30 | 52 |
alias :inline_restore_redmine_links :restore_redmine_links |
| ... | ... | |
| 41 | 63 | |
| 42 | 64 |
def to_html(*rules) |
| 43 | 65 |
@toc = [] |
| 44 |
super(*RULES).to_s
|
|
| 66 |
super(*RULES) |
|
| 45 | 67 |
end |
| 46 | 68 | |
| 47 | 69 |
def extract_sections(index) |
| test/helpers/application_helper_test.rb | ||
|---|---|---|
| 80 | 80 |
'(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).', |
| 81 | 81 |
'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>', |
| 82 | 82 |
'http://foo.bar/page?p=1&t=z&s=' => |
| 83 |
'<a class="external" href="http://foo.bar/page?p=1&t=z&s=">http://foo.bar/page?p=1&t=z&s=</a>',
|
|
| 83 |
'<a class="external" href="http://foo.bar/page?p=1&t=z&s=">http://foo.bar/page?p=1&t=z&s=</a>',
|
|
| 84 | 84 |
'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>', |
| 85 | 85 |
'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>', |
| 86 | 86 |
'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>', |
| ... | ... | |
| 92 | 92 |
'<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">' \ |
| 93 | 93 |
'http://example.net/path!602815048C7B5C20!302.html</a>', |
| 94 | 94 |
# escaping |
| 95 |
'http://foo"bar' => '<a class="external" href="http://foo"bar">http://foo"bar</a>',
|
|
| 95 |
'http://foo"bar' => '<a class="external" href="http://foo"bar">http://foo"bar</a>',
|
|
| 96 | 96 |
# wrap in angle brackets |
| 97 | 97 |
'<http://foo.bar>' => '<<a class="external" href="http://foo.bar">http://foo.bar</a>>', |
| 98 | 98 |
# invalid urls |
| ... | ... | |
| 130 | 130 | |
| 131 | 131 |
def test_inline_images |
| 132 | 132 |
to_test = {
|
| 133 |
'!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
|
|
| 133 |
'!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="">', |
|
| 134 | 134 |
'floating !>http://foo.bar/image.jpg!' => |
| 135 |
'floating <span style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></span>',
|
|
| 135 |
'floating <span style="float:right"><img src="http://foo.bar/image.jpg" alt=""></span>', |
|
| 136 | 136 |
'with class !(some-class)http://foo.bar/image.jpg!' => |
| 137 |
'with class <img src="http://foo.bar/image.jpg" class="wiki-class-some-class" alt="" />',
|
|
| 137 |
'with class <img src="http://foo.bar/image.jpg" class="wiki-class-some-class" alt="">', |
|
| 138 | 138 |
'with class !(wiki-class-foo)http://foo.bar/image.jpg!' => |
| 139 |
'with class <img src="http://foo.bar/image.jpg" class="wiki-class-foo" alt="" />',
|
|
| 139 |
'with class <img src="http://foo.bar/image.jpg" class="wiki-class-foo" alt="">', |
|
| 140 | 140 |
'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' =>
|
| 141 |
'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
|
|
| 141 |
'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="">', |
|
| 142 | 142 |
'with title !http://foo.bar/image.jpg(This is a title)!' => |
| 143 |
'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
|
|
| 143 |
'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title">', |
|
| 144 | 144 |
'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => |
| 145 | 145 |
'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted "title"" ' \ |
| 146 |
'alt="This is a double-quoted "title"" />',
|
|
| 146 |
'alt="This is a double-quoted "title"">', |
|
| 147 | 147 |
'with query string !http://foo.bar/image.cgi?a=1&b=2!' => |
| 148 |
'with query string <img src="http://foo.bar/image.cgi?a=1&b=2" alt="" />'
|
|
| 148 |
'with query string <img src="http://foo.bar/image.cgi?a=1&b=2" alt="">'
|
|
| 149 | 149 |
} |
| 150 | 150 |
with_settings :text_formatting => 'textile' do |
| 151 | 151 |
to_test.each {|text, result| assert_equal "<p>#{result}</p>", textilizable(text)}
|
| ... | ... | |
| 161 | 161 |
p=. !bar.gif! |
| 162 | 162 |
RAW |
| 163 | 163 |
with_settings :text_formatting => 'textile' do |
| 164 |
assert textilizable(raw).include?('<img src="foo.png" alt="" />')
|
|
| 165 |
assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
|
|
| 164 |
assert textilizable(raw).include?('<img src="foo.png" alt="">')
|
|
| 165 |
assert textilizable(raw).include?('<img src="bar.gif" alt="">')
|
|
| 166 | 166 |
end |
| 167 | 167 |
end |
| 168 | 168 | |
| 169 | 169 |
def test_attached_images |
| 170 | 170 |
to_test = {
|
| 171 | 171 |
'Inline image: !logo.gif!' => |
| 172 |
'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy" />',
|
|
| 172 |
'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy">', |
|
| 173 | 173 |
'Inline image: !logo.GIF!' => |
| 174 |
'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy" />',
|
|
| 174 |
'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy">', |
|
| 175 | 175 |
'Inline WebP image: !logo.webp!' => |
| 176 |
'Inline WebP image: <img src="/attachments/download/24/logo.webp" title="WebP image" alt="WebP image" loading="lazy" />',
|
|
| 177 |
'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
|
|
| 178 |
'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
|
|
| 176 |
'Inline WebP image: <img src="/attachments/download/24/logo.webp" title="WebP image" alt="WebP image" loading="lazy">', |
|
| 177 |
'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="">', |
|
| 178 |
'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="">', |
|
| 179 | 179 |
# link image |
| 180 | 180 |
'!logo.gif!:http://foo.bar/' => |
| 181 |
'<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>',
|
|
| 181 |
'<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>', |
|
| 182 | 182 |
} |
| 183 | 183 |
attachments = Attachment.all |
| 184 | 184 |
with_settings :text_formatting => 'textile' do |
| ... | ... | |
| 190 | 190 |
attachments = Attachment.all |
| 191 | 191 |
with_settings text_formatting: 'textile' do |
| 192 | 192 |
# When alt text is set |
| 193 |
assert_match %r[<img src=".+?" title="alt text" alt="alt text" loading=".+?" />],
|
|
| 193 |
assert_match %r[<img src=".+?" title="alt text" alt="alt text" loading=".+?">], |
|
| 194 | 194 |
textilizable('!logo.gif(alt text)!', attachments: attachments)
|
| 195 | 195 | |
| 196 | 196 |
# When alt text and style are set |
| 197 |
assert_match %r[<img src=".+?" title="alt text" alt="alt text" loading=".+?" style="width:100px;" />],
|
|
| 197 |
assert_match %r[<img src=".+?" title="alt text" alt="alt text" loading=".+?" style="width:100px;">], |
|
| 198 | 198 |
textilizable('!{width:100px}logo.gif(alt text)!', attachments: attachments)
|
| 199 | 199 | |
| 200 | 200 |
# When alt text is not set |
| 201 |
assert_match %r[<img src=".+?" title="This is a logo" alt="This is a logo" loading=".+?" />],
|
|
| 201 |
assert_match %r[<img src=".+?" title="This is a logo" alt="This is a logo" loading=".+?">], |
|
| 202 | 202 |
textilizable('!logo.gif!', attachments: attachments)
|
| 203 | 203 | |
| 204 | 204 |
# When alt text is not set and the attachment has no description |
| 205 |
assert_match %r[<img src=".+?" alt="" loading=".+?" />],
|
|
| 205 |
assert_match %r[<img src=".+?" alt="" loading=".+?">], |
|
| 206 | 206 |
textilizable('!testfile.PNG!', attachments: attachments)
|
| 207 | 207 | |
| 208 | 208 |
# When no matching attachments are found |
| 209 |
assert_match %r[<img src=".+?" alt="" />],
|
|
| 209 |
assert_match %r[<img src=".+?" alt="">], |
|
| 210 | 210 |
textilizable('!no-match.jpg!', attachments: attachments)
|
| 211 |
assert_match %r[<img src=".+?" alt="alt text" />],
|
|
| 211 |
assert_match %r[<img src=".+?" alt="alt text">], |
|
| 212 | 212 |
textilizable('!no-match.jpg(alt text)!', attachments: attachments)
|
| 213 | 213 | |
| 214 | 214 |
# When no attachment is registered |
| 215 |
assert_match %r[<img src=".+?" alt="" />],
|
|
| 215 |
assert_match %r[<img src=".+?" alt="">], |
|
| 216 | 216 |
textilizable('!logo.gif!', attachments: [])
|
| 217 |
assert_match %r[<img src=".+?" alt="alt text" />],
|
|
| 217 |
assert_match %r[<img src=".+?" alt="alt text">], |
|
| 218 | 218 |
textilizable('!logo.gif(alt text)!', attachments: [])
|
| 219 | 219 |
end |
| 220 | 220 |
end |
| ... | ... | |
| 232 | 232 |
RAW |
| 233 | 233 | |
| 234 | 234 |
with_settings :text_formatting => 'textile' do |
| 235 |
assert textilizable(raw, :object => journal).include?("<img src=\"/attachments/download/#{attachment_1.id}/attached_on_issue.png\" alt=\"\" loading=\"lazy\" />")
|
|
| 236 |
assert textilizable(raw, :object => journal).include?("<img src=\"/attachments/download/#{attachment_2.id}/attached_on_journal.png\" alt=\"\" loading=\"lazy\" />")
|
|
| 235 |
assert textilizable(raw, :object => journal).include?("<img src=\"/attachments/download/#{attachment_1.id}/attached_on_issue.png\" alt=\"\" loading=\"lazy\">")
|
|
| 236 |
assert textilizable(raw, :object => journal).include?("<img src=\"/attachments/download/#{attachment_2.id}/attached_on_journal.png\" alt=\"\" loading=\"lazy\">")
|
|
| 237 | 237 |
end |
| 238 | 238 |
end |
| 239 | 239 | |
| ... | ... | |
| 245 | 245 |
with_settings :text_formatting => 'textile' do |
| 246 | 246 |
to_test.each do |filename, result| |
| 247 | 247 |
attachment = Attachment.generate!(:filename => filename) |
| 248 |
assert_include %(<img src="/attachments/download/#{attachment.id}/#{result}" alt="" loading="lazy" />),
|
|
| 248 |
assert_include %(<img src="/attachments/download/#{attachment.id}/#{result}" alt="" loading="lazy">),
|
|
| 249 | 249 |
textilizable("!#{filename}!", :attachments => [attachment])
|
| 250 | 250 |
end |
| 251 | 251 |
end |
| ... | ... | |
| 272 | 272 |
with_settings :text_formatting => 'textile' do |
| 273 | 273 |
assert_equal( |
| 274 | 274 |
%(<p><img src="/attachments/download/#{attachment.id}/image@2x.png" ) +
|
| 275 |
%(srcset="/attachments/download/#{attachment.id}/image@2x.png 2x" alt="" loading="lazy" /></p>),
|
|
| 275 |
%(srcset="/attachments/download/#{attachment.id}/image@2x.png 2x" alt="" loading="lazy"></p>),
|
|
| 276 | 276 |
textilizable("!image@2x.png!", :attachments => [attachment])
|
| 277 | 277 |
) |
| 278 | 278 |
end |
| ... | ... | |
| 325 | 325 | |
| 326 | 326 |
to_test = {
|
| 327 | 327 |
'Inline image: !testtest.jpg!' => |
| 328 |
'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" loading="lazy" />',
|
|
| 328 |
'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" loading="lazy">', |
|
| 329 | 329 |
'Inline image: !testtest.jpeg!' => |
| 330 |
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" loading="lazy" />',
|
|
| 330 |
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" loading="lazy">', |
|
| 331 | 331 |
'Inline image: !testtest.jpe!' => |
| 332 |
'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" loading="lazy" />',
|
|
| 332 |
'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" loading="lazy">', |
|
| 333 | 333 |
'Inline image: !testtest.bmp!' => |
| 334 |
'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" loading="lazy" />',
|
|
| 334 |
'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" loading="lazy">', |
|
| 335 | 335 |
} |
| 336 | 336 | |
| 337 | 337 |
attachments = [a1, a2, a3, a4] |
| ... | ... | |
| 356 | 356 | |
| 357 | 357 |
to_test = {
|
| 358 | 358 |
'Inline image: !testfile.png!' => |
| 359 |
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" loading="lazy" />',
|
|
| 359 |
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" loading="lazy">', |
|
| 360 | 360 |
'Inline image: !Testfile.PNG!' => |
| 361 |
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" loading="lazy" />',
|
|
| 361 |
'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" loading="lazy">', |
|
| 362 | 362 |
} |
| 363 | 363 |
attachments = [a1, a2] |
| 364 | 364 |
with_settings :text_formatting => 'textile' do |
| ... | ... | |
| 378 | 378 |
"This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph", |
| 379 | 379 |
# no multiline link text |
| 380 | 380 |
"This is a double quote \"on the first line\nand another on a second line\":test" => |
| 381 |
"This is a double quote \"on the first line<br />and another on a second line\":test",
|
|
| 381 |
"This is a double quote \"on the first line<br>and another on a second line\":test", |
|
| 382 | 382 |
# mailto link |
| 383 | 383 |
"\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => |
| 384 | 384 |
"<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>", |
| ... | ... | |
| 391 | 391 |
'(see "inline link":http://www.foo.bar/Test-)' => |
| 392 | 392 |
'(see <a href="http://www.foo.bar/Test-" class="external">inline link</a>)', |
| 393 | 393 |
'http://foo.bar/page?p=1&t=z&s=-' => |
| 394 |
'<a class="external" href="http://foo.bar/page?p=1&t=z&s=-">http://foo.bar/page?p=1&t=z&s=-</a>',
|
|
| 394 |
'<a class="external" href="http://foo.bar/page?p=1&t=z&s=-">http://foo.bar/page?p=1&t=z&s=-</a>',
|
|
| 395 | 395 |
'This is an intern "link":/foo/bar-' => 'This is an intern <a href="/foo/bar-">link</a>' |
| 396 | 396 |
} |
| 397 | 397 |
with_settings :text_formatting => 'textile' do |
| ... | ... | |
| 1317 | 1317 |
def test_html_tags |
| 1318 | 1318 |
to_test = {
|
| 1319 | 1319 |
"<div>content</div>" => "<p><div>content</div></p>", |
| 1320 |
"<div class=\"bold\">content</div>" => "<p><div class="bold">content</div></p>",
|
|
| 1320 |
"<div class=\"bold\">content</div>" => "<p><div class=\"bold\">content</div></p>",
|
|
| 1321 | 1321 |
"<script>some script;</script>" => "<p><script>some script;</script></p>", |
| 1322 | 1322 |
# do not escape pre/code tags |
| 1323 |
"<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
|
|
| 1323 |
"<pre>\nline 1\nline2</pre>" => "<pre>line 1\nline2</pre>", |
|
| 1324 | 1324 |
"<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>", |
| 1325 | 1325 |
"<pre><div class=\"foo\">content</div></pre>" => "<pre><div class=\"foo\">content</div></pre>", |
| 1326 | 1326 |
"<pre><div class=\"<foo\">content</div></pre>" => "<pre><div class=\"<foo\">content</div></pre>", |
| ... | ... | |
| 1477 | 1477 |
"</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
|
| 1478 | 1478 |
@project = Project.find(1) |
| 1479 | 1479 |
with_settings :text_formatting => 'textile' do |
| 1480 |
assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
|
|
| 1480 |
assert_equal "<table><tbody>#{result}</tbody></table>", textilizable(text).gsub(/[\t\n]/, '')
|
|
| 1481 | 1481 |
end |
| 1482 | 1482 |
end |
| 1483 | 1483 | |
| ... | ... | |
| 1498 | 1498 | |
| 1499 | 1499 |
def test_wiki_horizontal_rule |
| 1500 | 1500 |
with_settings :text_formatting => 'textile' do |
| 1501 |
assert_equal '<hr />', textilizable('---')
|
|
| 1501 |
assert_equal '<hr>', textilizable('---')
|
|
| 1502 | 1502 |
assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
|
| 1503 | 1503 |
end |
| 1504 | 1504 |
end |
| test/unit/lib/redmine/wiki_formatting/tablesort_scrubber_test.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# This code is released under the GNU General Public License. |
|
| 6 | ||
| 7 |
require_relative '../../../../test_helper' |
|
| 8 | ||
| 9 |
class Redmine::WikiFormatting::TablesortScrubberTest < ActiveSupport::TestCase |
|
| 10 |
def filter(html) |
|
| 11 |
fragment = Redmine::WikiFormatting::HtmlParser.parse(html) |
|
| 12 |
scrubber = Redmine::WikiFormatting::TablesortScrubber.new |
|
| 13 |
fragment.scrub!(scrubber) |
|
| 14 |
fragment.to_s |
|
| 15 |
end |
|
| 16 | ||
| 17 |
test 'should not add data-controller attribute by default' do |
|
| 18 |
table = <<~HTML |
|
| 19 |
<table> |
|
| 20 |
<tbody><tr> |
|
| 21 |
<th>A</th> |
|
| 22 |
<th>B</th> |
|
| 23 |
</tr> |
|
| 24 |
<tr> |
|
| 25 |
<td></td> |
|
| 26 |
<td></td> |
|
| 27 |
</tr> |
|
| 28 |
<tr> |
|
| 29 |
<td></td> |
|
| 30 |
<td></td> |
|
| 31 |
</tr> |
|
| 32 |
</tbody></table> |
|
| 33 |
HTML |
|
| 34 |
assert_equal table, filter(table) |
|
| 35 |
end |
|
| 36 | ||
| 37 |
test 'should not add data-controller attribute when the table has less than 3 rows' do |
|
| 38 |
table = <<~HTML |
|
| 39 |
<table> |
|
| 40 |
<tbody><tr> |
|
| 41 |
<th>A</th> |
|
| 42 |
<th>B</th> |
|
| 43 |
</tr> |
|
| 44 |
<tr> |
|
| 45 |
<td></td> |
|
| 46 |
<td></td> |
|
| 47 |
</tr> |
|
| 48 |
</tbody></table> |
|
| 49 |
HTML |
|
| 50 |
with_settings :wiki_tablesort_enabled => 1 do |
|
| 51 |
assert_equal table, filter(table) |
|
| 52 |
end |
|
| 53 |
end |
|
| 54 | ||
| 55 |
test 'should add data-controller attribute when the table contains at least 3 rows and enables sorting' do |
|
| 56 |
input = <<~HTML |
|
| 57 |
<table> |
|
| 58 |
<tbody><tr> |
|
| 59 |
<th>A</th> |
|
| 60 |
<th>B</th> |
|
| 61 |
</tr> |
|
| 62 |
<tr> |
|
| 63 |
<td></td> |
|
| 64 |
<td></td> |
|
| 65 |
</tr> |
|
| 66 |
<tr> |
|
| 67 |
<td></td> |
|
| 68 |
<td></td> |
|
| 69 |
</tr> |
|
| 70 |
</tbody></table> |
|
| 71 |
HTML |
|
| 72 |
expected = <<~HTML |
|
| 73 |
<table data-controller="tablesort"> |
|
| 74 |
<tbody><tr data-sort-method="none"> |
|
| 75 |
<th>A</th> |
|
| 76 |
<th>B</th> |
|
| 77 |
</tr> |
|
| 78 |
<tr> |
|
| 79 |
<td></td> |
|
| 80 |
<td></td> |
|
| 81 |
</tr> |
|
| 82 |
<tr> |
|
| 83 |
<td></td> |
|
| 84 |
<td></td> |
|
| 85 |
</tr> |
|
| 86 |
</tbody></table> |
|
| 87 |
HTML |
|
| 88 |
with_settings :wiki_tablesort_enabled => 1 do |
|
| 89 |
assert_equal expected, filter(input) |
|
| 90 |
end |
|
| 91 |
end |
|
| 92 |
end |
|
| test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb | ||
|---|---|---|
| 252 | 252 |
expected = <<~EXPECTED |
| 253 | 253 |
<p>John said:</p> |
| 254 | 254 |
<blockquote> |
| 255 |
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br />
|
|
| 255 |
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br> |
|
| 256 | 256 |
Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor. |
| 257 | 257 |
<ul> |
| 258 | 258 |
<li>Donec odio lorem,</li> |
| ... | ... | |
| 282 | 282 |
<p>This is a table with empty cells:</p> |
| 283 | 283 | |
| 284 | 284 |
<table> |
| 285 |
<tbody> |
|
| 285 | 286 |
<tr><td>cell11</td><td>cell12</td><td></td></tr> |
| 286 | 287 |
<tr><td>cell21</td><td></td><td>cell23</td></tr> |
| 287 | 288 |
<tr><td>cell31</td><td>cell32</td><td>cell33</td></tr> |
| 289 |
</tbody> |
|
| 288 | 290 |
</table> |
| 289 | 291 |
EXPECTED |
| 290 | 292 |
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
| ... | ... | |
| 298 | 300 |
RAW |
| 299 | 301 |
expected = <<~EXPECTED |
| 300 | 302 |
<table> |
| 303 |
<tbody> |
|
| 301 | 304 |
<tr><td style="text-align:right;">right</td></tr> |
| 302 | 305 |
<tr><td style="text-align:left;">left</td></tr> |
| 303 | 306 |
<tr><td style="text-align:justify;">justify</td></tr> |
| 307 |
</tbody> |
|
| 304 | 308 |
</table> |
| 305 | 309 |
EXPECTED |
| 306 | 310 |
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
| ... | ... | |
| 318 | 322 |
<p>This is a table with trailing whitespace in one row:</p> |
| 319 | 323 | |
| 320 | 324 |
<table> |
| 325 |
<tbody> |
|
| 321 | 326 |
<tr><td>cell11</td><td>cell12</td></tr> |
| 322 | 327 |
<tr><td>cell21</td><td>cell22</td></tr> |
| 323 | 328 |
<tr><td>cell31</td><td>cell32</td></tr> |
| 329 |
</tbody> |
|
| 324 | 330 |
</table> |
| 325 | 331 |
EXPECTED |
| 326 | 332 |
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
| ... | ... | |
| 343 | 349 |
<p>This is a table with line breaks:</p> |
| 344 | 350 | |
| 345 | 351 |
<table> |
| 352 |
<tbody> |
|
| 346 | 353 |
<tr> |
| 347 |
<td>cell11<br />continued</td>
|
|
| 354 |
<td>cell11<br>continued</td> |
|
| 348 | 355 |
<td>cell12</td> |
| 349 | 356 |
<td></td> |
| 350 | 357 |
</tr> |
| 351 | 358 |
<tr> |
| 352 | 359 |
<td><del>cell21</del></td> |
| 353 | 360 |
<td></td> |
| 354 |
<td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
|
|
| 361 |
<td>cell23<br>cell23 line2<br>cell23 <strong>line3</strong></td>
|
|
| 355 | 362 |
</tr> |
| 356 | 363 |
<tr> |
| 357 | 364 |
<td>cell31</td> |
| 358 |
<td>cell32<br/>cell32 line2</td>
|
|
| 365 |
<td>cell32<br>cell32 line2</td> |
|
| 359 | 366 |
<td>cell33</td> |
| 360 | 367 |
</tr> |
| 368 |
</tbody> |
|
| 361 | 369 |
</table> |
| 362 | 370 |
EXPECTED |
| 363 | 371 |
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
| ... | ... | |
| 380 | 388 |
<p>This is a table with lists:</p> |
| 381 | 389 | |
| 382 | 390 |
<table> |
| 391 |
<tbody> |
|
| 383 | 392 |
<tr> |
| 384 | 393 |
<td>cell11</td> |
| 385 | 394 |
<td>cell12</td> |
| 386 | 395 |
</tr> |
| 387 | 396 |
<tr> |
| 388 | 397 |
<td>cell21</td> |
| 389 |
<td>ordered list<br /># item<br /># item 2</td>
|
|
| 398 |
<td>ordered list<br># item<br># item 2</td>
|
|
| 390 | 399 |
</tr> |
| 391 | 400 |
<tr> |
| 392 | 401 |
<td>cell31</td> |
| 393 |
<td>unordered list<br />* item<br />* item 2</td>
|
|
| 402 |
<td>unordered list<br>* item<br>* item 2</td>
|
|
| 394 | 403 |
</tr> |
| 404 |
</tbody> |
|
| 395 | 405 |
</table> |
| 396 | 406 |
EXPECTED |
| 397 | 407 |
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
| ... | ... | |
| 408 | 418 |
expected = |
| 409 | 419 |
'<p><img src="/images/comment.png"onclick=' \ |
| 410 | 420 |
'&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;' \ |
| 411 |
'&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;" alt="" /></p>'
|
|
| 421 |
'&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;" alt=""></p>' |
|
| 412 | 422 |
assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
|
| 413 | 423 |
end |
| 414 | 424 | |
| ... | ... | |
| 635 | 645 |
"<code class=\"foolang\">unsupported language</code>" => |
| 636 | 646 |
"<code data-language=\"foolang\">unsupported language</code>", |
| 637 | 647 |
"<code class=\"c-k&r\">special-char language</code>" => |
| 638 |
"<code data-language=\"c-k&r\">special-char language</code>",
|
|
| 648 |
"<code data-language=\"c-k&r\">special-char language</code>",
|
|
| 639 | 649 |
}, |
| 640 | 650 |
false |
| 641 | 651 |
) |
| ... | ... | |
| 652 | 662 |
def test_should_prefix_class_attribute_on_tags |
| 653 | 663 |
assert_html_output( |
| 654 | 664 |
{
|
| 655 |
'!(foo)test.png!' => "<p><img src=\"test.png\" class=\"wiki-class-foo\" alt=\"\" /></p>",
|
|
| 665 |
'!(foo)test.png!' => "<p><img src=\"test.png\" class=\"wiki-class-foo\" alt=\"\"></p>", |
|
| 656 | 666 |
'%(foo)test%' => "<p><span class=\"wiki-class-foo\">test</span></p>", |
| 657 | 667 |
'p(foo). test' => "<p class=\"wiki-class-foo\">test</p>", |
| 658 | 668 |
'|(foo). test|' => |
| 659 |
"<table>\n\t\t<tr>\n\t\t\t<td class=\"wiki-class-foo\">test</td>\n\t\t</tr>\n\t</table>",
|
|
| 669 |
"<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>",
|
|
| 660 | 670 |
}, |
| 661 | 671 |
false |
| 662 | 672 |
) |
| ... | ... | |
| 665 | 675 |
def test_should_prefix_id_attribute_on_tags |
| 666 | 676 |
assert_html_output( |
| 667 | 677 |
{
|
| 668 |
'!(#foo)test.png!' => "<p><img src=\"test.png\" id=\"wiki-id-foo\" alt=\"\" /></p>",
|
|
| 678 |
'!(#foo)test.png!' => "<p><img src=\"test.png\" id=\"wiki-id-foo\" alt=\"\"></p>", |
|
| 669 | 679 |
'%(#foo)test%' => "<p><span id=\"wiki-id-foo\">test</span></p>", |
| 670 | 680 |
'p(#foo). test' => "<p id=\"wiki-id-foo\">test</p>", |
| 671 | 681 |
'|(#foo). test|' => |
| 672 |
"<table>\n\t\t<tr>\n\t\t\t<td id=\"wiki-id-foo\">test</td>\n\t\t</tr>\n\t</table>",
|
|
| 682 |
"<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>",
|
|
| 673 | 683 |
}, |
| 674 | 684 |
false |
| 675 | 685 |
) |
| ... | ... | |
| 679 | 689 |
assert_html_output( |
| 680 | 690 |
{
|
| 681 | 691 |
'!(wiki-class-foo#wiki-id-bar)test.png!' => |
| 682 |
"<p><img src=\"test.png\" class=\"wiki-class-foo\" id=\"wiki-id-bar\" alt=\"\" /></p>",
|
|
| 692 |
"<p><img src=\"test.png\" class=\"wiki-class-foo\" id=\"wiki-id-bar\" alt=\"\"></p>", |
|
| 683 | 693 |
}, |
| 684 | 694 |
false |
| 685 | 695 |
) |
| ... | ... | |
| 711 | 721 |
</pree> |
| 712 | 722 |
STR |
| 713 | 723 |
expected = <<~EXPECTED |
| 714 |
<p><pree><br />
|
|
| 715 |
This is some text<br />
|
|
| 724 |
<p><pree><br> |
|
| 725 |
This is some text<br> |
|
| 716 | 726 |
</pree></p> |
| 717 | 727 |
EXPECTED |
| 718 | 728 |
assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '')
|
| vendor/javascript/tablesort.min.js | ||
|---|---|---|
| 1 |
/*! |
|
| 2 |
* tablesort v5.7.0 (2026-01-03) |
|
| 3 |
* http://tristen.ca/tablesort/demo/ |
|
| 4 |
* Copyright (c) 2026 ; Licensed MIT |
|
| 5 |
*/ |
|
| 6 |
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};
|
|
| vendor/javascript/tablesort.number.min.js | ||
|---|---|---|
| 1 |
/*! |
|
| 2 |
* tablesort v5.7.0 (2026-01-03) |
|
| 3 |
* http://tristen.ca/tablesort/demo/ |
|
| 4 |
* Copyright (c) 2026 ; Licensed MIT |
|
| 5 |
*/ |
|
| 6 |
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};
|
|