feat: Improve inline rule matching (#8085)

* stash

* fix: Allow inline mark matching to work with preceding brackets
Refactor markInputRule, add markInputRuleForPattern

* docs
This commit is contained in:
Tom Moor
2024-12-07 15:46:25 -05:00
committed by GitHub
parent a738ea97b5
commit 92b1c578f6
7 changed files with 62 additions and 44 deletions

View File

@@ -1,15 +1,21 @@
import escapeRegExp from "lodash/escapeRegExp";
import { InputRule } from "prosemirror-inputrules";
import { MarkType } from "prosemirror-model";
import { EditorState } from "prosemirror-state";
import { getMarksBetween } from "../queries/getMarksBetween";
/**
* A factory function for creating Prosemirror plugins that automatically apply a mark to text
* A factory function for creating a Prosemirror InputRule that automatically apply a mark to text
* that matches a given regular expression.
*
* @param regexp The regular expression to match
* @param markType The mark type to apply
* @param getAttrs A function that returns the attributes to apply to the mark
* Assumes the mark is not already applied, and that the regex includes two named capture groups:
* `remove` and `text`. The `remove` group is used to determine what text should be removed from
* the document before applying the mark, and the `text` group is used to determine what text
* should be marked.
*
* @param regexp The regular expression to match.
* @param markType The mark type to apply.
* @param getAttrs An optional function that returns the attributes to apply to the new mark.
* @returns The input rule
*/
export default function markInputRule(
@@ -19,15 +25,20 @@ export default function markInputRule(
): InputRule {
return new InputRule(
regexp,
(state: EditorState, match: string[], start: number, end: number) => {
(
state: EditorState,
match: RegExpMatchArray,
start: number,
end: number
) => {
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
const { tr } = state;
const captureGroup = match[match.length - 1];
const captureGroup = match.groups?.text ?? match[match.length - 1];
const removalGroup = match.groups?.remove ?? match[match.length - 2];
const fullMatch = match[0];
const startSpaces = fullMatch.search(/\S/);
if (captureGroup) {
const matchStart = start + fullMatch.indexOf(captureGroup);
const matchStart = start + fullMatch.lastIndexOf(removalGroup);
const textStart = start + fullMatch.lastIndexOf(captureGroup);
const textEnd = textStart + captureGroup.length;
@@ -43,14 +54,41 @@ export default function markInputRule(
tr.delete(textEnd, end);
}
if (textStart > start) {
tr.delete(start + startSpaces, textStart);
tr.delete(matchStart, textStart);
}
end = start + startSpaces + captureGroup.length;
start = matchStart;
end = start + captureGroup.length;
}
tr.addMark(start + startSpaces, end, markType.create(attrs));
tr.addMark(start, end, markType.create(attrs));
tr.removeStoredMark(markType);
return tr;
}
);
}
/**
* A factory function for creating a Prosemirror InputRule that automatically applies a mark to
* text that is surrounded by a given pattern.
*
* @param pattern The pattern to match.
* @param markType The mark type to apply.
* @param getAttrs An optional function that returns the attributes to apply to the new mark.
* @returns The input rule
*/
export function markInputRuleForPattern(
pattern: string,
markType: MarkType,
getAttrs?: (match: string[]) => Record<string, unknown>
): InputRule {
const escapedPattern = escapeRegExp(pattern);
return markInputRule(
new RegExp(
`(?:^|[\\s\\[\\{\\(])(?<remove>${escapedPattern}(?<text>[^${escapedPattern}]+)${escapedPattern})$`
),
markType,
getAttrs
);
}

View File

@@ -1,7 +1,7 @@
import { toggleMark } from "prosemirror-commands";
import { InputRule } from "prosemirror-inputrules";
import { MarkSpec, MarkType } from "prosemirror-model";
import markInputRule from "../lib/markInputRule";
import { markInputRuleForPattern } from "../lib/markInputRule";
import Mark from "./Mark";
const heavyWeightRegex = /^(bold(er)?|[5-9]\d{2,})$/;
@@ -33,7 +33,7 @@ export default class Bold extends Mark {
}
inputRules({ type }: { type: MarkType }): InputRule[] {
return [markInputRule(/(?:\*\*)([^*]+)(?:\*\*)$/, type)];
return [markInputRuleForPattern("**", type)];
}
keys({ type }: { type: MarkType }) {

View File

@@ -9,7 +9,7 @@ import {
} from "prosemirror-model";
import { Plugin, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import markInputRule from "../lib/markInputRule";
import { markInputRuleForPattern } from "../lib/markInputRule";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
import { isInCode } from "../queries/isInCode";
import Mark from "./Mark";
@@ -28,7 +28,7 @@ export default class Code extends Mark {
}
inputRules({ type }: { type: MarkType }) {
return [markInputRule(/(?:^|\s)((?:`)((?:[^`]+))(?:`))$/, type)];
return [markInputRuleForPattern("`", type)];
}
keys({ type }: { type: MarkType }) {

View File

@@ -1,7 +1,7 @@
import { rgba } from "polished";
import { toggleMark } from "prosemirror-commands";
import { MarkSpec, MarkType } from "prosemirror-model";
import markInputRule from "../lib/markInputRule";
import { markInputRuleForPattern } from "../lib/markInputRule";
import markRule from "../rules/mark";
import Mark from "./Mark";
@@ -53,7 +53,7 @@ export default class Highlight extends Mark {
}
inputRules({ type }: { type: MarkType }) {
return [markInputRule(/(?:==)([^=]+)(?:==)$/, type)];
return [markInputRuleForPattern("==", type)];
}
keys({ type }: { type: MarkType }) {

View File

@@ -2,7 +2,7 @@ import { toggleMark } from "prosemirror-commands";
import { InputRule } from "prosemirror-inputrules";
import { MarkSpec, MarkType } from "prosemirror-model";
import { Command } from "prosemirror-state";
import markInputRule from "../lib/markInputRule";
import { markInputRuleForPattern } from "../lib/markInputRule";
import Mark from "./Mark";
export default class Italic extends Mark {
@@ -25,29 +25,9 @@ export default class Italic extends Mark {
}
inputRules({ type }: { type: MarkType }): InputRule[] {
/**
* Due to use of snake_case strings common in docs the matching conditions
* are a bit more strict than e.g. the ** bold syntax to help prevent
* false positives.
*
* Matches:
* _1_
* _123_
* (_one_
* [_one_
*
* No match:
* __
* __123_
* __123__
* _123
* one_123_
* ONE_123_
* 1_123_
*/
return [
markInputRule(/(?:^|[^_a-zA-Z0-9])(_([^_]+)_)$/, type),
markInputRule(/(?:^|[^*a-zA-Z0-9])(\*([^*]+)\*)$/, type),
markInputRuleForPattern("_", type),
markInputRuleForPattern("*", type),
];
}

View File

@@ -1,6 +1,6 @@
import { toggleMark } from "prosemirror-commands";
import { MarkSpec, MarkType } from "prosemirror-model";
import markInputRule from "../lib/markInputRule";
import { markInputRuleForPattern } from "../lib/markInputRule";
import Mark from "./Mark";
export default class Strikethrough extends Mark {
@@ -36,7 +36,7 @@ export default class Strikethrough extends Mark {
}
inputRules({ type }: { type: MarkType }) {
return [markInputRule(/~([^~]+)~$/, type)];
return [markInputRuleForPattern("~", type)];
}
toMarkdown() {

View File

@@ -1,6 +1,6 @@
import { toggleMark } from "prosemirror-commands";
import { MarkSpec, MarkType } from "prosemirror-model";
import markInputRule from "../lib/markInputRule";
import { markInputRuleForPattern } from "../lib/markInputRule";
import underlinesRule from "../rules/underlines";
import Mark from "./Mark";
@@ -32,7 +32,7 @@ export default class Underline extends Mark {
}
inputRules({ type }: { type: MarkType }) {
return [markInputRule(/(?:__)([^_]+)(?:__)$/, type)];
return [markInputRuleForPattern("__", type)];
}
keys({ type }: { type: MarkType }) {