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