From acf0f9019ed687e941344e1be57d2e31a0ffa03e Mon Sep 17 00:00:00 2001 From: Marius Balteanu Date: Fri, 23 Jan 2026 16:05:42 +0000 Subject: [PATCH] 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 --- app/assets/javascripts/application-legacy.js | 2 - app/helpers/application_helper.rb | 3 - .../controllers/tablesort_controller.js | 18 ++++ app/views/journals/update.js.erb | 1 - config/importmap.rb | 2 + .../wiki_formatting/common_mark/formatter.rb | 1 + .../wiki_formatting/tablesort_scrubber.rb | 27 ++++++ .../wiki_formatting/textile/formatter.rb | 30 +++++- test/helpers/application_helper_test.rb | 84 ++++++++--------- .../tablesort_scrubber_test.rb | 92 +++++++++++++++++++ .../wiki_formatting/textile_formatter_test.rb | 40 +++++--- vendor/javascript/tablesort.min.js | 6 ++ vendor/javascript/tablesort.number.min.js | 6 ++ 13 files changed, 245 insertions(+), 67 deletions(-) create mode 100644 app/javascript/controllers/tablesort_controller.js create mode 100644 lib/redmine/wiki_formatting/tablesort_scrubber.rb create mode 100644 test/unit/lib/redmine/wiki_formatting/tablesort_scrubber_test.rb create mode 100644 vendor/javascript/tablesort.min.js create mode 100644 vendor/javascript/tablesort.number.min.js diff --git a/app/assets/javascripts/application-legacy.js b/app/assets/javascripts/application-legacy.js index 3ee2e4335..833e7d8d1 100644 --- a/app/assets/javascripts/application-legacy.js +++ b/app/assets/javascripts/application-legacy.js @@ -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); }); diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6ea148f2d..df3bc8ddc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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)) diff --git a/app/javascript/controllers/tablesort_controller.js b/app/javascript/controllers/tablesort_controller.js new file mode 100644 index 000000000..12db0789a --- /dev/null +++ b/app/javascript/controllers/tablesort_controller.js @@ -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); + } +} diff --git a/app/views/journals/update.js.erb b/app/views/journals/update.js.erb index cf6bcd28f..218a164de 100644 --- a/app/views/journals/update.js.erb +++ b/app/views/journals/update.js.erb @@ -14,7 +14,6 @@ } else { journal_header.append('<%= escape_javascript(render_journal_update_info(@journal)) %>'); } - setupWikiTableSortableHeader(); setupCopyButtonsToPreElements(); setupHoverTooltips(); <% end %> diff --git a/config/importmap.rb b/config/importmap.rb index 3560330c1..795429f02 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -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" diff --git a/lib/redmine/wiki_formatting/common_mark/formatter.rb b/lib/redmine/wiki_formatting/common_mark/formatter.rb index 0a5137209..d9d3e9640 100644 --- a/lib/redmine/wiki_formatting/common_mark/formatter.rb +++ b/lib/redmine/wiki_formatting/common_mark/formatter.rb @@ -55,6 +55,7 @@ module Redmine SANITIZER = SanitizationFilter.new SCRUBBERS = [ SyntaxHighlightScrubber.new, + Redmine::WikiFormatting::TablesortScrubber.new, FixupAutoLinksScrubber.new, ExternalLinksScrubber.new, AlertsIconsScrubber.new diff --git a/lib/redmine/wiki_formatting/tablesort_scrubber.rb b/lib/redmine/wiki_formatting/tablesort_scrubber.rb new file mode 100644 index 000000000..01f368a8a --- /dev/null +++ b/lib/redmine/wiki_formatting/tablesort_scrubber.rb @@ -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 diff --git a/lib/redmine/wiki_formatting/textile/formatter.rb b/lib/redmine/wiki_formatting/textile/formatter.rb index cfb941711..462301461 100644 --- a/lib/redmine/wiki_formatting/textile/formatter.rb +++ b/lib/redmine/wiki_formatting/textile/formatter.rb @@ -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) diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb index 92c2cd1d9..4d0834773 100644 --- a/test/helpers/application_helper_test.rb +++ b/test/helpers/application_helper_test.rb @@ -80,7 +80,7 @@ class ApplicationHelperTest < Redmine::HelperTest '(see inline link).', 'www.foo.bar' => 'www.foo.bar', 'http://foo.bar/page?p=1&t=z&s=' => - 'http://foo.bar/page?p=1&t=z&s=', + 'http://foo.bar/page?p=1&t=z&s=', 'http://foo.bar/page#125' => 'http://foo.bar/page#125', 'http://foo@www.bar.com' => 'http://foo@www.bar.com', 'http://foo:bar@www.bar.com' => 'http://foo:bar@www.bar.com', @@ -92,7 +92,7 @@ class ApplicationHelperTest < Redmine::HelperTest '' \ 'http://example.net/path!602815048C7B5C20!302.html', # escaping - 'http://foo"bar' => 'http://foo"bar', + 'http://foo"bar' => 'http://foo"bar', # wrap in angle brackets '' => '<http://foo.bar>', # invalid urls @@ -130,22 +130,22 @@ class ApplicationHelperTest < Redmine::HelperTest def test_inline_images to_test = { - '!http://foo.bar/image.jpg!' => '', + '!http://foo.bar/image.jpg!' => '', 'floating !>http://foo.bar/image.jpg!' => - 'floating ', + 'floating ', 'with class !(some-class)http://foo.bar/image.jpg!' => - 'with class ', + 'with class ', 'with class !(wiki-class-foo)http://foo.bar/image.jpg!' => - 'with class ', + 'with class ', 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => - 'with style ', + 'with style ', 'with title !http://foo.bar/image.jpg(This is a title)!' => - 'with title This is a title', + 'with title This is a title', 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title ', + 'alt="This is a double-quoted "title"">', 'with query string !http://foo.bar/image.cgi?a=1&b=2!' => - 'with query string ' + 'with query string ' } with_settings :text_formatting => 'textile' do to_test.each {|text, result| assert_equal "

