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 });