@portabletext/plugin-character-pair-decorator
Version:
Automatically match a pair of characters and decorate the text in between
292 lines (291 loc) • 10.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: !0 });
var editor = require("@portabletext/editor"), behaviors = require("@portabletext/editor/behaviors"), utils = require("@portabletext/editor/utils"), react = require("@xstate/react"), remeda = require("remeda"), xstate = require("xstate"), selectors = require("@portabletext/editor/selectors");
function _interopNamespaceCompat(e) {
if (e && typeof e == "object" && "default" in e) return e;
var n = /* @__PURE__ */ Object.create(null);
return e && Object.keys(e).forEach(function(k) {
if (k !== "default") {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: !0,
get: function() {
return e[k];
}
});
}
}), n.default = e, Object.freeze(n);
}
var utils__namespace = /* @__PURE__ */ _interopNamespaceCompat(utils), selectors__namespace = /* @__PURE__ */ _interopNamespaceCompat(selectors);
function createCharacterPairRegex(char, amount) {
const prePrefix = `(?<!\\${char})`, prefix = `\\${char}`.repeat(Math.max(amount, 1)), postPrefix = "(?!\\s)", content = `([^${char}\\n]+?)`, preSuffix = "(?<!\\s)", suffix = `\\${char}`.repeat(Math.max(amount, 1)), postSuffix = `(?!\\${char})`;
return `${prePrefix}${prefix}${postPrefix}${content}${preSuffix}${suffix}${postSuffix}`;
}
function createCharacterPairDecoratorBehavior(config) {
config.pair.amount < 1 && console.warn(
"The amount of characters in the pair should be greater than 0"
);
const pairRegex = createCharacterPairRegex(
config.pair.char,
config.pair.amount
), regEx = new RegExp(`(${pairRegex})$`);
return behaviors.defineBehavior({
on: "insert.text",
guard: ({ snapshot, event }) => {
if (config.pair.amount < 1)
return !1;
const decorator = config.decorator({ schema: snapshot.context.schema });
if (decorator === void 0)
return !1;
const focusTextBlock = selectors__namespace.getFocusTextBlock(snapshot), selectionStartPoint = selectors__namespace.getSelectionStartPoint(snapshot), selectionStartOffset = selectionStartPoint ? utils__namespace.spanSelectionPointToBlockOffset({
context: snapshot.context,
selectionPoint: selectionStartPoint
}) : void 0;
if (!focusTextBlock || !selectionStartOffset)
return !1;
const newText = `${selectors__namespace.getBlockTextBefore(snapshot)}${event.text}`, textToDecorate = newText.match(regEx)?.at(0);
if (textToDecorate === void 0)
return !1;
const prefixOffsets = {
anchor: {
path: focusTextBlock.path,
// Example: "foo **bar**".length - "**bar**".length = 4
offset: newText.length - textToDecorate.length
},
focus: {
path: focusTextBlock.path,
// Example: "foo **bar**".length - "**bar**".length + "*".length * 2 = 6
offset: newText.length - textToDecorate.length + config.pair.char.length * config.pair.amount
}
}, suffixOffsets = {
anchor: {
path: focusTextBlock.path,
// Example: "foo **bar*|" (10) + "*".length - 2 = 9
offset: selectionStartOffset.offset + event.text.length - config.pair.char.length * config.pair.amount
},
focus: {
path: focusTextBlock.path,
// Example: "foo **bar*|" (10) + "*".length = 11
offset: selectionStartOffset.offset + event.text.length
}
};
if (prefixOffsets.focus.offset - prefixOffsets.anchor.offset > 1) {
const prefixSelection = utils__namespace.blockOffsetsToSelection({
context: snapshot.context,
offsets: prefixOffsets
}), inlineObjectBeforePrefixFocus = selectors__namespace.getPreviousInlineObject(
{
...snapshot,
context: {
...snapshot.context,
selection: prefixSelection ? {
anchor: prefixSelection.focus,
focus: prefixSelection.focus
} : null
}
}
), inlineObjectBeforePrefixFocusOffset = inlineObjectBeforePrefixFocus ? utils__namespace.childSelectionPointToBlockOffset({
context: snapshot.context,
selectionPoint: {
path: inlineObjectBeforePrefixFocus.path,
offset: 0
}
}) : void 0;
if (inlineObjectBeforePrefixFocusOffset && inlineObjectBeforePrefixFocusOffset.offset > prefixOffsets.anchor.offset && inlineObjectBeforePrefixFocusOffset.offset < prefixOffsets.focus.offset)
return !1;
}
if (suffixOffsets.focus.offset - suffixOffsets.anchor.offset > 1) {
const previousInlineObject = selectors__namespace.getPreviousInlineObject(snapshot), previousInlineObjectOffset = previousInlineObject ? utils__namespace.childSelectionPointToBlockOffset({
context: snapshot.context,
selectionPoint: {
path: previousInlineObject.path,
offset: 0
}
}) : void 0;
if (previousInlineObjectOffset && previousInlineObjectOffset.offset > suffixOffsets.anchor.offset && previousInlineObjectOffset.offset < suffixOffsets.focus.offset)
return !1;
}
return {
prefixOffsets,
suffixOffsets,
decorator
};
},
actions: [
// Insert the text as usual in its own undo step
({ event }) => [behaviors.execute(event)],
(_, { prefixOffsets, suffixOffsets, decorator }) => [
// Decorate the text between the prefix and suffix
behaviors.execute({
type: "decorator.add",
decorator,
at: {
anchor: prefixOffsets.focus,
focus: suffixOffsets.anchor
}
}),
// Delete the suffix
behaviors.execute({
type: "delete.text",
at: suffixOffsets
}),
// Delete the prefix
behaviors.execute({
type: "delete.text",
at: prefixOffsets
}),
// Toggle the decorator off so the next inserted text isn't emphasized
behaviors.execute({
type: "decorator.remove",
decorator
}),
behaviors.effect(() => {
config.onDecorate({
...suffixOffsets.anchor,
offset: suffixOffsets.anchor.offset - (prefixOffsets.focus.offset - prefixOffsets.anchor.offset)
});
})
]
]
});
}
function CharacterPairDecoratorPlugin(config) {
const editor$1 = editor.useEditor();
return react.useActorRef(decoratorPairMachine, {
input: {
editor: editor$1,
decorator: config.decorator,
pair: config.pair
}
}), null;
}
const decorateListener = ({ sendBack, input }) => input.editor.registerBehavior({
behavior: createCharacterPairDecoratorBehavior({
decorator: input.decorator,
pair: input.pair,
onDecorate: (offset) => {
sendBack({ type: "decorator.add", blockOffset: offset });
}
})
}), selectionListenerCallback = ({ sendBack, input }) => input.editor.registerBehavior({
behavior: behaviors.defineBehavior({
on: "select",
guard: ({ snapshot, event }) => {
if (!event.at)
return { blockOffsets: void 0 };
const anchor = utils__namespace.spanSelectionPointToBlockOffset({
context: snapshot.context,
selectionPoint: event.at.anchor
}), focus = utils__namespace.spanSelectionPointToBlockOffset({
context: snapshot.context,
selectionPoint: event.at.focus
});
return !anchor || !focus ? { blockOffsets: void 0 } : {
blockOffsets: {
anchor,
focus
}
};
},
actions: [
({ event }, { blockOffsets }) => [
{
type: "effect",
effect: () => {
sendBack({ type: "selection", blockOffsets });
}
},
behaviors.forward(event)
]
]
})
}), deleteBackwardListenerCallback = ({ sendBack, input }) => input.editor.registerBehavior({
behavior: behaviors.defineBehavior({
on: "delete.backward",
actions: [
() => [
behaviors.execute({
type: "history.undo"
}),
behaviors.effect(() => {
sendBack({ type: "delete.backward" });
})
]
]
})
}), decoratorPairMachine = xstate.setup({
types: {
context: {},
input: {},
events: {}
},
actors: {
"decorate listener": xstate.fromCallback(decorateListener),
"delete.backward listener": xstate.fromCallback(deleteBackwardListenerCallback),
"selection listener": xstate.fromCallback(selectionListenerCallback)
}
}).createMachine({
id: "decorator pair",
context: ({ input }) => ({
decorator: input.decorator,
editor: input.editor,
pair: input.pair
}),
initial: "idle",
states: {
idle: {
invoke: [
{
src: "decorate listener",
input: ({ context }) => ({
decorator: context.decorator,
editor: context.editor,
pair: context.pair
})
}
],
on: {
"decorator.add": {
target: "decorator added",
actions: xstate.assign({
offsetAfterDecorator: ({ event }) => event.blockOffset
})
}
}
},
"decorator added": {
exit: [
xstate.assign({
offsetAfterDecorator: void 0
})
],
invoke: [
{
src: "selection listener",
input: ({ context }) => ({ editor: context.editor })
},
{
src: "delete.backward listener",
input: ({ context }) => ({ editor: context.editor })
}
],
on: {
selection: {
target: "idle",
guard: ({ context, event }) => !remeda.isDeepEqual(
{
anchor: context.offsetAfterDecorator,
focus: context.offsetAfterDecorator
},
event.blockOffsets
)
},
"delete.backward": {
target: "idle"
}
}
}
}
});
exports.CharacterPairDecoratorPlugin = CharacterPairDecoratorPlugin;
//# sourceMappingURL=index.cjs.map