#{result}

", textilizable(text)} @@ -161,24 +161,24 @@ class ApplicationHelperTest < Redmine::HelperTest p=. !bar.gif! RAW with_settings :text_formatting => 'textile' do - assert textilizable(raw).include?('') - assert textilizable(raw).include?('') + assert textilizable(raw).include?('') + assert textilizable(raw).include?('') end end def test_attached_images to_test = { 'Inline image: !logo.gif!' => - 'Inline image: This is a logo', + 'Inline image: This is a logo', 'Inline image: !logo.GIF!' => - 'Inline image: This is a logo', + 'Inline image: This is a logo', 'Inline WebP image: !logo.webp!' => - 'Inline WebP image: WebP image', - 'No match: !ogo.gif!' => 'No match: ', - 'No match: !ogo.GIF!' => 'No match: ', + 'Inline WebP image: WebP image', + 'No match: !ogo.gif!' => 'No match: ', + 'No match: !ogo.GIF!' => 'No match: ', # link image '!logo.gif!:http://foo.bar/' => - 'This is a logo', + 'This is a logo', } 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[alt text], + assert_match %r[alt text], textilizable('!logo.gif(alt text)!', attachments: attachments) # When alt text and style are set - assert_match %r[alt text], + assert_match %r[alt text], textilizable('!{width:100px}logo.gif(alt text)!', attachments: attachments) # When alt text is not set - assert_match %r[This is a logo], + assert_match %r[This is a logo], textilizable('!logo.gif!', attachments: attachments) # When alt text is not set and the attachment has no description - assert_match %r[], + assert_match %r[], textilizable('!testfile.PNG!', attachments: attachments) # When no matching attachments are found - assert_match %r[], + assert_match %r[], textilizable('!no-match.jpg!', attachments: attachments) - assert_match %r[alt text], + assert_match %r[alt text], textilizable('!no-match.jpg(alt text)!', attachments: attachments) # When no attachment is registered - assert_match %r[], + assert_match %r[], textilizable('!logo.gif!', attachments: []) - assert_match %r[alt text], + assert_match %r[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?("\"\"") - assert textilizable(raw, :object => journal).include?("\"\"") + assert textilizable(raw, :object => journal).include?("\"\"") + assert textilizable(raw, :object => journal).include?("\"\"") 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 %(), + assert_include %(), textilizable("!#{filename}!", :attachments => [attachment]) end end @@ -272,7 +272,7 @@ class ApplicationHelperTest < Redmine::HelperTest with_settings :text_formatting => 'textile' do assert_equal( %(

), + %(srcset="/attachments/download/#{attachment.id}/image@2x.png 2x" alt="" loading="lazy">

), textilizable("!image@2x.png!", :attachments => [attachment]) ) end @@ -325,13 +325,13 @@ class ApplicationHelperTest < Redmine::HelperTest to_test = { 'Inline image: !testtest.jpg!' => - 'Inline image: ', + 'Inline image: ', 'Inline image: !testtest.jpeg!' => - 'Inline image: ', + 'Inline image: ', 'Inline image: !testtest.jpe!' => - 'Inline image: ', + 'Inline image: ', 'Inline image: !testtest.bmp!' => - 'Inline image: ', + 'Inline image: ', } attachments = [a1, a2, a3, a4] @@ -356,9 +356,9 @@ class ApplicationHelperTest < Redmine::HelperTest to_test = { 'Inline image: !testfile.png!' => - 'Inline image: ', + 'Inline image: ', 'Inline image: !Testfile.PNG!' => - 'Inline image: ', + 'Inline image: ', } 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\":

\n\n\n\t

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
and another on a second line\":test", + "This is a double quote \"on the first line
and another on a second line\":test", # mailto link "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "system administrator", @@ -391,7 +391,7 @@ class ApplicationHelperTest < Redmine::HelperTest '(see "inline link":http://www.foo.bar/Test-)' => '(see inline link)', 'http://foo.bar/page?p=1&t=z&s=-' => - 'http://foo.bar/page?p=1&t=z&s=-', + 'http://foo.bar/page?p=1&t=z&s=-', 'This is an intern "link":/foo/bar-' => 'This is an intern link' } with_settings :text_formatting => 'textile' do @@ -1317,10 +1317,10 @@ class ApplicationHelperTest < Redmine::HelperTest def test_html_tags to_test = { "

