Skip to content

Commit 237c77c

Browse files
authored
fix: do not apply input rule to text with code mark (#224)
1 parent c80c4d0 commit 237c77c

File tree

10 files changed

+114
-17
lines changed

10 files changed

+114
-17
lines changed

src/extensions/markdown/Blockquote/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {chainCommands, wrapIn} from 'prosemirror-commands';
2-
import {wrappingInputRule} from 'prosemirror-inputrules';
32
import type {NodeType} from 'prosemirror-model';
43
import {hasParentNodeOfType} from 'prosemirror-utils';
54

65
import type {Action, ExtensionAuto} from '../../../core';
6+
import {wrappingInputRule} from '../../../utils/inputrules';
77
import {withLogAction} from '../../../utils/keymap';
88

99
import {BlockquoteSpecs, blockquoteType} from './BlockquoteSpecs';

src/extensions/markdown/CodeBlock/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {setBlockType} from 'prosemirror-commands';
2-
import {textblockTypeInputRule} from 'prosemirror-inputrules';
32
import {Fragment, NodeType, Slice} from 'prosemirror-model';
43
import {Command, Plugin} from 'prosemirror-state';
54
import {hasParentNodeOfType} from 'prosemirror-utils';
65

76
import type {Action, ExtensionAuto, Keymap} from '../../../core';
7+
import {textblockTypeInputRule} from '../../../utils/inputrules';
88
import {withLogAction} from '../../../utils/keymap';
99

1010
import {CodeBlockSpecs, CodeBlockSpecsOptions} from './CodeBlockSpecs';

src/extensions/markdown/Heading/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {textblockTypeInputRule} from 'prosemirror-inputrules';
21
import type {NodeType} from 'prosemirror-model';
32
import type {EditorState} from 'prosemirror-state';
43
import {hasParentNode} from 'prosemirror-utils';
54

5+
import {textblockTypeInputRule} from '../../../utils/inputrules';
6+
67
import {headingType} from './HeadingSpecs';
78
import {HeadingLevel, headingLevelAttr} from './const';
89

src/extensions/markdown/Link/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {InputRule} from 'prosemirror-inputrules';
22
import type {MarkType} from 'prosemirror-model';
33

44
import type {Action, ExtensionAuto} from '../../../core';
5+
import {hasCodeMark} from '../../../utils/inputrules';
56

67
import {LinkSpecs, linkMarkName, linkType} from './LinkSpecs';
78
import {LinkActionMeta, LinkActionParams, linkCommand} from './actions';
@@ -37,6 +38,8 @@ declare global {
3738
// TODO: think about generalizing with markInputRule
3839
function linkInputRule(markType: MarkType): InputRule {
3940
return new InputRule(/\[(.+)]\((\S+)\)\s$/, (state, match, start, end) => {
41+
if (hasCodeMark(state, match, start, end)) return null;
42+
4043
// handle the rule only if is start of line or there is a space before "open" symbols
4144
if ((match as RegExpMatchArray).index! > 0) {
4245
const re = match as RegExpMatchArray;

src/extensions/markdown/Lists/inputrules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {wrappingInputRule} from 'prosemirror-inputrules';
21
import type {NodeType} from 'prosemirror-model';
32

43
import type {ExtensionWithOptions} from '../../../core';
4+
import {wrappingInputRule} from '../../../utils/inputrules';
55

66
import {blType, olType} from './utils';
77

src/extensions/yfm/Emoji/EmojiInput/EmojiInput.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {InputRule} from 'prosemirror-inputrules';
22

33
import type {ExtensionAuto} from '../../../../core';
44
import {escapeRegExp} from '../../../../utils/ecapeRegexp';
5+
import {hasCodeMark} from '../../../../utils/inputrules';
56
import {EmojiConsts, EmojiSpecsOptions} from '../EmojiSpecs';
67

78
export const EmojiInput: ExtensionAuto<EmojiSpecsOptions> = (builder, opts) => {
@@ -24,6 +25,8 @@ export const EmojiInput: ExtensionAuto<EmojiSpecsOptions> = (builder, opts) => {
2425
builder.addInputRules(() => ({
2526
rules: [
2627
new InputRule(regex, (state, match, start, end) => {
28+
if (hasCodeMark(state, match, start, end)) return null;
29+
2730
const pattern = match[1];
2831
const markup = mapPatternToMarkup[pattern];
2932
const content = defs[markup];

src/extensions/yfm/Math/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import {chainCommands, setBlockType} from 'prosemirror-commands';
2-
import {textblockTypeInputRule} from 'prosemirror-inputrules';
32
import {Command, TextSelection} from 'prosemirror-state';
43
import {hasParentNodeOfType} from 'prosemirror-utils';
54

65
import type {Action, ExtensionAuto} from '../../../core';
7-
import {inlineNodeInputRule} from '../../../utils/inputrules';
6+
import {inlineNodeInputRule, textblockTypeInputRule} from '../../../utils/inputrules';
87
import {isTextSelection} from '../../../utils/selection';
98

109
import {MathSpecs} from './MathSpecs';

src/utils/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ export * from './clipboard';
55
export * from './ecapeRegexp';
66
export * from './event-emitter';
77
export * from './helpers';
8-
export * from './inputrules';
8+
export {
9+
markInputRule,
10+
nodeInputRule,
11+
inlineNodeInputRule,
12+
textblockTypeInputRule,
13+
wrappingInputRule,
14+
} from './inputrules';
915
export * from './keymap';
1016
export * from './marks';
1117
export * from './node-children';

src/utils/inputrules.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ import {isFunction} from '../lodash';
77
import {isMarkActive} from '../utils/marks';
88
// TODO: remove explicit import from code extension
99

10+
export function hasCodeMark(
11+
state: EditorState,
12+
_match: RegExpMatchArray,
13+
start: number,
14+
end: number,
15+
): boolean {
16+
// TODO: remove explicit import from code extension
17+
const codeMarkType = codeType(state.schema);
18+
if (isMarkActive(state, codeMarkType)) return true;
19+
if (state.doc.rangeHasMark(start, end, codeMarkType)) return true;
20+
return false;
21+
}
22+
23+
export {textblockTypeInputRule, wrappingInputRule} from './rulebuilders';
24+
1025
function getMarksBetween(start: number, end: number, state: EditorState) {
1126
let marks: {start: number; end: number; mark: Mark}[] = [];
1227

@@ -55,11 +70,7 @@ export function markInputRule(
5570
if (re.input![re.index! - 1] !== ' ') return null;
5671
}
5772

58-
// TODO: remove explicit import from code extension
59-
const codeMarkType = codeType(state.schema);
60-
if (isMarkActive(state, codeMarkType)) {
61-
return null;
62-
}
73+
if (hasCodeMark(state, match, start, end)) return null;
6374

6475
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
6576
const {tr} = state;
@@ -74,11 +85,6 @@ export function markInputRule(
7485
const textEnd = textStart + match[m].length;
7586

7687
const marksBetween = getMarksBetween(start, end, state);
77-
78-
if (marksBetween.some((item) => item.mark.type === codeMarkType)) {
79-
return null;
80-
}
81-
8288
const excludedMarks = marksBetween
8389
.filter((item) => item.mark.type.excludes(markType))
8490
.filter((item) => item.end > matchStart);
@@ -111,6 +117,8 @@ export function nodeInputRule(
111117
selectionOffset = 0,
112118
): InputRule {
113119
return new InputRule(regexp, (state, match, start, end) => {
120+
if (hasCodeMark(state, match, start, end)) return null;
121+
114122
const [matchStr] = match;
115123
const {tr} = state;
116124

@@ -133,6 +141,8 @@ export function inlineNodeInputRule(
133141
fragment: (match: string) => NodeInputRuleReplaceFragment,
134142
): InputRule {
135143
return new InputRule(regexp, (state, match, start, end) => {
144+
if (hasCodeMark(state, match, start, end)) return null;
145+
136146
const [matchStr] = match;
137147
const {tr} = state;
138148

src/utils/rulebuilders.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
///
2+
/// Copy of https://github.com/ProseMirror/prosemirror-inputrules/blob/1.4.0/src/rulebuilders.ts
3+
/// Added a check for the presence of a code mark
4+
///
5+
6+
import {InputRule} from 'prosemirror-inputrules';
7+
import {Attrs, Node, NodeType} from 'prosemirror-model';
8+
import {canJoin, findWrapping} from 'prosemirror-transform';
9+
10+
import {hasCodeMark} from './inputrules';
11+
12+
/// Build an input rule for automatically wrapping a textblock when a
13+
/// given string is typed. The `regexp` argument is
14+
/// directly passed through to the `InputRule` constructor. You'll
15+
/// probably want the regexp to start with `^`, so that the pattern can
16+
/// only occur at the start of a textblock.
17+
///
18+
/// `nodeType` is the type of node to wrap in. If it needs attributes,
19+
/// you can either pass them directly, or pass a function that will
20+
/// compute them from the regular expression match.
21+
///
22+
/// By default, if there's a node with the same type above the newly
23+
/// wrapped node, the rule will try to [join](#transform.Transform.join) those
24+
/// two nodes. You can pass a join predicate, which takes a regular
25+
/// expression match and the node before the wrapped node, and can
26+
/// return a boolean to indicate whether a join should happen.
27+
export function wrappingInputRule(
28+
regexp: RegExp,
29+
nodeType: NodeType,
30+
getAttrs: Attrs | null | ((matches: RegExpMatchArray) => Attrs | null) = null,
31+
joinPredicate?: (match: RegExpMatchArray, node: Node) => boolean,
32+
) {
33+
return new InputRule(regexp, (state, match, start, end) => {
34+
if (hasCodeMark(state, match, start, end)) return null;
35+
36+
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
37+
const tr = state.tr.delete(start, end);
38+
const $start = tr.doc.resolve(start),
39+
range = $start.blockRange(),
40+
wrapping = range && findWrapping(range, nodeType, attrs);
41+
if (!wrapping) return null;
42+
tr.wrap(range!, wrapping);
43+
const before = tr.doc.resolve(start - 1).nodeBefore;
44+
if (
45+
before &&
46+
before.type == nodeType &&
47+
canJoin(tr.doc, start - 1) &&
48+
(!joinPredicate || joinPredicate(match, before))
49+
)
50+
tr.join(start - 1);
51+
return tr;
52+
});
53+
}
54+
55+
/// Build an input rule that changes the type of a textblock when the
56+
/// matched text is typed into it. You'll usually want to start your
57+
/// regexp with `^` to that it is only matched at the start of a
58+
/// textblock. The optional `getAttrs` parameter can be used to compute
59+
/// the new node's attributes, and works the same as in the
60+
/// `wrappingInputRule` function.
61+
export function textblockTypeInputRule(
62+
regexp: RegExp,
63+
nodeType: NodeType,
64+
getAttrs: Attrs | null | ((match: RegExpMatchArray) => Attrs | null) = null,
65+
) {
66+
return new InputRule(regexp, (state, match, start, end) => {
67+
if (hasCodeMark(state, match, start, end)) return null;
68+
69+
const $start = state.doc.resolve(start);
70+
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
71+
if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), nodeType))
72+
return null;
73+
return state.tr.delete(start, end).setBlockType(start, start, nodeType, attrs);
74+
});
75+
}

0 commit comments

Comments
 (0)