Thinkmill/keystatic

Enable smart quotes in markdoc editor

Closed this issue · 4 comments

I would like the editor in Keystatic to replace my keyboard's dumb quotes with smart quotes. The markdoc field's editor is ProseMirror, right? I see ProseMirror/prosemirror-inputrules already has a "smartQuotes" input rule defined. Could those rules be added to the editor?

If this is possible through currently available methods I apologize. I checked the documentation for the markdoc field, but I didn't find the answer I needed. I looked at the type signature referenced from the docs, but the MarkdocEditorOptions don't seem to mention anything about quotation marks or input rules, either.

Could it be done by doing import {smartQuotes} from 'prosemirror-inputrules' in the list of input rules?

diff --git a/packages/keystatic/src/form/fields/markdoc/editor/inputrules/rules.ts b/packages/keystatic/src/form/fields/markdoc/editor/inputrules/rules.ts
index ba7d13f6..afe5df6d 100644
--- a/packages/keystatic/src/form/fields/markdoc/editor/inputrules/rules.ts
+++ b/packages/keystatic/src/form/fields/markdoc/editor/inputrules/rules.ts
@@ -10,6 +10,7 @@ import { InputRule } from './inputrules';
 import { shortcuts, simpleMarkShortcuts } from './shortcuts';
 import { MarkType, Node } from 'prosemirror-model';
 import { insertMenuInputRule } from '../autocomplete/insert-menu';
+import { smartQuotes } from 'prosemirror-inputrules';
 
 const textShortcutRules = Object.entries(shortcuts).map(
   ([shortcut, replacement]): InputRule => ({
@@ -19,7 +20,7 @@ const textShortcutRules = Object.entries(shortcuts).map(
 );
 
 export function inputRulesForSchema({ nodes, marks, config }: EditorSchema) {
-  const rules = [...textShortcutRules];
+  const rules = [...textShortcutRules, ...smartQuotes];
   if (nodes.blockquote) {
     rules.push({
       pattern: /^\s*>\s$/,

I learned how to patch a dependency with pnpm, and I created this patch to support smart quotes in the editor. My previous idea about importing prosemirror-inputrules didn't work. It works in production on my blog! I'd be happy to create a pull request if this is something worth including in Keystatic.

# patches/@keystatic__core@0.5.23.patch
diff --git a/dist/index-c7081f7b.js b/dist/index-c7081f7b.js
index 589433e87fc656a155b0405504330eb5806afa10..f14589e66004f6c257f5b11449a54f98a516e235 100644
--- a/dist/index-c7081f7b.js
+++ b/dist/index-c7081f7b.js
@@ -22286,6 +22286,31 @@ const shortcuts = {
   '(r)': '®',
   '(tm)': '™'
 };
+
+// Regex from https://github.com/ProseMirror/prosemirror-inputrules/blob/8433778a3ce4e45c0188341b72fd71da3a440b5b/src/rules.ts
+const smartQuotes = [
+  {
+    regex: /(?:^|[\s\{\[\(\<'"\u2018\u201C])(")$/,
+    replacement: "“"
+  },
+  {
+    regex: /"$/,
+    replacement: "”"
+  },
+  {
+    regex: /(?:^|[\s\{\[\(\<'"\u2018\u201C])(')$/,
+    replacement: "‘"
+  },
+  {
+    regex: /'$/,
+    replacement: "’"
+  }
+]
+const smartQuotesRules = smartQuotes.map(({ regex, replacement }) => ({
+  pattern: regex,
+  handler: stringHandler(replacement),
+}))
+
 const simpleMarkShortcuts = new Map([['bold', ['**', '__']], ['italic', ['*', '_']], ['strikethrough', ['~~']], ['code', ['`']]]);
 
 const textShortcutRules = Object.entries(shortcuts).map(([shortcut, replacement]) => ({
@@ -22297,7 +22322,7 @@ function inputRulesForSchema({
   marks,
   config
 }) {
-  const rules = [...textShortcutRules];
+  const rules = [...textShortcutRules, ...smartQuotesRules];
   if (nodes.blockquote) {
     rules.push({
       pattern: /^\s*>\s$/,

IMO it would be great to have this. But ideally configurable and it would need localization so different smart quotes can be configured for German, French and so on.

something similar that worked for me is to keep dumb quotes in my markdoc files, and rely on markdown-it's 'typographer' option to turn them into smart quotes

markdoc/markdoc#516 (comment)

After using my solution for a while, I agree that this should be handled by markdown-it or something else. One reason, for example, is that because the regex matching in ProseMirror still replaces quotes in places like code blocks, where you typically wouldn't want it to. Another reason is that if you type fast enough, sometimes ProseMirror won't catch apostrophes and they will stay dumb, so you have to watch for that and change them manually—or type more slowly. A third reason is that on mobile, if you use swipe input, the regex isn't smart enough to catch apostrophes in the middle of the word.

With that I'll close the issue. Thanks!