• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

marekdedic / prosemirror-remark / 14286065237

05 Apr 2025 09:44PM UTC coverage: 86.92% (+5.3%) from 81.604%
14286065237

push

github

web-flow
Merge pull request #509 from marekdedic/vitest

Switched to vitest

197 of 211 branches covered (93.36%)

1442 of 1659 relevant lines covered (86.92%)

9.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.34
/src/syntax-extensions/TaskListItemExtension.ts
1
import type { BlockContent, DefinitionContent, ListItem } from "mdast";
2
import type {
3
  DOMOutputSpec,
4
  NodeSpec,
5
  Node as ProseMirrorNode,
6
  Schema,
7
} from "prosemirror-model";
8
import type { Command, EditorState } from "prosemirror-state";
9
import type {
10
  EditorView,
11
  NodeView,
12
  NodeViewConstructor,
13
} from "prosemirror-view";
14
import type { Processor } from "unified";
15
import type { Node as UnistNode } from "unist";
16

17
import {
1✔
18
  gfmTaskListItemFromMarkdown,
19
  gfmTaskListItemToMarkdown,
20
} from "mdast-util-gfm-task-list-item";
21
import { gfmTaskListItem } from "micromark-extension-gfm-task-list-item";
1✔
22
import { InputRule } from "prosemirror-inputrules";
1✔
23
import { createProseMirrorNode, NodeExtension } from "prosemirror-unified";
1✔
24

25
import { buildUnifiedExtension } from "../utils/buildUnifiedExtension";
1✔
26

27
class TaskListItemView implements NodeView {
1✔
28
  public readonly contentDOM: HTMLElement;
29
  public readonly dom: HTMLElement;
30

31
  public constructor(
1✔
32
    node: ProseMirrorNode,
×
33
    view: EditorView,
×
34
    getPos: () => number | undefined,
×
35
  ) {
×
36
    const checkbox = document.createElement("input");
×
37
    checkbox.setAttribute("type", "checkbox");
×
38
    checkbox.setAttribute("style", "cursor: pointer;");
×
39
    if (node.attrs["checked"] === true) {
×
40
      checkbox.setAttribute("checked", "checked");
×
41
    }
×
42
    checkbox.addEventListener("click", (e) => {
×
43
      const pos = getPos();
×
44
      if (pos === undefined) {
×
45
        return;
×
46
      }
×
47
      e.preventDefault();
×
48
      view.dispatch(
×
49
        view.state.tr.setNodeAttribute(
×
50
          pos,
×
51
          "checked",
×
52
          !(node.attrs["checked"] as boolean),
×
53
        ),
×
54
      );
×
55
    });
×
56

57
    const checkboxContainer = document.createElement("span");
×
58
    checkboxContainer.setAttribute("contenteditable", "false");
×
59
    checkboxContainer.setAttribute("style", "position: absolute; left: 5px;");
×
60
    checkboxContainer.appendChild(checkbox);
×
61

62
    this.contentDOM = document.createElement("span");
×
63
    this.contentDOM.setAttribute("style", "position: relative; left: 30px;");
×
64

65
    this.dom = document.createElement("li");
×
66
    this.dom.setAttribute(
×
67
      "style",
×
68
      "list-style-type: none; margin-left: -30px;",
×
69
    );
×
70
    this.dom.appendChild(checkboxContainer);
×
71
    this.dom.appendChild(this.contentDOM);
×
72
  }
×
73

74
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Inherited from the NodeView interface
75
  public stopEvent(): boolean {
1✔
76
    return true;
×
77
  }
×
78
}
1✔
79

80
/**
81
 * @public
82
 */