content
" => "

<div>content</div>

", - "
content
" => "

<div class="bold">content</div>

", + "
content
" => "

<div class=\"bold\">content</div>

", "" => "

<script>some script;</script>

", # do not escape pre/code tags - "
\nline 1\nline2
" => "
\nline 1\nline2
", + "
\nline 1\nline2
" => "
line 1\nline2
", "
\nline 1\nline2
" => "
\nline 1\nline2
", "
content
" => "
<div class=\"foo\">content</div>
", "
content
" => "
<div class=\"<foo\">content</div>
", @@ -1477,7 +1477,7 @@ class ApplicationHelperTest < Redmine::HelperTest "Cell 21#{link3}" @project = Project.find(1) with_settings :text_formatting => 'textile' do - assert_equal "#{result}
", textilizable(text).gsub(/[\t\n]/, '') + assert_equal "#{result}
", 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 '
', textilizable('---') + assert_equal '
', textilizable('---') assert_equal '

Dashes: ---

', textilizable('Dashes: ---') end end diff --git a/test/unit/lib/redmine/wiki_formatting/tablesort_scrubber_test.rb b/test/unit/lib/redmine/wiki_formatting/tablesort_scrubber_test.rb new file mode 100644 index 000000000..8144309c0 --- /dev/null +++ b/test/unit/lib/redmine/wiki_formatting/tablesort_scrubber_test.rb @@ -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 + + + + + + + + + + + + + +
AB
+ 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 + + + + + + + + + +
AB
+ 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 + + + + + + + + + + + + + +
AB
+ HTML + expected = <<~HTML + + + + + + + + + + + + + +
AB
+ HTML + with_settings :wiki_tablesort_enabled => 1 do + assert_equal expected, filter(input) + end + end +end diff --git a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb index 678d4c6b2..dd11ea99e 100644 --- a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb @@ -252,7 +252,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase expected = <<~EXPECTED

John said:

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
  • Donec odio lorem,
  • @@ -282,9 +282,11 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase

    This is a table with empty cells:

    + +
    cell11cell12
    cell21cell23
    cell31cell32cell33
    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 + +
    right
    left
    justify
    EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') @@ -318,9 +322,11 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase

    This is a table with trailing whitespace in one row:

    + +
    cell11cell12
    cell21cell22
    cell31cell32
    EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') @@ -343,21 +349,23 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase

    This is a table with line breaks:

    + - + - + - + +
    cell11
    continued
    cell11
    continued
    cell12
    cell21 cell23
    cell23 line2
    cell23 line3
    cell23
    cell23 line2
    cell23 line3
    cell31cell32
    cell32 line2
    cell32
    cell32 line2
    cell33
    EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') @@ -380,18 +388,20 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase

    This is a table with lists:

    + - + - + +
    cell11 cell12
    cell21ordered list
    # item
    # item 2
    ordered list
    # item
    # item 2
    cell31unordered list
    * item
    * item 2
    unordered list
    * item
    * item 2
    EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') @@ -408,7 +418,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase expected = '

    ' + '&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;" alt="">

    ' assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') end @@ -635,7 +645,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase "unsupported language" => "unsupported language", "special-char language" => - "special-char language", + "special-char language", }, 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!' => "

    \"\"

    ", + '!(foo)test.png!' => "

    \"\"

    ", '%(foo)test%' => "

    test

    ", 'p(foo). test' => "

    test

    ", '|(foo). test|' => - "\n\t\t\n\t\t\t\n\t\t\n\t
    test
    ", + "\n\t\t\n\t\t\t\n\t\t\n\t
    test
    ", }, 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!' => "

    \"\"

    ", + '!(#foo)test.png!' => "

    \"\"

    ", '%(#foo)test%' => "

    test

    ", 'p(#foo). test' => "

    test

    ", '|(#foo). test|' => - "\n\t\t\n\t\t\t\n\t\t\n\t
    test
    ", + "\n\t\t\n\t\t\t\n\t\t\n\t
    test
    ", }, false ) @@ -679,7 +689,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase assert_html_output( { '!(wiki-class-foo#wiki-id-bar)test.png!' => - "

    \"\"

    ", + "

    \"\"

    ", }, false ) @@ -711,8 +721,8 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase STR expected = <<~EXPECTED -

    <pree>
    - This is some text
    +

    <pree>
    + This is some text
    </pree>

    EXPECTED assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '') diff --git a/vendor/javascript/tablesort.min.js b/vendor/javascript/tablesort.min.js new file mode 100644 index 000000000..01a6be3e3 --- /dev/null +++ b/vendor/javascript/tablesort.min.js @@ -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:n0)if(t.tHead&&t.tHead.rows.length>0){for(let s=0;s{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;i0&&f.push(r),c++;if(!f)return}for(let o=0;o