Parse task checkboxes in Markdown, part Ⅱ: The Checkboxening
What does this MR do and why?
Follow-up to Parse task checkboxes in Markdown, part Ⅰ: The ... (!219036 - merged).
That MR made no user-facing changes while putting into place the infrastructure required to solve Parse checkboxes in Markdown tables (#21506 - closed). This MR makes all the user facing changes.
Overview
Parsing task checkboxes in Markdown tables, like most Markdown changes, needs to touch a lot of places:
- Done: The Markdown parser needs to know how to parse them (https://github.com/kivikakk/comrak/pull/622, https://github.com/kivikakk/comrak/pull/705)
- Done: The GLFM renderer needs to know how to render them (gitlab-org/ruby/gems/gitlab-glfm-markdown!123 (merged), gitlab-org/ruby/gems/gitlab-glfm-markdown!128 (merged), gitlab-org/ruby/gems/gitlab-glfm-markdown!131 (merged))
- The sanitizer needs to know to keep the newly rendered elements
- The task list Banzai filter need to know what to do with these elements
-
Done: Multiple backend components need to be able to parse task list items out of source Markdown via the
Taskableconcern (for "1 of 2 checklist items completed" and system notes) -
Done: Multiple frontend components need to know how to translate checking or unchecking a rendered checkbox into a call to the backend to update the item
- Done: Every frontend except work items actually has the backend do the source-level change to minimise accidental reverts/overwrites, meaning the backend needs to know how to update Markdown and HTML given the source position of a changed checkbox
- Done: Work items does the source-level change on the frontend, meaning the frontend also needs to know how to update Markdown given the source position of a changed checkbox
- The rich text editor needs to be able to parse the rendered items, needs its editable node views to co-operate with them, needs to know how to serialise them back into Markdown, and needs to know when the nodes in the document require using HTML instead
The previous MR took care of all the rendering/parsing infrastructure challenges required to make this feature possible, marked above as Done. This one actually adds the feature.
The parser recognises a task item in a table only when it's the sole content of a table cell. In Markdown, it looks like this:
| Complete | Action item | Links and notes |
| -------- | ----------------------- | ------------------------- |
| [x] | Refactor the backend | #12345, !7000+ |
| [x] | Refactor the frontend | #555555, !8484+ |
| [ ] | Implement feature | todo |
| [~] | remove every other node | Should probably keep them |
These task items are parsed into <input type="checkbox"> in a similar manner to task list items.
In the rich-text editor, we leave the schema as-is — we parse a table task item rendered from the backend into a task list with a single item with no text. This way, the existing UI for adding/removing task items and otherwise modifying the table contents continues to work in exactly the same way.
If a table cell only contains a single task item without text, it'll be serialised out using the new syntax above. If the task item has extra content, or there are multiple, the table structure will be serialised as HTML, the same as when you put multiple block items in a table cell (since there's no way to represent those cells with GFM table syntax).
Task items can still be changed "live" from the rendered view, and editing these tables works from both the PTE and RTE works as usual. Likewise, if you remove the elements from the table that were preventing it from being serialised as Markdown, it will automatically convert back to Markdown. (This feature is what we have currently — the new syntax just supports it seamlessly.)
What's included in this MR?
We do the following:
-
backend + frontend
- Change existing behaviour to not add
aria-labelto checkboxes if we can't determine any relevant text, as the added label in that case is confusing — worse than nothing.
- Change existing behaviour to not add
-
backend
- Enable the
tasklist_in_tableoption in our backend Markdown parser. - Permit the
task-table-itemclass on<th>and<td>in our sanitiser, and extend the existing permission of<input type="checkbox" class="task-list-item-checkbox">from "when<li>child only" to "when<li>,<td>or<th>child". - Extend
TaskListFilter'saria-labeladdition to handle the new task table items.- Task list items use the text following the task item checkbox to denote the meaning of the checkbox when constructing its
aria-label. Task table items by definition have no such text, and horizontally adjacent cells are unlikely to uniquely identify the role of the checkbox. Instead, we use the column header text, as that's likely to contain relevant context for the meaning of the checkbox, and it's relatively easy for screenreader users to navigate to adjacent cells if necessary for more information.
- Task list items use the text following the task item checkbox to denote the meaning of the checkbox when constructing its
- Add
task_table_item?toTaskable::Item, the class used by the backend to transiently represent task items. - Adjust the
textfor a task item — used to generate system notes when task items are checked or unchecked — to handle task table items, by concatenating non-task table item cells of the row it was checked in. - Fix a bug in
TaskListToggleServicewhere a byte index was used as a character index. - Handle a tricky edge case introduced by task table items where we need to adjust the precise sourcepos of an item and those following it when it originally contained a Unicode whitespace character.
- Enable the
-
frontend
- Extend
TaskItemto also parse itself when located within a<td class="task-table-item">or<th class="task-table-item">.- The schema causes a parent task list to automatically be created for it within the table cell.
- Update the
aria-labelrendering to support task table items per the backend logic.
- Clean up the table serialiser logic which determines whether a given table can be rendered as Markdown (or must be rendered as HTML), and adapt for the addition of the task table item.
- Teach the table row serialiser how to serialise a cell with a task table item.
- Adjust the task item serialiser to not call
renderContentif it's empty — it causes extra newlines to be rendered (since it thinks it's starting a new block item), which in turn breaks the table parse completely. - Extend the existing a11y helper (used when viewing Markdown content) for task list items without a backend-provided
aria-labelto also work forol. We don't need to support task table items here, as they have backend-providedaria-labelsince their inception, i.e. this MR! - Omit rendering an attribute in our tag renderer if the value is
=== false, rather than renderingkey="false". This was causing some issues when something expected a key to be omitted rather than set to the stringfalse. - Modify the pop-up text when a Markdown table is now being serialised as an HTML one to refer to task lists with text or multiple items as a possible cause.
- Extend
| Non-spec | Spec | |
|---|---|---|
| backend | +211 -29 | +354 -15 |
| frontend | +180 -53 | +237 -4 |
Screenshots or screen recordings
Creating a table item with task items in the plain-text editor:
Checking and unchecking task table items live:
Editing, creating, and deleting task table items from the rich-text editor:
Adding multiple task items (not supported by the new syntax), saving and using that, then paring it back down:
How to set up and validate locally
Check out the branch, restart your GDK if you're feeling superstitious, and then give it a try in an issue/work item/MR/comment/any good bookstore where tables and task items are sold!
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

