• 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

89.81
/src/syntax-extensions/HeadingExtension.ts
1
import type { Heading, PhrasingContent } 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 { EditorView } from "prosemirror-view";
10

11
import { setBlockType } from "prosemirror-commands";
1✔
12
import { type InputRule, textblockTypeInputRule } from "prosemirror-inputrules";
1✔
13
import {
1✔
14
  createProseMirrorNode,
15
  type Extension,
16
  NodeExtension,
17
} from "prosemirror-unified";
18

19
import { ParagraphExtension } from "./ParagraphExtension";
1✔
20
import { TextExtension } from "./TextExtension";
1✔
21

22
/**
23
 * @public
24
 */
25
export class HeadingExtension extends NodeExtension<Heading> {
1✔
26
  private static headingLevelCommandBuilder(
1✔
27
    proseMirrorSchema: Schema<string, string>,
20✔
28
    levelUpdate: -1 | 1,
20✔
29
    onlyAtStart: boolean,
20✔
30
  ): Command {
20✔
31
    return (state, dispatch, view) => {
20✔
32
      if (onlyAtStart && !HeadingExtension.isAtStart(state, view)) {
6!
33
        return false;
×
34
      }
×
35

36
      const { $anchor } = state.selection;
6✔
37
      const headingNode = $anchor.parent;
6✔
38
      if (headingNode.type.name !== "heading") {
6!
39
        return false;
×
40
      }
×
41

42
      const newHeadingLevel =
6✔
43
        (headingNode.attrs["level"] as number) + levelUpdate;
6✔
44

45
      if (newHeadingLevel < 0 || newHeadingLevel > 6) {
6✔
46
        return false;
1✔
47
      }
1✔
48
      if (dispatch === undefined) {
6!
49
        return true;
×
50
      }
✔
51

52
      const headingPosition = $anchor.before($anchor.depth);
5✔
53

54
      if (newHeadingLevel > 0) {
5✔
55
        dispatch(
5✔
56
          state.tr.setNodeMarkup(headingPosition, undefined, {
5✔
57
            level: newHeadingLevel,
5✔
58
          }),
5✔
59
        );
5✔
60
      } else {
6!
61
        dispatch(
×
62
          state.tr.setNodeMarkup(
×
63
            headingPosition,
×
64
            proseMirrorSchema.nodes["paragraph"],
×
65
          ),
×
66
        );
×
67
      }
✔
68
      return true;
5✔
69
    };
6✔
70
  }
20✔
71

72
  private static isAtStart(
1✔
73
    state: EditorState,
6✔
74
    view: EditorView | undefined,
6✔
75
  ): boolean {
6✔
76
    if (!state.selection.empty) {
6!
77
      return false;
×
78
    }
×
79
    if (view !== undefined) {
6✔
80
      return view.endOfTextblock("backward", state);
6✔
81
    }
6!
82
    return state.selection.$anchor.parentOffset > 0;
×
83
  }
6✔
84

85
  public override dependencies(): Array<Extension> {
1✔
86
    return [new ParagraphExtension(), new TextExtension()];
5✔
87
  }
5✔
88

89
  public override proseMirrorInputRules(
1✔
90
    proseMirrorSchema: Schema<string, string>,
5✔
91
  ): Array<InputRule> {
5✔
92
    return [
5✔
93
      textblockTypeInputRule(
5✔
94
        /^\s{0,3}(#{1,6})\s$/u,
5✔
95
        proseMirrorSchema.nodes[this.proseMirrorNodeName()],
5✔
96
        (match) => ({ level: match[1].length }),
5✔
97
      ),
5✔
98
    ];
5✔
99
  }
5✔
100

101
  public override proseMirrorKeymap(
1✔
102
    proseMirrorSchema: Schema<string, string>,
5✔
103
  ): Record<string, Command> {
5✔
104
    const keymap: Record<string, Command> = {
5✔
105
      "#": HeadingExtension.headingLevelCommandBuilder(
5✔
106
        proseMirrorSchema,
5✔
107
        +1,
5✔
108
        true,
5✔
109
      ),
5✔
110
      Backspace: HeadingExtension.headingLevelCommandBuilder(
5✔
111
        proseMirrorSchema,
5✔
112
        -1,
5✔
113
        true,
5✔
114
      ),
5✔
115
      "Shift-Tab": HeadingExtension.headingLevelCommandBuilder(
5✔
116
        proseMirrorSchema,
5✔
117
        -1,
5✔
118
        false,
5✔
119
      ),
5✔
120
      Tab: HeadingExtension.headingLevelCommandBuilder(
5✔
121
        proseMirrorSchema,
5✔
122
        +1,
5✔
123
        false,
5✔
124
      ),
5✔
125
    };
5✔
126

127
    for (let i = 1; i <= 6; i++) {
5✔
128
      keymap[`Shift-Mod-${i.toString()}`] = setBlockType(
30✔
129
        proseMirrorSchema.nodes[this.proseMirrorNodeName()],
30✔
130
        { level: i },
30✔
131
      );
30✔
132
    }
30✔
133
    return keymap;
5✔
134
  }
5✔
135

136
  public override proseMirrorNodeName(): string {
1✔
137
    return "heading";
189✔
138
  }
189✔
139

140
  public override proseMirrorNodeSpec(): NodeSpec {
1✔
141
    return {
5✔
142
      attrs: { level: { default: 1 } },
5✔
143
      content: "text*",
5✔
144
      defining: true,
5✔
145
      group: "block",
5✔
146
      parseDOM: [
5✔
147
        { attrs: { level: 1 }, tag: "h1" },
5✔
148
        { attrs: { level: 2 }, tag: "h2" },
5✔
149
        { attrs: { level: 3 }, tag: "h3" },
5✔
150
        { attrs: { level: 4 }, tag: "h4" },
5✔
151
        { attrs: { level: 5 }, tag: "h5" },
5✔
152
        { attrs: { level: 6 }, tag: "h6" },
5✔
153
      ],
5✔
154
      toDOM(node: ProseMirrorNode): DOMOutputSpec {
5✔
155
        return [`h${(node.attrs["level"] as number).toString()}`, 0];
20✔
156
      },
20✔
157
    };
5✔
158
  }
5✔
159

160
  public override proseMirrorNodeToUnistNodes(
1✔
161
    node: ProseMirrorNode,
21✔
162
    convertedChildren: Array<PhrasingContent>,
21✔
163
  ): Array<Heading> {
21✔
164
    return [
21✔
165
      {
21✔
166
        children: convertedChildren,
21✔
167
        depth: node.attrs["level"] as 1 | 2 | 3 | 4 | 5 | 6,
21✔
168
        type: this.unistNodeName(),
21✔
169
      },
21✔
170
    ];
21✔
171
  }
21✔
172

173
  public override unistNodeName(): "heading" {
1✔
174
    return "heading";
61✔
175
  }
61✔
176

177
  public override unistNodeToProseMirrorNodes(
1✔
178
    node: Heading,
5✔
179
    proseMirrorSchema: Schema<string, string>,
5✔
180
    convertedChildren: Array<ProseMirrorNode>,
5✔
181
  ): Array<ProseMirrorNode> {
5✔
182
    return createProseMirrorNode(
5✔
183
      this.proseMirrorNodeName(),
5✔
184
      proseMirrorSchema,
5✔
185
      convertedChildren,
5✔
186
      {
5✔
187
        level: node.depth,
5✔
188
      },
5✔
189
    );
5✔
190
  }
5✔
191
}
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