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