Skip to content

Commit

Permalink
fix: do not apply input rule to text with code mark (#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
d3m1d0v authored Apr 9, 2024
1 parent c80c4d0 commit 237c77c
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/extensions/markdown/Blockquote/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {chainCommands, wrapIn} from 'prosemirror-commands';
import {wrappingInputRule} from 'prosemirror-inputrules';
import type {NodeType} from 'prosemirror-model';
import {hasParentNodeOfType} from 'prosemirror-utils';

import type {Action, ExtensionAuto} from '../../../core';
import {wrappingInputRule} from '../../../utils/inputrules';
import {withLogAction} from '../../../utils/keymap';

import {BlockquoteSpecs, blockquoteType} from './BlockquoteSpecs';
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/markdown/CodeBlock/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {setBlockType} from 'prosemirror-commands';
import {textblockTypeInputRule} from 'prosemirror-inputrules';
import {Fragment, NodeType, Slice} from 'prosemirror-model';
import {Command, Plugin} from 'prosemirror-state';
import {hasParentNodeOfType} from 'prosemirror-utils';

import type {Action, ExtensionAuto, Keymap} from '../../../core';
import {textblockTypeInputRule} from '../../../utils/inputrules';
import {withLogAction} from '../../../utils/keymap';

import {CodeBlockSpecs, CodeBlockSpecsOptions} from './CodeBlockSpecs';
Expand Down
3 changes: 2 additions & 1 deletion src/extensions/markdown/Heading/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {textblockTypeInputRule} from 'prosemirror-inputrules';
import type {NodeType} from 'prosemirror-model';
import type {EditorState} from 'prosemirror-state';
import {hasParentNode} from 'prosemirror-utils';

import {textblockTypeInputRule} from '../../../utils/inputrules';

import {headingType} from './HeadingSpecs';
import {HeadingLevel, headingLevelAttr} from './const';

Expand Down
3 changes: 3 additions & 0 deletions src/extensions/markdown/Link/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {InputRule} from 'prosemirror-inputrules';
import type {MarkType} from 'prosemirror-model';

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

import {LinkSpecs, linkMarkName, linkType} from './LinkSpecs';
import {LinkActionMeta, LinkActionParams, linkCommand} from './actions';
Expand Down Expand Up @@ -37,6 +38,8 @@ declare global {
// TODO: think about generalizing with markInputRule
function linkInputRule(markType: MarkType): InputRule {
return new InputRule(/\[(.+)]\((\S+)\)\s$/, (state, match, start, end) => {
if (hasCodeMark(state, match, start, end)) return null;

// handle the rule only if is start of line or there is a space before "open" symbols
if ((match as RegExpMatchArray).index! > 0) {
const re = match as RegExpMatchArray;
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/markdown/Lists/inputrules.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {wrappingInputRule} from 'prosemirror-inputrules';
import type {NodeType} from 'prosemirror-model';

import type {ExtensionWithOptions} from '../../../core';
import {wrappingInputRule} from '../../../utils/inputrules';

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

Expand Down
3 changes: 3 additions & 0 deletions src/extensions/yfm/Emoji/EmojiInput/EmojiInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {InputRule} from 'prosemirror-inputrules';

import type {ExtensionAuto} from '../../../../core';
import {escapeRegExp} from '../../../../utils/ecapeRegexp';
import {hasCodeMark} from '../../../../utils/inputrules';
import {EmojiConsts, EmojiSpecsOptions} from '../EmojiSpecs';

export const EmojiInput: ExtensionAuto<EmojiSpecsOptions> = (builder, opts) => {
Expand All @@ -24,6 +25,8 @@ export const EmojiInput: ExtensionAuto<EmojiSpecsOptions> = (builder, opts) => {
builder.addInputRules(() => ({
rules: [
new InputRule(regex, (state, match, start, end) => {
if (hasCodeMark(state, match, start, end)) return null;

const pattern = match[1];
const markup = mapPatternToMarkup[pattern];
const content = defs[markup];
Expand Down
3 changes: 1 addition & 2 deletions src/extensions/yfm/Math/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {chainCommands, setBlockType} from 'prosemirror-commands';
import {textblockTypeInputRule} from 'prosemirror-inputrules';
import {Command, TextSelection} from 'prosemirror-state';
import {hasParentNodeOfType} from 'prosemirror-utils';

import type {Action, ExtensionAuto} from '../../../core';
import {inlineNodeInputRule} from '../../../utils/inputrules';
import {inlineNodeInputRule, textblockTypeInputRule} from '../../../utils/inputrules';
import {isTextSelection} from '../../../utils/selection';

import {MathSpecs} from './MathSpecs';
Expand Down
8 changes: 7 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ export * from './clipboard';
export * from './ecapeRegexp';
export * from './event-emitter';
export * from './helpers';
export * from './inputrules';
export {
markInputRule,
nodeInputRule,
inlineNodeInputRule,
textblockTypeInputRule,
wrappingInputRule,
} from './inputrules';
export * from './keymap';
export * from './marks';
export * from './node-children';
Expand Down
30 changes: 20 additions & 10 deletions src/utils/inputrules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ import {isFunction} from '../lodash';
import {isMarkActive} from '../utils/marks';
// TODO: remove explicit import from code extension

export function hasCodeMark(
state: EditorState,
_match: RegExpMatchArray,
start: number,
end: number,
): boolean {
// TODO: remove explicit import from code extension
const codeMarkType = codeType(state.schema);
if (isMarkActive(state, codeMarkType)) return true;
if (state.doc.rangeHasMark(start, end, codeMarkType)) return true;
return false;
}

export {textblockTypeInputRule, wrappingInputRule} from './rulebuilders';

function getMarksBetween(start: number, end: number, state: EditorState) {
let marks: {start: number; end: number; mark: Mark}[] = [];

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

// TODO: remove explicit import from code extension
const codeMarkType = codeType(state.schema);
if (isMarkActive(state, codeMarkType)) {
return null;
}
if (hasCodeMark(state, match, start, end)) return null;

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

const marksBetween = getMarksBetween(start, end, state);

if (marksBetween.some((item) => item.mark.type === codeMarkType)) {
return null;
}

const excludedMarks = marksBetween
.filter((item) => item.mark.type.excludes(markType))
.filter((item) => item.end > matchStart);
Expand Down Expand Up @@ -111,6 +117,8 @@ export function nodeInputRule(
selectionOffset = 0,
): InputRule {
return new InputRule(regexp, (state, match, start, end) => {
if (hasCodeMark(state, match, start, end)) return null;

const [matchStr] = match;
const {tr} = state;

Expand All @@ -133,6 +141,8 @@ export function inlineNodeInputRule(
fragment: (match: string) => NodeInputRuleReplaceFragment,
): InputRule {
return new InputRule(regexp, (state, match, start, end) => {
if (hasCodeMark(state, match, start, end)) return null;

const [matchStr] = match;
const {tr} = state;

Expand Down
75 changes: 75 additions & 0 deletions src/utils/rulebuilders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
///
/// Copy of https://github.com/ProseMirror/prosemirror-inputrules/blob/1.4.0/src/rulebuilders.ts
/// Added a check for the presence of a code mark
///

import {InputRule} from 'prosemirror-inputrules';
import {Attrs, Node, NodeType} from 'prosemirror-model';
import {canJoin, findWrapping} from 'prosemirror-transform';

import {hasCodeMark} from './inputrules';

/// Build an input rule for automatically wrapping a textblock when a
/// given string is typed. The `regexp` argument is
/// directly passed through to the `InputRule` constructor. You'll
/// probably want the regexp to start with `^`, so that the pattern can
/// only occur at the start of a textblock.
///
/// `nodeType` is the type of node to wrap in. If it needs attributes,
/// you can either pass them directly, or pass a function that will
/// compute them from the regular expression match.
///
/// By default, if there's a node with the same type above the newly
/// wrapped node, the rule will try to [join](#transform.Transform.join) those
/// two nodes. You can pass a join predicate, which takes a regular
/// expression match and the node before the wrapped node, and can
/// return a boolean to indicate whether a join should happen.
export function wrappingInputRule(
regexp: RegExp,
nodeType: NodeType,
getAttrs: Attrs | null | ((matches: RegExpMatchArray) => Attrs | null) = null,
joinPredicate?: (match: RegExpMatchArray, node: Node) => boolean,
) {
return new InputRule(regexp, (state, match, start, end) => {
if (hasCodeMark(state, match, start, end)) return null;

const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
const tr = state.tr.delete(start, end);
const $start = tr.doc.resolve(start),
range = $start.blockRange(),
wrapping = range && findWrapping(range, nodeType, attrs);
if (!wrapping) return null;
tr.wrap(range!, wrapping);
const before = tr.doc.resolve(start - 1).nodeBefore;
if (
before &&
before.type == nodeType &&
canJoin(tr.doc, start - 1) &&
(!joinPredicate || joinPredicate(match, before))
)
tr.join(start - 1);
return tr;
});
}

/// Build an input rule that changes the type of a textblock when the
/// matched text is typed into it. You'll usually want to start your
/// regexp with `^` to that it is only matched at the start of a
/// textblock. The optional `getAttrs` parameter can be used to compute
/// the new node's attributes, and works the same as in the
/// `wrappingInputRule` function.
export function textblockTypeInputRule(
regexp: RegExp,
nodeType: NodeType,
getAttrs: Attrs | null | ((match: RegExpMatchArray) => Attrs | null) = null,
) {
return new InputRule(regexp, (state, match, start, end) => {
if (hasCodeMark(state, match, start, end)) return null;

const $start = state.doc.resolve(start);
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), nodeType))
return null;
return state.tr.delete(start, end).setBlockType(start, start, nodeType, attrs);
});
}

0 comments on commit 237c77c

Please sign in to comment.