Here's the code I'm interested in:
new InputRule(match: RegExp, filter: ?string, handler: union<string, fn(pm: ProseMirror, match: [string], pos: number))
You can optionally provide a filter, which should be a single character that always appears at the end of the match, and will be used to only apply the rule when there's an actual chance of it succeeding.
After reading this I was left wondering "but why wouldn't it just use the regex?". I presume the answer is performance — checking 1 character is _much_ faster than running a regex.
What follows is my thought process on how I think we could improve this.
I think we can probably have a more generic API here, e.g.
new InputRule(match: fn(string) → ?[string], handler: union<string, fn(pm: ProseMirror, match: [string], pos: number))
The change is removing filter and changing the type of match. This shifts the filter behaviour into the match function — which opens the door to the match function being more powerful. e.g. it could choose to short circuit on more than just a single character.
To avoid burdening consumers who want something simple, we could provide a helper function that does the old behaviour:
re(match: RegExp, filter?: string) → ?[string]
Consumers wanting the old behaviour could then write:
const { re } = require('inputrules/utils');
new InputRule(re(/h1.$/, '.'), …)
At this point I realised we could go a step further and open up API even more, to support other scenarios like:
The API could be:
<T>InputRule(match: fn(pm: ProseMirror, pos: number, prefix: string) → ?T, handler: fn(pm: ProseMirror, pos: number, match: T))
Both the match type and handler type is changed.
prefix is the text between the parent and the cursor (what ProseMirror currently tests match against). Its existence is entirely as a performance optimisation (to avoid each match function from needing to compute this string).
match function would have complete flexibility to inspect the current state of the editor (using the public API), and return a nullable result that's passed onto the handler (if it's not null).
In conjunction with #380 this means we could end up with consumers writing:
// Convert wiki markup headings `h1.` to markdown `#`
const { re, replaceWith } = require('inputrules/utils');
new InputRule(re(/h1.$/, '.'), replaceWith("#"))
An extension here would be to allow matchers to be composed. For example if might want to decompose the propose 're' function into:
We'd then have a combining function that would let you do:
const { reMatch, suffixMatch, combineAnd } = require('inputrules/utils');
const re = (regex, suffix) => combineAnd(suffixMatch(suffix), reMatch(regex))
Interested in your thoughts!
The reason filter is there (though it might be an over-optimization), is that to match a regexp we first need to gather all text before the cursor into a single string, which for big textblocks can be expensive.
But thinking about this, it probably usually isn't, and we could set a limit on the amount of text you can match (at, say, 100 characters) to work around it.
As for allowing a function to match, that sounds good. But I'd be inclined to just allow the first argument to the constructor to be either a function _or_ a regexp, and automatically wrap it inside the constructor when it's a regexp.
Would you be interested in working on this?
Yes I'm interested in working on this. I'll raise a pull request and give you something concrete to look at.
Wonderful!
The rewritten inputrules module implements the changes mentioned here.
Most helpful comment
Yes I'm interested in working on this. I'll raise a pull request and give you something concrete to look at.