From 27ff5748040a29ce67a332ce3ff462c338185310 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Wed, 10 Dec 2025 14:48:40 +1100 Subject: [PATCH 1/4] Update to gitlab-glfm-markdown 0.0.40 --- Gemfile | 2 +- Gemfile.checksum | 14 +++++++------- Gemfile.lock | 4 ++-- Gemfile.next.checksum | 14 +++++++------- Gemfile.next.lock | 4 ++-- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Gemfile b/Gemfile index 957461471f0062..8e1302042e35f0 100644 --- a/Gemfile +++ b/Gemfile @@ -268,7 +268,7 @@ gem 'asciidoctor-kroki', '~> 0.10.0', require: false, feature_category: :markdow gem 'rouge', '~> 4.6.1', feature_category: :shared # rubocop:todo Gemfile/MissingFeatureCategory -- https://gitlab.com/gitlab-org/gitlab/-/issues/581839 gem 'truncato', '~> 0.7.13', feature_category: :team_planning gem 'nokogiri', '~> 1.18', feature_category: :shared # rubocop:todo Gemfile/MissingFeatureCategory -- https://gitlab.com/gitlab-org/gitlab/-/issues/581839 -gem 'gitlab-glfm-markdown', '~> 0.0.39', feature_category: :markdown +gem 'gitlab-glfm-markdown', '~> 0.0.40', feature_category: :markdown gem 'tanuki_emoji', '~> 0.13', feature_category: :markdown gem 'unicode-emoji', '~> 4.0', feature_category: :markdown diff --git a/Gemfile.checksum b/Gemfile.checksum index 0d8374aa1c25f2..722f3eaa855530 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -226,13 +226,13 @@ {"name":"gitlab-dangerfiles","version":"4.10.0","platform":"ruby","checksum":"0adb9cfec58ffce42f68b1aef528503bdc89aed3994ba461c67e1d9246513e1c"}, {"name":"gitlab-experiment","version":"1.0.0","platform":"ruby","checksum":"9ff76fe55d30012f0531be97143f3af5f74dd1ef3f33d630f320de0ceec9eab9"}, {"name":"gitlab-fog-azure-rm","version":"2.4.0","platform":"ruby","checksum":"678b86e542a37eda10e63ca02d04c9ff998b771df4aabc1f87e5c20148cb360b"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"aarch64-linux-gnu","checksum":"3c589bf98fa04b3ad7760564af0892d53b7cfc7058153a0aab331cf15ffbf06f"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"aarch64-linux-musl","checksum":"c3f1ccd9f958b8be0dfeb4f3cb58a185afdc602318748509f009da58fdd49a51"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"arm64-darwin","checksum":"efddd0a07a1b26a1b9af61dc4d9246b9599877a1221a9e0ceb2a5e69ab664cc2"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"ruby","checksum":"a7d7d237203ebe290c2519b742c2c536ee94401a038ad63f043db9f4166fa0c7"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"x86_64-darwin","checksum":"9f326f6e6771e4fad240bf9bfb2acd85eb86eb44ef058c58d2ba81bc2e0075d5"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"x86_64-linux-gnu","checksum":"90a12179eb5a9d311a840dccbab7f6d61c52fef5e526586bcfc70154016af683"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"x86_64-linux-musl","checksum":"f353114bf503e3389159e22c8af1bd12fea7016081bf60ee05b269b80793c101"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"aarch64-linux-gnu","checksum":"32e4f2c2f8e49ad815a15706314abc63a1f0642172e72182d81a148765366312"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"aarch64-linux-musl","checksum":"b65881fe1f9e5bba13b7e35eb6cd4c145efab7dc2443a02e205bd272ffaa299d"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"arm64-darwin","checksum":"1464682c4b86408144f1443c306b0b69396bcd5b8f4af70ac14c2215d715685b"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"ruby","checksum":"00db6475a2bfd5bcd981270bc16145ebdca09e3da838b6c56f77814d82436cc9"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"x86_64-darwin","checksum":"2b680fff79162171d55ef6039048db03123c617e4b405e5493cc1f1aa8a0ec13"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"x86_64-linux-gnu","checksum":"c1ff008c220416a8b7ab7b6c8a82d832dc6e9a8f2efdcbac91f8b043e9c0e4c0"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"x86_64-linux-musl","checksum":"d714afdc8cfdd306d77dff8ab981def13aefe03f093a8d2ebae742a4a739e6f0"}, {"name":"gitlab-kas-grpc","version":"18.5.0.pre.rc4","platform":"ruby","checksum":"8efe8bc957572bad2ee90c22836b285eb65574591db5c8a7bc8080f69ddebcc6"}, {"name":"gitlab-labkit","version":"1.0.1","platform":"ruby","checksum":"6f354529ea9a898dc622bfd21ea4e0024dbab0d0d5a6496f0af7f3163013ecb2"}, {"name":"gitlab-license","version":"2.6.0","platform":"ruby","checksum":"2c1f8ae73835640ec77bf758c1d0c9730635043c01cf77902f7976e826d7d016"}, diff --git a/Gemfile.lock b/Gemfile.lock index 6baaae652810f0..99fa2e804fc43a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -763,7 +763,7 @@ GEM mime-types net-http-persistent (~> 4.0) nokogiri (~> 1, >= 1.10.8) - gitlab-glfm-markdown (0.0.39) + gitlab-glfm-markdown (0.0.40) rb_sys (~> 0.9.109) gitlab-kas-grpc (18.5.0.pre.rc4) grpc (~> 1.0) @@ -2191,7 +2191,7 @@ DEPENDENCIES gitlab-duo-workflow-service-client (~> 0.6)! gitlab-experiment (~> 1.0.0) gitlab-fog-azure-rm (~> 2.4.0) - gitlab-glfm-markdown (~> 0.0.39) + gitlab-glfm-markdown (~> 0.0.40) gitlab-grape-openapi! gitlab-housekeeper! gitlab-http! diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum index 0d8374aa1c25f2..722f3eaa855530 100644 --- a/Gemfile.next.checksum +++ b/Gemfile.next.checksum @@ -226,13 +226,13 @@ {"name":"gitlab-dangerfiles","version":"4.10.0","platform":"ruby","checksum":"0adb9cfec58ffce42f68b1aef528503bdc89aed3994ba461c67e1d9246513e1c"}, {"name":"gitlab-experiment","version":"1.0.0","platform":"ruby","checksum":"9ff76fe55d30012f0531be97143f3af5f74dd1ef3f33d630f320de0ceec9eab9"}, {"name":"gitlab-fog-azure-rm","version":"2.4.0","platform":"ruby","checksum":"678b86e542a37eda10e63ca02d04c9ff998b771df4aabc1f87e5c20148cb360b"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"aarch64-linux-gnu","checksum":"3c589bf98fa04b3ad7760564af0892d53b7cfc7058153a0aab331cf15ffbf06f"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"aarch64-linux-musl","checksum":"c3f1ccd9f958b8be0dfeb4f3cb58a185afdc602318748509f009da58fdd49a51"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"arm64-darwin","checksum":"efddd0a07a1b26a1b9af61dc4d9246b9599877a1221a9e0ceb2a5e69ab664cc2"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"ruby","checksum":"a7d7d237203ebe290c2519b742c2c536ee94401a038ad63f043db9f4166fa0c7"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"x86_64-darwin","checksum":"9f326f6e6771e4fad240bf9bfb2acd85eb86eb44ef058c58d2ba81bc2e0075d5"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"x86_64-linux-gnu","checksum":"90a12179eb5a9d311a840dccbab7f6d61c52fef5e526586bcfc70154016af683"}, -{"name":"gitlab-glfm-markdown","version":"0.0.39","platform":"x86_64-linux-musl","checksum":"f353114bf503e3389159e22c8af1bd12fea7016081bf60ee05b269b80793c101"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"aarch64-linux-gnu","checksum":"32e4f2c2f8e49ad815a15706314abc63a1f0642172e72182d81a148765366312"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"aarch64-linux-musl","checksum":"b65881fe1f9e5bba13b7e35eb6cd4c145efab7dc2443a02e205bd272ffaa299d"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"arm64-darwin","checksum":"1464682c4b86408144f1443c306b0b69396bcd5b8f4af70ac14c2215d715685b"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"ruby","checksum":"00db6475a2bfd5bcd981270bc16145ebdca09e3da838b6c56f77814d82436cc9"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"x86_64-darwin","checksum":"2b680fff79162171d55ef6039048db03123c617e4b405e5493cc1f1aa8a0ec13"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"x86_64-linux-gnu","checksum":"c1ff008c220416a8b7ab7b6c8a82d832dc6e9a8f2efdcbac91f8b043e9c0e4c0"}, +{"name":"gitlab-glfm-markdown","version":"0.0.40","platform":"x86_64-linux-musl","checksum":"d714afdc8cfdd306d77dff8ab981def13aefe03f093a8d2ebae742a4a739e6f0"}, {"name":"gitlab-kas-grpc","version":"18.5.0.pre.rc4","platform":"ruby","checksum":"8efe8bc957572bad2ee90c22836b285eb65574591db5c8a7bc8080f69ddebcc6"}, {"name":"gitlab-labkit","version":"1.0.1","platform":"ruby","checksum":"6f354529ea9a898dc622bfd21ea4e0024dbab0d0d5a6496f0af7f3163013ecb2"}, {"name":"gitlab-license","version":"2.6.0","platform":"ruby","checksum":"2c1f8ae73835640ec77bf758c1d0c9730635043c01cf77902f7976e826d7d016"}, diff --git a/Gemfile.next.lock b/Gemfile.next.lock index 6baaae652810f0..99fa2e804fc43a 100644 --- a/Gemfile.next.lock +++ b/Gemfile.next.lock @@ -763,7 +763,7 @@ GEM mime-types net-http-persistent (~> 4.0) nokogiri (~> 1, >= 1.10.8) - gitlab-glfm-markdown (0.0.39) + gitlab-glfm-markdown (0.0.40) rb_sys (~> 0.9.109) gitlab-kas-grpc (18.5.0.pre.rc4) grpc (~> 1.0) @@ -2191,7 +2191,7 @@ DEPENDENCIES gitlab-duo-workflow-service-client (~> 0.6)! gitlab-experiment (~> 1.0.0) gitlab-fog-azure-rm (~> 2.4.0) - gitlab-glfm-markdown (~> 0.0.39) + gitlab-glfm-markdown (~> 0.0.40) gitlab-grape-openapi! gitlab-housekeeper! gitlab-http! -- GitLab From 142f6762e56e9c28af49aa46368f40f9ea89d29e Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Wed, 10 Dec 2025 14:49:53 +1100 Subject: [PATCH 2/4] Enable tasklist_in_table, permit in sanitize --- lib/banzai/filter/markdown_engines/glfm_markdown.rb | 2 +- lib/banzai/filter/sanitization_filter.rb | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/banzai/filter/markdown_engines/glfm_markdown.rb b/lib/banzai/filter/markdown_engines/glfm_markdown.rb index 77a6cfdd01ec67..4e919e967e6991 100644 --- a/lib/banzai/filter/markdown_engines/glfm_markdown.rb +++ b/lib/banzai/filter/markdown_engines/glfm_markdown.rb @@ -39,7 +39,7 @@ class GlfmMarkdown < Base tagfilter: false, tasklist: true, tasklist_classes: true, - tasklist_in_table: false, + tasklist_in_table: true, wikilinks_title_before_pipe: true, unsafe: true }.freeze diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 4d758019a1a91f..cb6452c78c0be5 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -87,6 +87,7 @@ def allow_anchor_data_heading_content(allowlist) def allow_tasklists(allowlist) allowlist[:elements].push('input') allowlist[:attributes]['input'].push('data-inapplicable') + allowlist[:attributes]['input'].push('data-checkbox-sourcepos') allowlist[:transformers].push(self.class.method(:remove_non_tasklist_inputs)) end @@ -191,12 +192,14 @@ def remove_id_attributes(env) node.remove_attribute('id') end + TASK_LIST_ITEM_CHECKBOX_PARENTS = %w[li td th].freeze + def remove_non_tasklist_inputs(env) node = env[:node] return unless node.name == 'input' - return if node['type'] == 'checkbox' && node['class'] == 'task-list-item-checkbox' && node.parent.name == 'li' + return if node['type'] == 'checkbox' && node['class'] == 'task-list-item-checkbox' && TASK_LIST_ITEM_CHECKBOX_PARENTS.include?(node.parent.name) node.remove end -- GitLab From 905c380ebaad8433946b3416c4512499a032e125 Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Wed, 10 Dec 2025 14:49:53 +1100 Subject: [PATCH 3/4] Adjust frontend task item aria-label for new parents; don't add empty --- .../behaviors/markdown/accessibility.js | 22 ++++++++++--------- lib/banzai/filter/task_list_filter.rb | 4 +++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/behaviors/markdown/accessibility.js b/app/assets/javascripts/behaviors/markdown/accessibility.js index 2bf7b28a04a832..1d8887f6e6c0a5 100644 --- a/app/assets/javascripts/behaviors/markdown/accessibility.js +++ b/app/assets/javascripts/behaviors/markdown/accessibility.js @@ -3,19 +3,21 @@ import { truncate } from '~/lib/utils/text_utility'; function addAriaLabels(checkboxes) { checkboxes.forEach((checkbox) => { - // If we already get the ariaLabel from RTE, return + // If we already get the ariaLabel from RTE/backend, return if (!checkbox || checkbox.hasAttribute('aria-label')) return; - const li = checkbox.closest('li').cloneNode(true); - li.querySelector('ul')?.remove(); - const textContent = li?.textContent?.trim(); + const parent = checkbox.closest('li, td, th').cloneNode(true); + parent.querySelector('ul, ol')?.remove(); + const textContent = parent?.textContent?.trim(); - checkbox.setAttribute( - 'aria-label', - sprintf(__('Check option: %{option}'), { - option: truncate(textContent, 100), - }), - ); + if (textContent?.length) { + checkbox.setAttribute( + 'aria-label', + sprintf(__('Check option: %{option}'), { + option: truncate(textContent, 100), + }), + ); + } }); } diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb index 63149b84d135a3..159e4b6b22edae 100644 --- a/lib/banzai/filter/task_list_filter.rb +++ b/lib/banzai/filter/task_list_filter.rb @@ -20,7 +20,9 @@ def call text_content << el.text end truncated_text_content = text_content.strip.truncate(100, separator: ' ', omission: '…') - node['aria-label'] = format(_('Check option: %{option}'), option: truncated_text_content) + if truncated_text_content.present? + node['aria-label'] = format(_('Check option: %{option}'), option: truncated_text_content) + end next unless node.has_attribute?('data-inapplicable') -- GitLab From 0a9c7443db0a8f00f10a751763e782ae7f3303fa Mon Sep 17 00:00:00 2001 From: Asherah Connor Date: Wed, 10 Dec 2025 18:26:58 +1100 Subject: [PATCH 4/4] Add precise checkbox sourcepos support to toggleCheckbox, pass through --- .../javascripts/behaviors/markdown/utils.js | 86 ++++++++++++++----- .../design_preview/design_description.vue | 3 +- .../components/notes/work_item_note_body.vue | 2 +- .../work_item_description_rendered.vue | 3 +- .../frontend/behaviors/markdown/utils_spec.js | 32 +++++++ 5 files changed, 102 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/behaviors/markdown/utils.js b/app/assets/javascripts/behaviors/markdown/utils.js index 0b2034f6181f92..2fe79907f5bd29 100644 --- a/app/assets/javascripts/behaviors/markdown/utils.js +++ b/app/assets/javascripts/behaviors/markdown/utils.js @@ -1,27 +1,71 @@ /** - * This method parses raw markdown text in GFM input field and toggles checkboxes - * based on checkboxChecked property. + * Parse a CommonMark-style sourcepos line-column pair, like '1:5' (first line, 5th column), + * into a JavaScript object with zero-based indices, like `{ line: 0, column: 4 }`. + */ +const parseLineColumn = (lineColumn) => { + const [line, column] = lineColumn.split(':'); + return { line: Number(line) - 1, column: Number(column) - 1 }; +}; + +/** + * Parse a CommonMark-style sourcepos, like '1:5-1:8' (columns 5 through 8 inclusive of the first line) + * into a JavaScript object with zero-based indices, like + * `{ start: { line: 0, column: 4 }, end: { line: 0, column: 7 } }`. + */ +const parseSourcepos = (sourcepos) => { + const [start, end] = sourcepos.split('-'); + return { start: parseLineColumn(start), end: parseLineColumn(end) }; +}; + +/** + * This method does its best to toggle a task item in GLFM based on the given source position, + * settings its state to checkboxChecked. + * + * If the sourcepos range covers multiple characters, it's assumed to come from a list task item, in + * which case we replace the first task item-looking sequence we see in the line. + * + * If it's a single character, it's assumed to target a specific checkbox character (such as comes + * from a table, where there could be multiple in one line; see https://github.com/kivikakk/comrak/pull/622 + * and https://github.com/kivikakk/comrak/pull/705), and we replace the target character exactly, + * as long as it does look like it matches a task item. * * @param {Object} object containing rawMarkdown, sourcepos, checkboxChecked properties * @returns String with toggled checkboxes */ -export const toggleCheckbox = ({ rawMarkdown, sourcepos, checkboxChecked }) => { - // Extract the description text - const [startRange] = sourcepos.split('-'); - let [startRow] = startRange.split(':'); - startRow = Number(startRow) - 1; - - // Mark/Unmark the checkboxes - return rawMarkdown - .split('\n') - .map((row, index) => { - if (startRow === index) { - if (checkboxChecked) { - return row.replace(/\[ \]/, '[x]'); - } - return row.replace(/\[[x~]\]/i, '[ ]'); - } - return row; - }) - .join('\n'); +export const toggleCheckbox = ({ rawMarkdown, sourcepos: rawSourcepos, checkboxChecked }) => { + const sourcepos = parseSourcepos(rawSourcepos); + const lines = rawMarkdown.split('\n'); + + const line = lines[sourcepos.start.line]; + if (!line) return rawMarkdown; + + if (sourcepos.start.column === sourcepos.end.column) { + // Precise sourcepos given; check that a task item does appear to be exactly there and set accordingly. + + // Avoid underflow if given an unrealistic start column (task item symbol can't be at position 0, + // since the '[' character must come before it). + if (sourcepos.start.column === 0) return rawMarkdown; + + const lineBefore = line.substr(0, sourcepos.start.column - 1); + const lineAfter = line.substr(sourcepos.start.column + 2); + + if (checkboxChecked && line.substr(sourcepos.start.column - 1, 3) === '[ ]') { + lines[sourcepos.start.line] = `${lineBefore}[x]${lineAfter}`; + return lines.join('\n'); + } + if (!checkboxChecked && line.substr(sourcepos.start.column - 1, 3).toLowerCase() === '[x]') { + lines[sourcepos.start.line] = `${lineBefore}[ ]${lineAfter}`; + return lines.join('\n'); + } + } else { + // Imprecise sourcepos given; replace first task item-looking thing on the line. + if (checkboxChecked) { + lines[sourcepos.start.line] = line.replace('[ ]', '[x]'); + } else { + lines[sourcepos.start.line] = line.replace(/\[[x~]\]/i, '[ ]'); + } + return lines.join('\n'); + } + + return rawMarkdown; }; diff --git a/app/assets/javascripts/work_items/components/design_management/design_preview/design_description.vue b/app/assets/javascripts/work_items/components/design_management/design_preview/design_description.vue index b0ea5903ce4f0d..497b582cc13f21 100644 --- a/app/assets/javascripts/work_items/components/design_management/design_preview/design_description.vue +++ b/app/assets/javascripts/work_items/components/design_management/design_preview/design_description.vue @@ -186,7 +186,8 @@ export default { if (isCheckbox(target)) { target.disabled = true; - const { sourcepos } = target.parentElement.dataset; + const sourcepos = + target.dataset.checkboxSourcepos ?? target.parentElement.dataset.sourcepos; if (!sourcepos) return; diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue index 8ad2ac25d87b41..79a9c79783ec8d 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue @@ -76,7 +76,7 @@ export default { return; } - const { sourcepos } = target.parentElement.dataset; + const sourcepos = target.dataset.checkboxSourcepos ?? target.parentElement.dataset.sourcepos; if (!sourcepos) { return; diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue index 6663d7e20aa5c0..576ff4ed06fbca 100644 --- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue +++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue @@ -331,7 +331,8 @@ export default { if (isCheckbox(target)) { target.disabled = true; - const { sourcepos } = target.parentElement.dataset; + const sourcepos = + target.dataset.checkboxSourcepos ?? target.parentElement.dataset.sourcepos; if (!sourcepos) return; diff --git a/spec/frontend/behaviors/markdown/utils_spec.js b/spec/frontend/behaviors/markdown/utils_spec.js index 91e636f2a7b5fb..440a44a088c441 100644 --- a/spec/frontend/behaviors/markdown/utils_spec.js +++ b/spec/frontend/behaviors/markdown/utils_spec.js @@ -13,4 +13,36 @@ describe('toggleCheckbox', () => { expect(toggleCheckbox({ rawMarkdown, sourcepos, checkboxChecked })).toEqual(expectedMarkdown); }, ); + + const tableMarkdown = ` +| t | table | t | table | t | table | t | table | +| - | ----- | - | ----- | - | ----- | - | ----- | +| 1 | [ ] | 2 | [x] | 3 | [ ] | 4 | [x] | +`; + + it('marks a table checkbox when precisely located', () => { + expect( + toggleCheckbox({ rawMarkdown: tableMarkdown, sourcepos: '4:9-4:9', checkboxChecked: true }), + ).toEqual(tableMarkdown.replace('1 | [ ]', '1 | [x]')); + expect( + toggleCheckbox({ rawMarkdown: tableMarkdown, sourcepos: '4:33-4:33', checkboxChecked: true }), + ).toEqual(tableMarkdown.replace('3 | [ ]', '3 | [x]')); + }); + + it('unmarks a table checkbox when precisely located', () => { + expect( + toggleCheckbox({ + rawMarkdown: tableMarkdown, + sourcepos: '4:21-4:21', + checkboxChecked: false, + }), + ).toEqual(tableMarkdown.replace('2 | [x]', '2 | [ ]')); + expect( + toggleCheckbox({ + rawMarkdown: tableMarkdown, + sourcepos: '4:45-4:45', + checkboxChecked: false, + }), + ).toEqual(tableMarkdown.replace('4 | [x]', '4 | [ ]')); + }); }); -- GitLab