@@ -17,43 +17,17 @@ import {phrasing} from 'hast-util-phrasing'
1717 * mdast node.
1818 */
1919export 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+ }
0 commit comments