mirror of
https://github.com/outline/outline.git
synced 2026-01-06 11:09:55 -06:00
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:
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
Reference in New Issue
Block a user