Skip to content

Commit 183cbf9

Browse files
authored
Add support for task list checkboxes outside p
Closes GH-80. Closes GH-81. Reviewed-by: Titus Wormer <[email protected]>
1 parent 7be4e81 commit 183cbf9

File tree

5 files changed

+101
-33
lines changed

5 files changed

+101
-33
lines changed

lib/handlers/li.js

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,43 +17,17 @@ import {phrasing} from 'hast-util-phrasing'
1717
* mdast node.
1818
*/
1919
export function li(state, node) {
20-
const head = node.children[0]
21-
/** @type {boolean | null} */
22-
let checked = null
23-
/** @type {Element | undefined} */
24-
let clone
25-
26-
// Check if this node starts with a checkbox.
27-
if (head && head.type === 'element' && head.tagName === 'p') {
28-
const checkbox = head.children[0]
29-
30-
if (
31-
checkbox &&
32-
checkbox.type === 'element' &&
33-
checkbox.tagName === 'input' &&
34-
checkbox.properties &&
35-
(checkbox.properties.type === 'checkbox' ||
36-
checkbox.properties.type === 'radio')
37-
) {
38-
checked = Boolean(checkbox.properties.checked)
39-
clone = {
40-
...node,
41-
children: [
42-
{...head, children: head.children.slice(1)},
43-
...node.children.slice(1)
44-
]
45-
}
46-
}
47-
}
20+
// If the list item starts with a checkbox, remove the checkbox and mark the
21+
// list item as a GFM task list item.
22+
const {cleanNode, checkbox} = extractLeadingCheckbox(node)
23+
const checked = checkbox && Boolean(checkbox.properties.checked)
4824

49-
if (!clone) clone = node
50-
51-
const spread = spreadout(clone)
52-
const children = state.toFlow(state.all(clone))
25+
const spread = spreadout(cleanNode)
26+
const children = state.toFlow(state.all(cleanNode))
5327

5428
/** @type {ListItem} */
5529
const result = {type: 'listItem', spread, checked, children}
56-
state.patch(clone, result)
30+
state.patch(cleanNode, result)
5731
return result
5832
}
5933

@@ -99,3 +73,61 @@ function spreadout(node) {
9973

10074
return false
10175
}
76+
77+
/**
78+
* If the first bit of content in an element is a checkbox, create a copy of
79+
* the element that does not include the checkbox and return the cleaned up
80+
* copy alongside the checkbox that was removed. If there was no leading
81+
* checkbox, this returns the original element unaltered (not a copy).
82+
*
83+
* This detects trees like:
84+
* `<li><input type="checkbox">Text</li>`
85+
* And returns a tree like:
86+
* `<li>Text</li>`
87+
*
88+
* Or with nesting:
89+
* `<li><p><input type="checkbox">Text</p></li>`
90+
* Which returns a tree like:
91+
* `<li><p>Text</p></li>`
92+
*
93+
* @param {Readonly<Element>} node
94+
* @returns {{cleanNode: Element, checkbox: Element | null}}
95+
*/
96+
function extractLeadingCheckbox(node) {
97+
const head = node.children[0]
98+
99+
if (
100+
head &&
101+
head.type === 'element' &&
102+
head.tagName === 'input' &&
103+
head.properties &&
104+
(head.properties.type === 'checkbox' || head.properties.type === 'radio')
105+
) {
106+
return {
107+
cleanNode: {...node, children: node.children.slice(1)},
108+
checkbox: head
109+
}
110+
}
111+
112+
// The checkbox may be nested in another element. If the first element has
113+
// children, look for a leading checkbox inside it.
114+
//
115+
// NOTE: this only handles nesting in `<p>` elements, which is most common.
116+
// It's possible a leading checkbox might be nested in other types of flow or
117+
// phrasing elements (and *deeply* nested, which is not possible with `<p>`).
118+
// Limiting things to `<p>` elements keeps this simpler for now.
119+
if (head && head.type === 'element' && head.tagName === 'p') {
120+
const {cleanNode: cleanHead, checkbox} = extractLeadingCheckbox(head)
121+
if (checkbox) {
122+
return {
123+
cleanNode: {
124+
...node,
125+
children: [cleanHead, ...node.children.slice(1)]
126+
},
127+
checkbox
128+
}
129+
}
130+
}
131+
132+
return {cleanNode: node, checkbox: null}
133+
}

test/fixtures/ol/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,14 @@
5959
<li><p><input type="checkbox">Echo</p></li>
6060
<li><p><input type="checkbox"><strong>Foxtrot</strong></p></li>
6161
<li><p><input type="checkbox"> <strong>Golf</strong></p></li>
62+
<li>
63+
<p>
64+
<input type="checkbox"> Hotel
65+
</p>
66+
</li>
67+
<li><input type="checkbox"> India</li>
68+
<li>
69+
<input type="checkbox"> Juliet
70+
</li>
71+
<li><input type="checkbox"></li>
6272
</ol>

test/fixtures/ol/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,11 @@ Quuux.
5757
5. [ ] **Foxtrot**
5858

5959
6. [ ] **Golf**
60+
61+
7. [ ] Hotel
62+
63+
8. [ ] India
64+
65+
9. [ ] Juliet
66+
67+
10.

test/fixtures/ul/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,14 @@
5959
<li><p><input type="checkbox">Echo</p></li>
6060
<li><p><input type="checkbox"><strong>Foxtrot</strong></p></li>
6161
<li><p><input type="checkbox"> <strong>Golf</strong></p></li>
62+
<li>
63+
<p>
64+
<input type="checkbox"> Hotel
65+
</p>
66+
</li>
67+
<li><input type="checkbox"> India</li>
68+
<li>
69+
<input type="checkbox"> Juliet
70+
</li>
71+
<li><input type="checkbox"></li>
6272
</ul>

test/fixtures/ul/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,11 @@ Quuux.
5757
* [ ] **Foxtrot**
5858

5959
* [ ] **Golf**
60+
61+
* [ ] Hotel
62+
63+
* [ ] India
64+
65+
* [ ] Juliet
66+
67+
*

0 commit comments

Comments
 (0)