diff --git a/app/assets/javascripts/behaviors/markdown/marks/strike.js b/app/assets/javascripts/behaviors/markdown/marks/strike.js index 967c0a120cd9e801dc70e146f8f0294ea7df1733..afab266b645654c3da9d80d2c075710bb69341d5 100644 --- a/app/assets/javascripts/behaviors/markdown/marks/strike.js +++ b/app/assets/javascripts/behaviors/markdown/marks/strike.js @@ -2,16 +2,35 @@ export default () => ({ name: 'strike', schema: { - parseDOM: [ - { - tag: 'del', + attrs: { + strike: { + default: false, + }, + inapplicable: { + default: false, }, + }, + parseDOM: [ + { tag: 'li.inapplicable > s', attrs: { inapplicable: true } }, + { tag: 'li.inapplicable > p:first-of-type > s', attrs: { inapplicable: true } }, + { tag: 's', attrs: { strike: true } }, + { tag: 'del' }, ], toDOM: () => ['s', 0], }, toMarkdown: { - open: '~~', - close: '~~', + open(_, mark) { + if (mark.attrs.strike) { + return ''; + } + return mark.attrs.inapplicable ? '' : '~~'; + }, + close(_, mark) { + if (mark.attrs.strike) { + return ''; + } + return mark.attrs.inapplicable ? '' : '~~'; + }, mixable: true, expelEnclosingWhitespace: true, }, diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js index 10ffce9b1b8730792af87df7d749a3587e85e287..095634340c110fbce7c0a12f98d6a0588edb9ed7 100644 --- a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js +++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js @@ -5,8 +5,8 @@ export default () => ({ name: 'task_list_item', schema: { attrs: { - done: { - default: false, + state: { + default: null, }, }, defining: true, @@ -18,21 +18,53 @@ export default () => ({ tag: 'li.task-list-item', getAttrs: (el) => { const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox'); - return { done: checkbox && checkbox.checked }; + if (checkbox?.matches('[data-inapplicable]')) { + return { state: 'inapplicable' }; + } else if (checkbox?.checked) { + return { state: 'done' }; + } + + return {}; }, }, ], toDOM(node) { return [ 'li', - { class: 'task-list-item' }, - ['input', { type: 'checkbox', class: 'task-list-item-checkbox', checked: node.attrs.done }], + { + class: () => { + if (node.attrs.state === 'inapplicable') { + return 'task-list-item inapplicable'; + } + + return 'task-list-item'; + }, + }, + [ + 'input', + { + type: 'checkbox', + class: 'task-list-item-checkbox', + checked: node.attrs.state === 'done', + 'data-inapplicable': node.attrs.state === 'inapplicable', + }, + ], ['div', { class: 'todo-content' }, 0], ]; }, }, toMarkdown(state, node) { - state.write(`[${node.attrs.done ? 'x' : ' '}] `); + switch (node.attrs.state) { + case 'done': + state.write('[x] '); + break; + case 'inapplicable': + state.write('[~] '); + break; + default: + state.write('[ ] '); + break; + } state.renderContent(node); }, }); diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index 449da3948413c9491b12d6bb583c7cf4db2e871f..3a93bccee88a19a0cfb745e874d011ba2915a24f 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -315,7 +315,7 @@ export default { } this.taskButtons = []; - const taskListFields = this.$el.querySelectorAll('.task-list-item'); + const taskListFields = this.$el.querySelectorAll('.task-list-item:not(.inapplicable)'); taskListFields.forEach((item, index) => { const taskLink = item.querySelector('.gfm-issue'); diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 243de48948c43e62739766efbaaec1a39ad7699e..262cf024ee3d998affbee8ec2689f3f3208111ea 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -9,7 +9,7 @@ const LINK_TAG_PATTERN = '[{text}](url)'; // a bullet point character (*+-) and an optional checkbox ([ ] [x]) // OR a number with a . after it and an optional checkbox ([ ] [x]) // followed by one or more whitespace characters -const LIST_LINE_HEAD_PATTERN = /^(?\s*)(?((?[*+-])|(?\d+\.))( \[([xX\s])\])?\s)(?.)?/; +const LIST_LINE_HEAD_PATTERN = /^(?\s*)(?((?[*+-])|(?\d+\.))( \[([xX~\s])\])?\s)(?.)?/; // detect a horizontal rule that might be mistaken for a list item (not full pattern for an
) const HR_PATTERN = /^((\s{0,3}-+\s*-+\s*-+\s*[\s-]*)|(\s{0,3}\*+\s*\*+\s*\*+\s*[\s*]*))$/; @@ -399,7 +399,7 @@ function handleContinueList(e, textArea) { itemToInsert = `${indent}${leader}`; } - itemToInsert = itemToInsert.replace(/\[x\]/i, '[ ]'); + itemToInsert = itemToInsert.replace(/\[[x~]\]/i, '[ ]'); e.preventDefault(); diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index 79a30340856b8b5f7354608695a82bacdd3fae27..6e72d95c8e653e93f694a85e75ad4aab9235fe90 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -62,13 +62,21 @@ export default class TaskList { .prop('disabled', true); } + updateInapplicableTaskListItems(e) { + this.getTaskListTarget(e) + .find('.task-list-item-checkbox[data-inapplicable]') + .prop('disabled', true); + } + disableTaskListItems(e) { this.getTaskListTarget(e).taskList('disable'); + this.updateInapplicableTaskListItems(); } enableTaskListItems(e) { this.getTaskListTarget(e).taskList('enable'); this.disableNonMarkdownTaskListItems(e); + this.updateInapplicableTaskListItems(e); } enable() { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index b5e0dcd875accebc5cd5465cfd1f1535f828ea57..031f5dc45cadadab702d878d64807fb4562b9859 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -435,6 +435,35 @@ } } + li.inapplicable { + // for a single line list item, no paragraph (tight list) + > s { + color: $gl-text-color-disabled; + } + + // additional blocks, other than paragraphs + > div { + text-decoration: line-through; + color: $gl-text-color-disabled; + } + + // because of the embedded checkbox, putting line-through on the entire + // paragraph causes the space between the checkbox and the text to have the + // line-through. Targeting just the `s` fixes this + > p:first-of-type > s { + color: $gl-text-color-disabled; + } + + > p:not(:first-of-type) { + text-decoration: line-through; + color: $gl-text-color-disabled; + } + + .drag-icon { + color: $gl-text-color; + } + } + a.with-attachment-icon, a[href*='/uploads/'], a[href*='storage.googleapis.com/google-code-attachments/'] { diff --git a/doc/user/img/completed_tasks_v13_3.png b/doc/user/img/completed_tasks_v13_3.png deleted file mode 100644 index b12d95f0a236da771b751594fd094ec598bbb62c..0000000000000000000000000000000000000000 Binary files a/doc/user/img/completed_tasks_v13_3.png and /dev/null differ diff --git a/doc/user/img/completed_tasks_v15_3.png b/doc/user/img/completed_tasks_v15_3.png new file mode 100644 index 0000000000000000000000000000000000000000..09174c688dabf3a850fa8b212bbce8a64efb46b8 Binary files /dev/null and b/doc/user/img/completed_tasks_v15_3.png differ diff --git a/doc/user/markdown.md b/doc/user/markdown.md index b089c84a784e95d1a028749428bd15e5f28c56fd..6a524fe206a9cb939b64dc3e1cc3beba1476b9f8 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -376,6 +376,8 @@ the [Asciidoctor user manual](https://asciidoctor.org/docs/user-manual/#activati ### Task lists +> Inapplicable checkboxes [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85982) in GitLab 15.3. + [View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#task-lists). You can add task lists anywhere Markdown is supported. @@ -384,22 +386,28 @@ You can add task lists anywhere Markdown is supported. - In all other places, you cannot select the boxes. You must edit the Markdown manually by adding or removing an `x` in the brackets. +Besides complete and incomplete, tasks can also be **inapplicable**. Selecting an inapplicable checkbox +in an issue, merge request, or comment has no effect. + To create a task list, follow the format of an ordered or unordered list: ```markdown - [x] Completed task +- [~] Inapplicable task - [ ] Incomplete task - - [ ] Sub-task 1 - - [x] Sub-task 2 + - [x] Sub-task 1 + - [~] Sub-task 2 - [ ] Sub-task 3 1. [x] Completed task +1. [~] Inapplicable task 1. [ ] Incomplete task - 1. [ ] Sub-task 1 - 1. [x] Sub-task 2 + 1. [x] Sub-task 1 + 1. [~] Sub-task 2 + 1. [ ] Sub-task 3 ``` -![Task list as rendered by GitLab](img/completed_tasks_v13_3.png) +![Task list as rendered by GitLab](img/completed_tasks_v15_3.png) ### Table of contents diff --git a/glfm_specification/example_snapshots/examples_index.yml b/glfm_specification/example_snapshots/examples_index.yml index 9b601460b1d9132567e92bc9e95a65ae8ec58693..a6668173cc9b2a3f3aa7b19bfe9eae56ae19a95f 100644 --- a/glfm_specification/example_snapshots/examples_index.yml +++ b/glfm_specification/example_snapshots/examples_index.yml @@ -2015,3 +2015,15 @@ 07_01__gitlab_specific_markdown__footnotes__001: spec_txt_example_position: 674 source_specification: gitlab +07_02__gitlab_specific_markdown__task_list_items__001: + spec_txt_example_position: 675 + source_specification: gitlab +07_02__gitlab_specific_markdown__task_list_items__002: + spec_txt_example_position: 676 + source_specification: gitlab +07_02__gitlab_specific_markdown__task_list_items__003: + spec_txt_example_position: 677 + source_specification: gitlab +07_02__gitlab_specific_markdown__task_list_items__004: + spec_txt_example_position: 678 + source_specification: gitlab diff --git a/glfm_specification/example_snapshots/html.yml b/glfm_specification/example_snapshots/html.yml index 376a4bc72ca6c4ab1d46222a79451bb9fc86fa7b..834dd49a93458db61b078256b74fa8bb59fd33c5 100644 --- a/glfm_specification/example_snapshots/html.yml +++ b/glfm_specification/example_snapshots/html.yml @@ -7588,3 +7588,75 @@ wysiwyg: |-

footnote reference tag fortytwo

footnote text

+07_02__gitlab_specific_markdown__task_list_items__001: + canonical: | +
    +
  • + + + incomplete +
  • +
+ static: |- +
    +
  • + incomplete
  • +
+ wysiwyg: |- +
  • incomplete

+07_02__gitlab_specific_markdown__task_list_items__002: + canonical: | +
    +
  • + + + completed +
  • +
+ static: |- +
    +
  • + completed
  • +
+ wysiwyg: |- +
  • completed

+07_02__gitlab_specific_markdown__task_list_items__003: + canonical: | +
    +
  • + + + + inapplicable + +
  • +
+ static: |- +
    +
  • + inapplicable +
  • +
+07_02__gitlab_specific_markdown__task_list_items__004: + canonical: | +
    +
  • +

    + + + + inapplicable + +

    +

    + text in loose list +

    +
  • +
+ static: |- +
    +
  • +

    inapplicable

    +

    text in loose list

    +
  • +
diff --git a/glfm_specification/example_snapshots/markdown.yml b/glfm_specification/example_snapshots/markdown.yml index c4c30dcb5132d64682972f9e94d2d8d1d415b369..ffbc7d77ce5b57f537afab1de4528c46b2d579c0 100644 --- a/glfm_specification/example_snapshots/markdown.yml +++ b/glfm_specification/example_snapshots/markdown.yml @@ -2193,3 +2193,13 @@ footnote reference tag [^fortytwo] [^fortytwo]: footnote text +07_02__gitlab_specific_markdown__task_list_items__001: | + - [ ] incomplete +07_02__gitlab_specific_markdown__task_list_items__002: | + - [x] completed +07_02__gitlab_specific_markdown__task_list_items__003: | + - [~] inapplicable +07_02__gitlab_specific_markdown__task_list_items__004: | + - [~] inapplicable + + text in loose list diff --git a/glfm_specification/example_snapshots/prosemirror_json.yml b/glfm_specification/example_snapshots/prosemirror_json.yml index 0b46894504238cad7be41e849d8f3f670ab10fde..f770d341c426e80b3e0bdf995e684a991eb5ade7 100644 --- a/glfm_specification/example_snapshots/prosemirror_json.yml +++ b/glfm_specification/example_snapshots/prosemirror_json.yml @@ -19244,3 +19244,73 @@ } ] } +07_02__gitlab_specific_markdown__task_list_items__001: |- + { + "type": "doc", + "content": [ + { + "type": "taskList", + "attrs": { + "numeric": false, + "start": 1, + "parens": false + }, + "content": [ + { + "type": "taskItem", + "attrs": { + "checked": false + }, + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "incomplete" + } + ] + } + ] + } + ] + } + ] + } +07_02__gitlab_specific_markdown__task_list_items__002: |- + { + "type": "doc", + "content": [ + { + "type": "taskList", + "attrs": { + "numeric": false, + "start": 1, + "parens": false + }, + "content": [ + { + "type": "taskItem", + "attrs": { + "checked": true + }, + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "completed" + } + ] + } + ] + } + ] + } + ] + } +07_02__gitlab_specific_markdown__task_list_items__003: |- + Inapplicable task list items not yet implemented for WYSYWIG +07_02__gitlab_specific_markdown__task_list_items__004: |- + Inapplicable task list items not yet implemented for WYSYWIG diff --git a/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt b/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt index d0d450b66bf105a21f6685da87b38402ecbfcbd3..a50c28c929668ed13a8d1187af7a29a0e95a4314 100644 --- a/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt +++ b/glfm_specification/input/gitlab_flavored_markdown/glfm_canonical_examples.txt @@ -38,3 +38,85 @@ 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 +. +
    +
  • + + +incomplete +
  • +
+```````````````````````````````` + +Completed task: + +```````````````````````````````` example gitlab tasklist +- [x] completed +. +
    +
  • + + +completed +
  • +
+```````````````````````````````` + +Inapplicable task: + +```````````````````````````````` example gitlab tasklist +- [~] inapplicable +. +
    +
  • + + + +inapplicable + +
  • +
+```````````````````````````````` + +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 +. +
    +
  • + + +incomplete +
  • +
+```````````````````````````````` + +Completed task: + +```````````````````````````````` example gitlab tasklist +- [x] completed +. +
    +
  • + + +completed +
  • +
+```````````````````````````````` + +Inapplicable task: + +```````````````````````````````` example gitlab tasklist +- [~] inapplicable +. +
    +
  • + + + +inapplicable + +
  • +
+```````````````````````````````` + +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
    1. 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