+````````````````````````````````
+
+Inapplicable task in a "loose" list. Note that the `` tag is not applied to the
+loose text; it has strikethrough applied with CSS.
+
+```````````````````````````````` example gitlab tasklist
+- [~] inapplicable
+
+ text in loose list
+.
+
+
+
+
+
+
+inapplicable
+
+
+
+text in loose list
+
+
+
+````````````````````````````````
diff --git a/glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml b/glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml
index b09a092c02a503eec6c154bb9468bb96aaa571a5..3881819e38ae582b8d413713c6622b5b5661b73d 100644
--- a/glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml
+++ b/glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml
@@ -12,3 +12,15 @@
skip_running_snapshot_static_html_tests: false # NOT YET SUPPORTED
skip_running_snapshot_wysiwyg_html_tests: false
skip_running_snapshot_prosemirror_json_tests: false
+07_02__gitlab_specific_markdown__task_list_items__003:
+ skip_update_example_snapshot_html_wysiwyg: Inapplicable task list items not yet implemented for WYSYWIG
+ skip_update_example_snapshot_prosemirror_json: Inapplicable task list items not yet implemented for WYSYWIG
+ skip_running_conformance_wysiwyg_tests: Inapplicable task list items not yet implemented for WYSYWIG
+ skip_running_snapshot_wysiwyg_html_tests: Inapplicable task list items not yet implemented for WYSYWIG
+ skip_running_snapshot_prosemirror_json_tests: Inapplicable task list items not yet implemented for WYSYWIG
+07_02__gitlab_specific_markdown__task_list_items__004:
+ skip_update_example_snapshot_html_wysiwyg: Inapplicable task list items not yet implemented for WYSYWIG
+ skip_update_example_snapshot_prosemirror_json: Inapplicable task list items not yet implemented for WYSYWIG
+ skip_running_conformance_wysiwyg_tests: Inapplicable task list items not yet implemented for WYSYWIG
+ skip_running_snapshot_wysiwyg_html_tests: Inapplicable task list items not yet implemented for WYSYWIG
+ skip_running_snapshot_prosemirror_json_tests: Inapplicable task list items not yet implemented for WYSYWIG
diff --git a/glfm_specification/output/spec.txt b/glfm_specification/output/spec.txt
index 3fc27efdc34444f0391095cc2e88bfee028fc6eb..b2735219d02ad0ef34274bb06a263daf00d13414 100644
--- a/glfm_specification/output/spec.txt
+++ b/glfm_specification/output/spec.txt
@@ -9641,6 +9641,88 @@ footnote text
````````````````````````````````
+## Task list items
+
+See
+[Task lists](https://docs.gitlab.com/ee/user/markdown.html#task-lists) in the GitLab Flavored Markdown documentation.
+
+Task list items (checkboxes) are defined as a GitHub Flavored Markdown extension in a section above.
+GitLab extends the behavior of task list items to support additional features.
+Some of these features are in-progress, and should not yet be considered part of the official
+GitLab Flavored Markdown specification.
+
+Some of the behavior of task list items is implemented as client-side JavaScript/CSS.
+
+The following are some basic examples; more examples may be added in the future.
+
+Incomplete task:
+
+```````````````````````````````` example gitlab tasklist
+- [ ] incomplete
+.
+
+````````````````````````````````
+
+Inapplicable task in a "loose" list. Note that the `` tag is not applied to the
+loose text; it has strikethrough applied with CSS.
+
+```````````````````````````````` example gitlab tasklist
+- [~] inapplicable
+
+ text in loose list
+.
+
+
+
+
+
+
+inapplicable
+
+
+
+text in loose list
+
+
+
+````````````````````````````````
+
# Appendix: A parsing strategy
diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index 896f67cb8759c40abb2a41c2caaa3bbefae12cec..e8a7677b102490a3909c7e0fe462929ce0f510d0 100644
--- a/lib/banzai/filter/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -8,9 +8,93 @@
# - app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
module Banzai
module Filter
+ # TaskList filter replaces task list item markers (`[ ]`, `[x]`, and `[~]`)
+ # with checkboxes, marked up with metadata and behavior.
+ #
+ # This should be run on the HTML generated by the Markdown filter, after the
+ # SanitizationFilter.
+ #
+ # Syntax
+ # ------
+ #
+ # Task list items must be in a list format:
+ #
+ # ```
+ # - [ ] incomplete
+ # - [x] complete
+ # - [~] inapplicable
+ # ```
+ #
+ # This class overrides TaskList::Filter in the `deckar01-task_list` gem
+ # to add support for inapplicable task items
class TaskListFilter < TaskList::Filter
+ extend ::Gitlab::Utils::Override
+
+ XPATH = 'descendant-or-self::li[input[@data-inapplicable]] | descendant-or-self::li[p[input[@data-inapplicable]]]'
+ INAPPLICABLE = '[~]'
+ INAPPLICABLEPATTERN = /\[~\]/.freeze
+
+ # Pattern used to identify all task list items.
+ # Useful when you need iterate over all items.
+ NEWITEMPATTERN = /
+ ^
+ (?:\s*[-+*]|(?:\d+\.))? # optional list prefix
+ \s* # optional whitespace prefix
+ ( # checkbox
+ #{CompletePattern}|
+ #{IncompletePattern}|
+ #{INAPPLICABLEPATTERN}
+ )
+ (?=\s) # followed by whitespace
+ /x.freeze
+
+ # Force the gem's constant to use our new one
+ superclass.send(:remove_const, :ItemPattern) # rubocop: disable GitlabSecurity/PublicSend
+ superclass.const_set(:ItemPattern, NEWITEMPATTERN)
+
+ def inapplicable?(item)
+ !!(item.checkbox_text =~ INAPPLICABLEPATTERN)
+ end
+
+ override :render_item_checkbox
def render_item_checkbox(item)
- "#{super}"
+ %()
+ end
+
+ override :render_task_list_item
+ def render_task_list_item(item)
+ source = item.source
+
+ if inapplicable?(item)
+ # Add a `` tag around the list item text. However because of the
+ # way tasks are built, the source can include an embedded sublist, like
+ # `[~] foobar\n` should only be added to the main text.
+ source = source.partition("#{INAPPLICABLE} ")
+ text = source.last.partition(/\<(ol|ul)/)
+ text[0] = "#{text[0]}"
+ source[-1] = text.join
+ source = source.join
+ end
+
+ Nokogiri::HTML.fragment \
+ source.sub(ItemPattern, render_item_checkbox(item)), 'utf-8'
+ end
+
+ override :call
+ def call
+ super
+
+ # add class to li for any inapplicable checkboxes
+ doc.xpath(XPATH).each do |li|
+ li.add_class('inapplicable')
+ end
+
+ doc
end
end
end
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index d472134a2c780fd4d763e494c997dd520959a85a..b5bf9279371547e2a3908f23bf67bbf151ea99bf 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -109,10 +109,24 @@
<<~GFM,
* [ ] Unchecked task
* [x] Checked task
+ * [~] Inapplicable task
+ * [~] Inapplicable task with ~~del~~ and strike embedded
GFM
- <<~GFM
+ <<~GFM,
1. [ ] Unchecked ordered task
1. [x] Checked ordered task
+ 1. [~] Inapplicable ordered task
+ 1. [~] Inapplicable ordered task with ~~del~~ and strike embedded
+ GFM
+ <<~GFM
+ * [ ] Unchecked loose list task
+ * [x] Checked loose list task
+ * [~] Inapplicable loose list task
+
+ With a paragraph
+ * [~] Inapplicable loose list task with ~~del~~ and strike embedded
+
+ With a paragraph
GFM
)
@@ -605,7 +619,8 @@ def foo
'###### Heading',
'**Bold**',
'*Italics*',
- '~~Strikethrough~~',
+ '~~Strikethrough (del)~~',
+ 'Strikethrough',
'---',
# table
<<~GFM,
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 2da16408fbc8225cfa322da1b1a8a429b37f5253..18cd63b7bcb3fd83ccb5296f8fad1dcf39b00816 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -275,9 +275,11 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- [ ] Incomplete task 1
- [x] Complete task 1
+- [~] Inapplicable task 1
- [ ] Incomplete task 2
- [ ] Incomplete sub-task 1
- [ ] Incomplete sub-task 2
+ - [~] Inapplicable sub-task 1
- [x] Complete sub-task 1
- [X] Complete task 2
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index d1bca3c73b64381ae1084817cb0b7cc5a3e8db19..8f37c79e2354f6366579e0816979d50b391257a9 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -193,6 +193,7 @@ describe('init markdown', () => {
${'- [ ] item'} | ${'- [ ] item\n- [ ] '}
${'- [x] item'} | ${'- [x] item\n- [ ] '}
${'- [X] item'} | ${'- [X] item\n- [ ] '}
+ ${'- [~] item'} | ${'- [~] item\n- [ ] '}
${'- [ ] nbsp (U+00A0)'} | ${'- [ ] nbsp (U+00A0)\n- [ ] '}
${'- item\n - second'} | ${'- item\n - second\n - '}
${'- - -'} | ${'- - -'}
@@ -205,6 +206,7 @@ describe('init markdown', () => {
${'1. [ ] item'} | ${'1. [ ] item\n2. [ ] '}
${'1. [x] item'} | ${'1. [x] item\n2. [ ] '}
${'1. [X] item'} | ${'1. [X] item\n2. [ ] '}
+ ${'1. [~] item'} | ${'1. [~] item\n2. [ ] '}
${'108. item'} | ${'108. item\n109. '}
${'108. item\n - second'} | ${'108. item\n - second\n - '}
${'108. item\n 1. second'} | ${'108. item\n 1. second\n 2. '}
@@ -228,11 +230,13 @@ describe('init markdown', () => {
${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'}
${'- [x] item\n- [x] '} | ${'- [x] item\n'}
${'- [X] item\n- [X] '} | ${'- [X] item\n'}
+ ${'- [~] item\n- [~] '} | ${'- [~] item\n'}
${'- item\n - second\n - '} | ${'- item\n - second\n'}
${'1. item\n2. '} | ${'1. item\n'}
${'1. [ ] item\n2. [ ] '} | ${'1. [ ] item\n'}
${'1. [x] item\n2. [x] '} | ${'1. [x] item\n'}
${'1. [X] item\n2. [X] '} | ${'1. [X] item\n'}
+ ${'1. [~] item\n2. [~] '} | ${'1. [~] item\n'}
${'108. item\n109. '} | ${'108. item\n'}
${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb
index c89acd1a643d8e1a0e4324aadfa217dd2f24760d..920904b0f29b77ced16939e538cc4556f765811e 100644
--- a/spec/lib/banzai/filter/task_list_filter_spec.rb
+++ b/spec/lib/banzai/filter/task_list_filter_spec.rb
@@ -10,4 +10,38 @@
expect(doc.xpath('.//li//task-button').count).to eq(2)
end
+
+ describe 'inapplicable list items' do
+ shared_examples 'a valid inapplicable task list item' do |html|
+ it "behaves correctly for `#{html}`" do
+ doc = filter("
#{html}
")
+
+ expect(doc.css('li.inapplicable input[data-inapplicable]').count).to eq(1)
+ expect(doc.css('li.inapplicable > s').count).to eq(1)
+ end
+ end
+
+ shared_examples 'an invalid inapplicable task list item' do |html|
+ it "does nothing for `#{html}`" do
+ doc = filter("
#{html}
")
+
+ expect(doc.css('li.inapplicable input[data-inapplicable]').count).to eq(0)
+ end
+ end
+
+ it_behaves_like 'a valid inapplicable task list item', '[~] foobar'
+ it_behaves_like 'a valid inapplicable task list item', '[~] foo bar'
+ it_behaves_like 'an invalid inapplicable task list item', '[ ] foobar'
+ it_behaves_like 'an invalid inapplicable task list item', '[x] foobar'
+ it_behaves_like 'an invalid inapplicable task list item', 'foo [~] bar'
+
+ it 'does not wrap a sublist with ' do
+ html = '[~] foo bar\n
sublist
'
+ doc = filter("
#{html}
")
+
+ expect(doc.to_html).to include('foo bar\n')
+ expect(doc.css('li.inapplicable input[data-inapplicable]').count).to eq(1)
+ expect(doc.css('li.inapplicable > s').count).to eq(1)
+ end
+ end
end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 1932f78506fa0ba0be1a470e16e8561f090bdbb9..8bec3be2535fe9e18c1459a5b7d5c53de684cde4 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -189,8 +189,10 @@ def have_image(src)
match do |actual|
expect(actual).to have_selector('ul.task-list', count: 2)
- expect(actual).to have_selector('li.task-list-item', count: 7)
+ expect(actual).to have_selector('li.task-list-item', count: 9)
+ expect(actual).to have_selector('li.task-list-item.inapplicable > s', count: 2)
expect(actual).to have_selector('input[checked]', count: 3)
+ expect(actual).to have_selector('input[data-inapplicable]', count: 2)
end
end