Awesome! Couldn’t get to it yesterday, but I finally did today. I scratched my head quite a bit at first, but with a little squinting at my screen, what you said became clear.
Here is the code I’ve come up with, should anyone find a similar use case in the future. Or should someone point out that I’ve gone about this the wrong way. It’s two functions that return a plugin to add a highlight decoration to current paragraph, and to add a highlight decoration to current sentence, respectively. (current = around cursor position).
Thanks @marijn for the help.
export function getParaFocusPlugin() {
return new Plugin({
state: {
init() { return {deco: DecorationSet.empty} },
apply(tr, prev, oldState, state) {
//check if empty selection (cursor) otherwise don't bother
//if we are at the end of a paragraph, no index is defined, so skip that as well
if (tr.selection.empty && state.doc.nodeAt(tr.selection.from)) {
//get the cursor position
const pos = tr.selection.from;
//get the resolved position
const $pos = state.doc.resolve(pos);
//get the start position of the paragraph
const ps = pos - $pos.textOffset;
//get the end position of the paragraph
const pe = ps + $pos.parent.child($pos.index()).nodeSize;
//add the decoration between the chosen positions
const deco = Decoration.inline(ps, pe, {class: 'highlight'});
return {deco: DecorationSet.create(state.doc, [deco])}
} else return prev;
}
},
props: {
decorations(state) { return this.getState(state).deco }
}
});
}
export function getSentenceFocusPlugin() {
return new Plugin({
state: {
init() { return {deco: DecorationSet.empty, commit: null} },
apply(tr, prev, oldState, state) {
if (tr.selection.empty) {
const pos = tr.selection.from;
//extract text from parent node (paragraph)
let txt = tr.selection.$from.parent.textBetween(0, tr.selection.$from.parent.content.size, '%');
//get the start position of the parent node (paragraph)
const startp = tr.selection.$from.start();
//regular expression to test for sentences [TODO: refine to match all use cases]
const reg = new RegExp(/[(\.|\?|\!|\n|\r)]/, 'gi');
//switch variable for the matches below
let match = null;
//empty array in which to store matched positions
let stcpositions = [];
//loop over matches and add the indices to stcpositions
while (match = reg.exec(txt)) {
stcpositions.push(match.index);
}
//add the start position of the parent node (paragraph) to all matches elements
//add start position to the array, so that the first sentence can be matched as well
//add cursor position to the array so that we may filter on it
//sort the array in ascending order to have all positions correctly positioned (haha!)
stcpositions = [startp, ...stcpositions.map(idx => startp + idx), pos].sort((a,b) => {
if (a > b) return 1;
if (a < b) return -1;
return 0;
});
//take the two positions immediately to the left and right of the cursor position, which gives us the sentence delimiter
const limits = stcpositions.map((p, i) => {
if (i === stcpositions.indexOf(pos) -1) return p;
if (i === stcpositions.indexOf(pos) + 1) return p;
else return null
}).filter(d => d !== null);
//add decorations between the limits
const deco = Decoration.inline(limits[0], limits[1], {class: 'highlight'});
return {deco: DecorationSet.create(state.doc, [deco])}
} else return prev;
}
},
props: {
decorations(state) { return this.getState(state).deco }
}
});
}