83
export class TaskListItemExtension extends NodeExtension<ListItem> {
1✔
84
  private static isAtStart(
1✔
85
    state: EditorState,
×
86
    view: EditorView | undefined,
×
87
  ): boolean {
×
88
    if (!state.selection.empty) {
×
89
      return false;
×
90
    }
×
91
    if (view !== undefined) {
×
92
      return view.endOfTextblock("backward", state);
×
93
    }
×
94
    return state.selection.$anchor.parentOffset > 0;
×
95
  }
×
96

97
  public override proseMirrorInputRules(
1✔
98
    proseMirrorSchema: Schema<string, string>,
3✔
99
  ): Array<InputRule> {
3✔
100
    return [
3✔
101
      new InputRule(/^\[([x\s]?)\][\s\S]$/u, (state, match, start) => {
3✔
102
        const wrappingNode = state.doc.resolve(start).node(-1);
×
103
        if (wrappingNode.type.name !== "regular_list_item") {
×
104
          return null;
×
105
        }
×
106
        return state.tr.replaceRangeWith(
×
107
          start - 2,
×
108
          start + wrappingNode.nodeSize,
×
109
          proseMirrorSchema.nodes[this.proseMirrorNodeName()].create(
×
110
            { checked: match[1] === "x" },
×
111
            wrappingNode.content.cut(3 + match[1].length),
×
112
          ),
×
113
        );
×
114
      }),
3✔
115
    ];
3✔
116
  }
3✔
117

118
  public override proseMirrorKeymap(
1✔
119
    proseMirrorSchema: Schema<string, string>,
3✔
120
  ): Record<string, Command> {
3✔
121
    return {
3✔
122
      Backspace: (state, dispatch, view): boolean => {
3✔
123
        if (!TaskListItemExtension.isAtStart(state, view)) {
×
124
          return false;
×
125
        }
×
126
        const taskListItemNode = state.selection.$anchor.node(-1);
×
127
        if (taskListItemNode.type.name !== "task_list_item") {
×
128
          return false;
×
129
        }
×
130
        if (dispatch === undefined) {
×
131
          return true;
×
132
        }
×
133
        dispatch(
×
134
          state.tr.replaceRangeWith(
×
135
            state.selection.$from.before() - 2,
×
136
            state.selection.$from.before() + taskListItemNode.nodeSize,
×
137
            proseMirrorSchema.nodes["regular_list_item"].create(
×
138
              {},
×
139
              taskListItemNode.content,
×
140
            ),
×
141
          ),
×
142
        );
×
143
        return true;
×
144
      },
×
145
    };
3✔
146
  }
3✔
147

148
  public override proseMirrorNodeName(): string {
1✔
149
    return "task_list_item";
85✔
150
  }
85✔
151

152
  public override proseMirrorNodeSpec(): NodeSpec {
1✔
153
    return {
3✔
154
      attrs: { checked: { default: false } },
3✔
155
      content: "paragraph block*",
3✔
156
      defining: true,
3✔
157
      group: "list_item",
3✔
158
      parseDOM: [
3✔
159
        {
3✔
160
          getAttrs(dom: Node | string): false | { checked: boolean } {
3✔
161
            const checkbox = (dom as HTMLElement).firstChild;
×
162
            if (!(checkbox instanceof HTMLInputElement)) {
×
163
              return false;
×
164
            }
×
165
            return { checked: checkbox.checked };
×
166
          },
×
167
          tag: "li",
3✔
168
        },
3✔
169
      ],
3✔
170
      toDOM(node: ProseMirrorNode): DOMOutputSpec {
3✔
171
        return [
2✔
172
          "li",
2✔
173
          { style: "list-style-type: none;, margin-left: -30px;" },
2✔
174
          [
2✔
175
            "span",
2✔
176
            {
2✔
177
              contenteditable: "false",
2✔
178
              style: "position: absolute; left: 5px;",
2✔
179
            },
2✔
180
            [
2✔
181
              "input",
2✔
182
              {
2✔
183
                checked: (node.attrs["checked"] as boolean)
2!
184
                  ? "checked"
×
185
                  : undefined,
2✔
186
                disabled: "disabled",
2✔
187
                type: "checkbox",
2✔
188
              },
2✔
189
            ],
2✔
190
          ],
2✔
191
          ["span", { style: "position: relative; left: 30px" }, 0],
2✔
192
        ];
2✔
193
      },
2✔
194
    };
3✔
195
  }
3✔
196

197
  public override proseMirrorNodeToUnistNodes(
1✔
198
    node: ProseMirrorNode,
8✔
199
    convertedChildren: Array<BlockContent | DefinitionContent>,
8✔
200
  ): Array<ListItem> {
8✔
201
    return [
8✔
202
      {
8✔
203
        checked: node.attrs["checked"] as boolean,
8✔
204
        children: convertedChildren,
8✔
205
        type: this.unistNodeName(),
8✔
206
      },
8✔
207
    ];
8✔
208
  }
8✔
209

210
  public override proseMirrorNodeView(): NodeViewConstructor | null {
1✔
211
    return (node, view, getPos) => new TaskListItemView(node, view, getPos);
3✔
212
  }
3✔
213

214
  public override unifiedInitializationHook(
1✔
215
    processor: Processor<UnistNode, UnistNode, UnistNode, UnistNode, string>,
3✔
216
  ): Processor<UnistNode, UnistNode, UnistNode, UnistNode, string> {
3✔
217
    return processor.use(
3✔
218
      buildUnifiedExtension(
3✔
219
        [gfmTaskListItem()],
3✔
220
        [gfmTaskListItemFromMarkdown()],
3✔
221
        [gfmTaskListItemToMarkdown()],
3✔
222
      ),
3✔
223
    );
3✔
224
  }
3✔
225

226
  public override unistNodeName(): "listItem" {
1✔
227
    return "listItem";
28✔
228
  }
28✔
229

230
  public override unistNodeToProseMirrorNodes(
1✔
231
    node: ListItem,
5✔
232
    proseMirrorSchema: Schema<string, string>,
5✔
233
    convertedChildren: Array<ProseMirrorNode>,
5✔
234
  ): Array<ProseMirrorNode> {
5✔
235
    return createProseMirrorNode(
5✔
236
      this.proseMirrorNodeName(),
5✔
237
      proseMirrorSchema,
5✔
238
      convertedChildren,
5✔
239
      { checked: node.checked },
5✔
240
    );
5✔
241
  }
5✔
242

243
  public override unistToProseMirrorTest(node: UnistNode): boolean {
1✔
244
    return (
20✔
245
      node.type === this.unistNodeName() &&
20✔
246
      "checked" in node &&
5✔
247
      typeof node.checked === "boolean"
5✔
248
    );
249
  }
20✔
250
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc