a tool for shared writing and social publishing
at main 7.3 kB view raw
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 openMentionAutocomplete?: () => void, 19) => 20 inputRules({ 21 //Strikethrough 22 rules: [ 23 new InputRule(/\~\~([^*]+)\~\~$/, (state, match, start, end) => { 24 const [fullMatch, content] = match; 25 const { tr } = state; 26 if (content) { 27 tr.replaceWith(start, end, state.schema.text(content)) 28 .addMark( 29 start, 30 start + content.length, 31 schema.marks.strikethrough.create(), 32 ) 33 .removeStoredMark(schema.marks.strikethrough); 34 return tr; 35 } 36 return null; 37 }), 38 39 //Highlight 40 new InputRule(/\=\=([^*]+)\=\=$/, (state, match, start, end) => { 41 const [fullMatch, content] = match; 42 const { tr } = state; 43 if (content) { 44 tr.replaceWith(start, end, state.schema.text(content)) 45 .addMark( 46 start, 47 start + content.length, 48 schema.marks.highlight.create({ 49 color: useUIState.getState().lastUsedHighlight || "1", 50 }), 51 ) 52 .removeStoredMark(schema.marks.highlight); 53 return tr; 54 } 55 return null; 56 }), 57 58 //Bold 59 new InputRule(/\*\*([^*]+)\*\*$/, (state, match, start, end) => { 60 const [fullMatch, content] = match; 61 const { tr } = state; 62 if (content) { 63 tr.replaceWith(start, end, state.schema.text(content)) 64 .addMark( 65 start, 66 start + content.length, 67 schema.marks.strong.create(), 68 ) 69 .removeStoredMark(schema.marks.strong); 70 return tr; 71 } 72 return null; 73 }), 74 75 //Code 76 new InputRule(/\`([^`]+)\`$/, (state, match, start, end) => { 77 const [fullMatch, content] = match; 78 const { tr } = state; 79 if (content) { 80 const startIndex = start + fullMatch.indexOf("`"); 81 tr.replaceWith(startIndex, end, state.schema.text(content)) 82 .addMark( 83 startIndex, 84 startIndex + content.length, 85 schema.marks.code.create(), 86 ) 87 .removeStoredMark(schema.marks.code); 88 return tr; 89 } 90 return null; 91 }), 92 93 //Italic 94 new InputRule(/(?:^|[^*])\*([^*]+)\*$/, (state, match, start, end) => { 95 const [fullMatch, content] = match; 96 const { tr } = state; 97 if (content) { 98 const startIndex = start + fullMatch.indexOf("*"); 99 tr.replaceWith(startIndex, end, state.schema.text(content)) 100 .addMark( 101 startIndex, 102 startIndex + content.length, 103 schema.marks.em.create(), 104 ) 105 .removeStoredMark(schema.marks.em); 106 return tr; 107 } 108 return null; 109 }), 110 111 // Code Block 112 new InputRule(/^```\s$/, (state, match) => { 113 flushSync(() => { 114 repRef.current?.mutate.assertFact({ 115 entity: propsRef.current.entityID, 116 attribute: "block/type", 117 data: { type: "block-type-union", value: "code" }, 118 }); 119 let lastLang = localStorage.getItem(LAST_USED_CODE_LANGUAGE_KEY); 120 if (lastLang) { 121 repRef.current?.mutate.assertFact({ 122 entity: propsRef.current.entityID, 123 attribute: "block/code-language", 124 data: { type: "string", value: lastLang }, 125 }); 126 } 127 }); 128 setTimeout(() => { 129 focusBlock({ ...propsRef.current, type: "code" }, { type: "start" }); 130 }, 20); 131 return null; 132 }), 133 134 //Checklist 135 new InputRule(/^\-?\[(\ |x)?\]\s$/, (state, match) => { 136 if (!propsRef.current.listData) 137 repRef.current?.mutate.assertFact({ 138 entity: propsRef.current.entityID, 139 attribute: "block/is-list", 140 data: { type: "boolean", value: true }, 141 }); 142 let tr = state.tr; 143 tr.delete(0, match[0].length); 144 repRef.current?.mutate.assertFact({ 145 entity: propsRef.current.entityID, 146 attribute: "block/check-list", 147 data: { type: "boolean", value: match[1] === "x" ? true : false }, 148 }); 149 return tr; 150 }), 151 152 // Unordered List 153 new InputRule(/^([-+*])\s$/, (state) => { 154 if (propsRef.current.listData) return null; 155 let tr = state.tr; 156 tr.delete(0, 2); 157 repRef.current?.mutate.assertFact({ 158 entity: propsRef.current.entityID, 159 attribute: "block/is-list", 160 data: { type: "boolean", value: true }, 161 }); 162 return tr; 163 }), 164 165 //Blockquote 166 new InputRule(/^([>]{1})\s$/, (state, match) => { 167 let tr = state.tr; 168 tr.delete(0, 2); 169 repRef.current?.mutate.assertFact({ 170 entity: propsRef.current.entityID, 171 attribute: "block/type", 172 data: { type: "block-type-union", value: "blockquote" }, 173 }); 174 return tr; 175 }), 176 177 //Header 178 new InputRule(/^([#]{1,3})\s$/, (state, match) => { 179 let tr = state.tr; 180 tr.delete(0, match[0].length); 181 let headingLevel = match[1].length; 182 repRef.current?.mutate.assertFact({ 183 entity: propsRef.current.entityID, 184 attribute: "block/type", 185 data: { type: "block-type-union", value: "heading" }, 186 }); 187 repRef.current?.mutate.assertFact({ 188 entity: propsRef.current.entityID, 189 attribute: "block/heading-level", 190 data: { type: "number", value: headingLevel }, 191 }); 192 return tr; 193 }), 194 195 // Mention - @ at start of line, after space, or after hard break 196 new InputRule(/(?:^|\s)@$/, (state, match, start, end) => { 197 if (!openMentionAutocomplete) return null; 198 // Schedule opening the autocomplete after the transaction is applied 199 setTimeout(() => openMentionAutocomplete(), 0); 200 return null; // Let the @ be inserted normally 201 }), 202 // Mention - @ immediately after a hard break (hard breaks are nodes, not text) 203 new InputRule(/@$/, (state, match, start, end) => { 204 if (!openMentionAutocomplete) return null; 205 // Check if the character before @ is a hard break node 206 const $pos = state.doc.resolve(start); 207 const nodeBefore = $pos.nodeBefore; 208 if (nodeBefore && nodeBefore.type.name === "hard_break") { 209 setTimeout(() => openMentionAutocomplete(), 0); 210 } 211 return null; // Let the @ be inserted normally 212 }), 213 ], 214 });