a tool for shared writing and social publishing
1import {
2 InputRule,
3 inputRules,
4 wrappingInputRule,
5} from "prosemirror-inputrules";
6import { MutableRefObject } from "react";
7import { Replicache } from "replicache";
8import type { ReplicacheMutators } from "src/replicache";
9import { BlockProps } from "../Block";
10import { focusBlock } from "src/utils/focusBlock";
11import { schema } from "./schema";
12import { useUIState } from "src/useUIState";
13import { flushSync } from "react-dom";
14export const inputrules = (
15 propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>,
16 repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>,
17) =>
18 inputRules({
19 //Strikethrough
20 rules: [
21 new InputRule(/\~\~([^*]+)\~\~$/, (state, match, start, end) => {
22 const [fullMatch, content] = match;
23 const { tr } = state;
24 if (content) {
25 tr.replaceWith(start, end, state.schema.text(content))
26 .addMark(
27 start,
28 start + content.length,
29 schema.marks.strikethrough.create(),
30 )
31 .removeStoredMark(schema.marks.strikethrough);
32 return tr;
33 }
34 return null;
35 }),
36
37 //Highlight
38 new InputRule(/\=\=([^*]+)\=\=$/, (state, match, start, end) => {
39 const [fullMatch, content] = match;
40 const { tr } = state;
41 if (content) {
42 tr.replaceWith(start, end, state.schema.text(content))
43 .addMark(
44 start,
45 start + content.length,
46 schema.marks.highlight.create({
47 color: useUIState.getState().lastUsedHighlight || "1",
48 }),
49 )
50 .removeStoredMark(schema.marks.highlight);
51 return tr;
52 }
53 return null;
54 }),
55
56 //Bold
57 new InputRule(/\*\*([^*]+)\*\*$/, (state, match, start, end) => {
58 const [fullMatch, content] = match;
59 const { tr } = state;
60 if (content) {
61 tr.replaceWith(start, end, state.schema.text(content))
62 .addMark(
63 start,
64 start + content.length,
65 schema.marks.strong.create(),
66 )
67 .removeStoredMark(schema.marks.strong);
68 return tr;
69 }
70 return null;
71 }),
72
73 //Code
74 new InputRule(/\`([^`]+)\`$/, (state, match, start, end) => {
75 const [fullMatch, content] = match;
76 const { tr } = state;
77 if (content) {
78 const startIndex = start + fullMatch.indexOf("`");
79 tr.replaceWith(startIndex, end, state.schema.text(content))
80 .addMark(
81 startIndex,
82 startIndex + content.length,
83 schema.marks.code.create(),
84 )
85 .removeStoredMark(schema.marks.code);
86 return tr;
87 }
88 return null;
89 }),
90
91 //Italic
92 new InputRule(/(?:^|[^*])\*([^*]+)\*$/, (state, match, start, end) => {
93 const [fullMatch, content] = match;
94 const { tr } = state;
95 if (content) {
96 const startIndex = start + fullMatch.indexOf("*");
97 tr.replaceWith(startIndex, end, state.schema.text(content))
98 .addMark(
99 startIndex,
100 startIndex + content.length,
101 schema.marks.em.create(),
102 )
103 .removeStoredMark(schema.marks.em);
104 return tr;
105 }
106 return null;
107 }),
108
109 // Code Block
110 new InputRule(/^```\s$/, (state, match) => {
111 flushSync(() =>
112 repRef.current?.mutate.assertFact({
113 entity: propsRef.current.entityID,
114 attribute: "block/type",
115 data: { type: "block-type-union", value: "code" },
116 }),
117 );
118 setTimeout(() => {
119 focusBlock({ ...propsRef.current, type: "code" }, { type: "start" });
120 }, 20);
121 return null;
122 }),
123
124 //Checklist
125 new InputRule(/^\-?\[(\ |x)?\]\s$/, (state, match) => {
126 if (!propsRef.current.listData)
127 repRef.current?.mutate.assertFact({
128 entity: propsRef.current.entityID,
129 attribute: "block/is-list",
130 data: { type: "boolean", value: true },
131 });
132 let tr = state.tr;
133 tr.delete(0, match[0].length);
134 repRef.current?.mutate.assertFact({
135 entity: propsRef.current.entityID,
136 attribute: "block/check-list",
137 data: { type: "boolean", value: match[1] === "x" ? true : false },
138 });
139 return tr;
140 }),
141
142 // Unordered List
143 new InputRule(/^([-+*])\s$/, (state) => {
144 if (propsRef.current.listData) return null;
145 let tr = state.tr;
146 tr.delete(0, 2);
147 repRef.current?.mutate.assertFact({
148 entity: propsRef.current.entityID,
149 attribute: "block/is-list",
150 data: { type: "boolean", value: true },
151 });
152 return tr;
153 }),
154
155 //Blockquote
156 new InputRule(/^([>]{1})\s$/, (state, match) => {
157 let tr = state.tr;
158 tr.delete(0, 2);
159 repRef.current?.mutate.assertFact({
160 entity: propsRef.current.entityID,
161 attribute: "block/type",
162 data: { type: "block-type-union", value: "blockquote" },
163 });
164 return tr;
165 }),
166
167 //Header
168 new InputRule(/^([#]{1,3})\s$/, (state, match) => {
169 let tr = state.tr;
170 tr.delete(0, match[0].length);
171 let headingLevel = match[1].length;
172 repRef.current?.mutate.assertFact({
173 entity: propsRef.current.entityID,
174 attribute: "block/type",
175 data: { type: "block-type-union", value: "heading" },
176 });
177 repRef.current?.mutate.assertFact({
178 entity: propsRef.current.entityID,
179 attribute: "block/heading-level",
180 data: { type: "number", value: headingLevel },
181 });
182 return tr;
183 }),
184 ],
185 });