@@ -17,43 +17,17 @@ import {phrasing} from 'hast-util-phrasing'
17
17
* mdast node.
18
18
*/
19
19
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 )
48
24
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 ) )
53
27
54
28
/** @type {ListItem } */
55
29
const result = { type : 'listItem' , spread, checked, children}
56
- state . patch ( clone , result )
30
+ state . patch ( cleanNode , result )
57
31
return result
58
32
}
59
33
@@ -99,3 +73,61 @@ function spreadout(node) {
99
73
100
74
return false
101
75
}
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