@liveblocks/node-prosemirror
Version:
A server-side utility that lets you modify prosemirror and tiptap documents hosted in Liveblocks.
249 lines (243 loc) • 6.82 kB
JavaScript
// src/index.ts
import { detectDupes } from "@liveblocks/core";
// src/version.ts
var PKG_NAME = "@liveblocks/node-prosemirror";
var PKG_VERSION = "3.4.0";
var PKG_FORMAT = "esm";
// src/document.ts
import { getSchema, getText } from "@tiptap/core";
import { EditorState } from "@tiptap/pm/state";
import StarterKit from "@tiptap/starter-kit";
import { defaultMarkdownSerializer } from "prosemirror-markdown";
import { initProseMirrorDoc, updateYFragment } from "y-prosemirror";
import { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector } from "yjs";
// src/comment.ts
import { Mark, mergeAttributes } from "@tiptap/core";
var LIVEBLOCKS_COMMENT_MARK_TYPE = "liveblocksCommentMark";
var CommentExtension = Mark.create({
name: LIVEBLOCKS_COMMENT_MARK_TYPE,
excludes: "",
inclusive: false,
keepOnSplit: true,
addAttributes() {
return {
orphan: {
parseHTML: (element) => !!element.getAttribute("data-orphan"),
renderHTML: (attributes) => {
return attributes.orphan ? {
"data-orphan": "true"
} : {};
},
default: false
},
threadId: {
parseHTML: (element) => element.getAttribute("data-lb-thread-id"),
renderHTML: (attributes) => {
return {
"data-lb-thread-id": attributes.threadId
};
},
default: ""
}
};
},
renderHTML({ HTMLAttributes }) {
return [
"span",
mergeAttributes(HTMLAttributes, {
class: "lb-root lb-tiptap-thread-mark"
})
];
}
});
// src/mention.ts
import { mergeAttributes as mergeAttributes2, Node } from "@tiptap/core";
var LIVEBLOCKS_MENTION_TYPE = "liveblocksMention";
var MentionExtension = Node.create({
name: LIVEBLOCKS_MENTION_TYPE,
group: "inline",
inline: true,
selectable: true,
atom: true,
priority: 101,
parseHTML() {
return [
{
tag: "liveblocks-mention"
}
];
},
renderHTML({ HTMLAttributes }) {
return ["liveblocks-mention", mergeAttributes2(HTMLAttributes)];
},
addAttributes() {
return {
id: {
default: null,
parseHTML: (element) => element.getAttribute("data-id"),
renderHTML: (attributes) => {
if (!attributes.id) {
return {};
}
return {
"data-id": attributes.id
// "as" typing because TipTap doesn't have a way to type attributes
};
}
},
notificationId: {
default: null,
parseHTML: (element) => element.getAttribute("data-notification-id"),
renderHTML: (attributes) => {
if (!attributes.notificationId) {
return {};
}
return {
"data-notification-id": attributes.notificationId
// "as" typing because TipTap doesn't have a way to type attributes
};
}
}
};
}
});
// src/document.ts
var DEFAULT_SCHEMA = getSchema([
StarterKit,
CommentExtension,
MentionExtension
]);
var getLiveblocksDocumentState = async (roomId, client, schema, field) => {
const update = new Uint8Array(
await client.getYjsDocumentAsBinaryUpdate(roomId)
);
const ydoc = new Doc();
applyUpdate(ydoc, update);
const fragment = ydoc.getXmlFragment(field ?? "default");
const { mapping, doc } = initProseMirrorDoc(fragment, schema);
const state = EditorState.create({
schema,
doc
});
return {
fragment,
state,
ydoc,
mapping
};
};
var createDocumentFromContent = (content, schema) => {
try {
return schema.nodeFromJSON(content);
} catch (error) {
console.warn(
"[warn]: Invalid content.",
"Passed value:",
content,
"Error:",
error
);
return false;
}
};
async function withProsemirrorDocument({ roomId, schema: maybeSchema, client, field }, callback) {
const schema = maybeSchema ?? DEFAULT_SCHEMA;
let liveblocksState = await getLiveblocksDocumentState(
roomId,
client,
schema,
field ?? "default"
);
const val = await callback({
/**
* Fetches and resyncs the latest document with Liveblocks
*/
async refresh() {
liveblocksState = await getLiveblocksDocumentState(
roomId,
client,
schema,
field ?? "default"
);
},
/**
* Provide a callback to modify documetns with Lexical's standard api. All calls are discrete.
*/
async update(modifyFn) {
const { ydoc, fragment, state, mapping } = liveblocksState;
const beforeVector = encodeStateVector(ydoc);
const afterState = state.apply(modifyFn(state.doc, state.tr));
ydoc.transact(() => {
updateYFragment(ydoc, fragment, afterState.doc, mapping);
});
const diffUpdate = encodeStateAsUpdate(ydoc, beforeVector);
await client.sendYjsBinaryUpdate(roomId, diffUpdate);
await this.refresh();
},
/**
* allows you to set content similar to TipTap's setcontent. Only accepts nulls, objects or strings.
* Unlike TipTap, strings won't be parsed with DOMParser
* */
async setContent(content) {
if (typeof content === "string") {
return this.update((doc, tr) => {
tr.delete(0, doc.content.size);
tr.insertText(content);
return tr;
});
}
if (content === null) {
return this.clearContent();
}
const node = createDocumentFromContent(content, schema);
if (!node) {
throw "Invalid content";
}
return this.update((doc, tr) => {
tr.delete(0, doc.content.size);
tr.insert(0, node);
return tr;
});
},
async clearContent() {
await this.update((doc, tr) => {
tr.delete(0, doc.content.size);
return tr;
});
},
/**
* Uses TipTap's getText function, which allows passing a custom text serializer
*/
getText(options) {
const { state } = liveblocksState;
return getText(state.doc, options);
},
/**
* Helper function to return prosemirror document in JSON form
*/
toJSON() {
return liveblocksState.state.doc.toJSON();
},
/**
* Helper function to return editor state as Markdown. By default it uses the defaultMarkdownSerializer from prosemirror-markdown, but you may pass your own
*/
toMarkdown(serializer) {
return (serializer ?? defaultMarkdownSerializer).serialize(
liveblocksState.state.doc
);
},
/**
* Helper function to return the editor's current prosemirror state
*/
getEditorState() {
return liveblocksState.state;
}
});
return val;
}
// src/index.ts
detectDupes(PKG_NAME, PKG_VERSION, PKG_FORMAT);
export {
withProsemirrorDocument
};
//# sourceMappingURL=index.js.map