Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 25 additions & 21 deletions packages/core/src/api/exporters/copyExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@ import { NodeSelection, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
import { initializeESMDependencies } from "../../util/esmDependencies";
import { createExternalHTMLExporter } from "./html/externalHTMLExporter";
import { createInternalHTMLSerializer } from "./html/internalHTMLSerializer";
import { cleanHTMLToMarkdown } from "./markdown/markdownExporter";

function selectedFragmentToHTML<
async function selectedFragmentToHTML<
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema
>(
view: EditorView,
editor: BlockNoteEditor<BSchema, I, S>
): {
): Promise<{
internalHTML: string;
externalHTML: string;
plainText: string;
} {
}> {
const selectedFragment = view.state.selection.content().content;

const internalHTMLSerializer = createInternalHTMLSerializer(
const internalHTMLSerializer = await createInternalHTMLSerializer(
view.state.schema,
editor
);
Expand All @@ -32,6 +33,7 @@ function selectedFragmentToHTML<
{}
);

await initializeESMDependencies();
const externalHTMLExporter = createExternalHTMLExporter(
view.state.schema,
editor
Expand All @@ -41,7 +43,7 @@ function selectedFragmentToHTML<
{}
);

const plainText = cleanHTMLToMarkdown(externalHTML);
const plainText = await cleanHTMLToMarkdown(externalHTML);

return { internalHTML, externalHTML, plainText };
}
Expand Down Expand Up @@ -83,15 +85,16 @@ export const createCopyToClipboardExtension = <
);
}

const { internalHTML, externalHTML, plainText } =
selectedFragmentToHTML(view, editor);

// TODO: Writing to other MIME types not working in Safari for
// some reason.
event.clipboardData!.setData("blocknote/html", internalHTML);
event.clipboardData!.setData("text/html", externalHTML);
event.clipboardData!.setData("text/plain", plainText);
(async () => {
const { internalHTML, externalHTML, plainText } =
await selectedFragmentToHTML(view, editor);

// TODO: Writing to other MIME types not working in Safari for
// some reason.
event.clipboardData!.setData("blocknote/html", internalHTML);
event.clipboardData!.setData("text/html", externalHTML);
event.clipboardData!.setData("text/plain", plainText);
})();
// Prevent default PM handler to be called
return true;
},
Expand Down Expand Up @@ -125,15 +128,16 @@ export const createCopyToClipboardExtension = <
event.preventDefault();
event.dataTransfer!.clearData();

const { internalHTML, externalHTML, plainText } =
selectedFragmentToHTML(view, editor);

// TODO: Writing to other MIME types not working in Safari for
// some reason.
event.dataTransfer!.setData("blocknote/html", internalHTML);
event.dataTransfer!.setData("text/html", externalHTML);
event.dataTransfer!.setData("text/plain", plainText);
(async () => {
const { internalHTML, externalHTML, plainText } =
await selectedFragmentToHTML(view, editor);

// TODO: Writing to other MIME types not working in Safari for
// some reason.
event.dataTransfer!.setData("blocknote/html", internalHTML);
event.dataTransfer!.setData("text/html", externalHTML);
event.dataTransfer!.setData("text/plain", plainText);
})();
// Prevent default PM handler to be called
return true;
},
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/api/exporters/html/externalHTMLExporter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { DOMSerializer, Fragment, Node, Schema } from "prosemirror-model";
import rehypeParse from "rehype-parse";
import rehypeStringify from "rehype-stringify";
import { unified } from "unified";

import { PartialBlock } from "../../../blocks/defaultBlocks";
import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema";
import { esmDependencies } from "../../../util/esmDependencies";
import { blockToNode } from "../../nodeConversions/nodeConversions";
import {
serializeNodeInner,
Expand Down Expand Up @@ -47,6 +45,8 @@ export interface ExternalHTMLExporter<
) => string;
}

// Needs to be sync because it's used in drag handler event (SideMenuPlugin)
// Ideally, call `await initializeESMDependencies()` before calling this function
export const createExternalHTMLExporter = <
BSchema extends BlockSchema,
I extends InlineContentSchema,
Expand All @@ -55,6 +55,14 @@ export const createExternalHTMLExporter = <
schema: Schema,
editor: BlockNoteEditor<BSchema, I, S>
): ExternalHTMLExporter<BSchema, I, S> => {
const deps = esmDependencies;

if (!deps) {
throw new Error(
"External HTML exporter requires ESM dependencies to be initialized"
);
}

const serializer = DOMSerializer.fromSchema(schema) as DOMSerializer & {
serializeNodeInner: (
node: Node,
Expand All @@ -79,16 +87,17 @@ export const createExternalHTMLExporter = <
// but additionally runs it through the `simplifyBlocks` rehype plugin to
// convert the internal HTML to external.
serializer.exportProseMirrorFragment = (fragment, options) => {
const externalHTML = unified()
.use(rehypeParse, { fragment: true })
const externalHTML = deps.unified
.unified()
.use(deps.rehypeParse.default, { fragment: true })
.use(simplifyBlocks, {
orderedListItemBlockTypes: new Set<string>(["numberedListItem"]),
unorderedListItemBlockTypes: new Set<string>([
"bulletListItem",
"checkListItem",
]),
})
.use(rehypeStringify)
.use(deps.rehypeStringify.default)
.processSync(serializeProseMirrorFragment(fragment, serializer, options));

return externalHTML.value as string;
Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/api/exporters/html/htmlConversion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { PartialBlock } from "../../../blocks/defaultBlocks";
import { BlockSchema } from "../../../schema/blocks/types";
import { InlineContentSchema } from "../../../schema/inlineContent/types";
import { StyleSchema } from "../../../schema/styles/types";
import { initializeESMDependencies } from "../../../util/esmDependencies";
import { customBlocksTestCases } from "../../testUtil/cases/customBlocks";
import { customInlineContentTestCases } from "../../testUtil/cases/customInlineContent";
import { customStylesTestCases } from "../../testUtil/cases/customStyles";
Expand Down Expand Up @@ -44,6 +45,7 @@ async function convertToHTMLAndCompareSnapshots<

expect(parsed).toStrictEqual(fullBlocks);

await initializeESMDependencies();
// Create the "external" HTML, which is a cleaned up HTML representation, but lossy
const exporter = createExternalHTMLExporter(editor.pmSchema, editor);
const externalHTML = exporter.exportBlocks(blocks, {});
Expand Down Expand Up @@ -175,7 +177,7 @@ describe("Test ProseMirror fragment edge case conversion", () => {
editor.replaceBlocks(editor.document, blocks);
});

it("Selection within a block's children", () => {
it("Selection within a block's children", async () => {
// Selection starts and ends within the first block's children.
editor.dispatch(
editor._tiptapEditor.state.tr.setSelection(
Expand All @@ -186,6 +188,7 @@ describe("Test ProseMirror fragment edge case conversion", () => {
const copiedFragment =
editor._tiptapEditor.state.selection.content().content;

await initializeESMDependencies();
const exporter = createExternalHTMLExporter(editor.pmSchema, editor);
const externalHTML = exporter.exportProseMirrorFragment(
copiedFragment,
Expand All @@ -197,7 +200,7 @@ describe("Test ProseMirror fragment edge case conversion", () => {
);
});

it("Selection leaves a block's children", () => {
it("Selection leaves a block's children", async () => {
// Selection starts and ends within the first block's children and ends
// outside, at a shallower nesting level in the second block.
editor.dispatch(
Expand All @@ -209,6 +212,7 @@ describe("Test ProseMirror fragment edge case conversion", () => {
const copiedFragment =
editor._tiptapEditor.state.selection.content().content;

await initializeESMDependencies();
const exporter = createExternalHTMLExporter(editor.pmSchema, editor);
const externalHTML = exporter.exportProseMirrorFragment(
copiedFragment,
Expand All @@ -220,7 +224,7 @@ describe("Test ProseMirror fragment edge case conversion", () => {
);
});

it("Selection spans multiple blocks' children", () => {
it("Selection spans multiple blocks' children", async () => {
// Selection starts and ends within the first block's children and ends
// within the second block's children.
editor.dispatch(
Expand All @@ -231,6 +235,7 @@ describe("Test ProseMirror fragment edge case conversion", () => {

const copiedFragment =
editor._tiptapEditor.state.selection.content().content;
await initializeESMDependencies();
const exporter = createExternalHTMLExporter(editor.pmSchema, editor);
const externalHTML = exporter.exportProseMirrorFragment(
copiedFragment,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Element as HASTElement, Parent as HASTParent } from "hast";
import { fromDom } from "hast-util-from-dom";
import { esmDependencies } from "../../../../util/esmDependencies";

type SimplifyBlocksOptions = {
orderedListItemBlockTypes: Set<string>;
Expand All @@ -16,6 +16,14 @@ type SimplifyBlocksOptions = {
* @param options Options for specifying which block types represent ordered and unordered list items.
*/
export function simplifyBlocks(options: SimplifyBlocksOptions) {
const deps = esmDependencies;

if (!deps) {
throw new Error(
"simplifyBlocks requires ESM dependencies to be initialized"
);
}

const listItemBlockTypes = new Set<string>([
...options.orderedListItemBlockTypes,
...options.unorderedListItemBlockTypes,
Expand Down Expand Up @@ -110,13 +118,13 @@ export function simplifyBlocks(options: SimplifyBlocksOptions) {
// type as this was already done earlier.
if (!activeList) {
// Creates a new list element to represent an active list.
activeList = fromDom(
activeList = deps.hastUtilFromDom.fromDom(
document.createElement(listItemBlockType!)
) as HASTElement;
}

// Creates a new list item element to represent the block.
const listItemElement = fromDom(
const listItemElement = deps.hastUtilFromDom.fromDom(
document.createElement("li")
) as HASTElement;

Expand Down
37 changes: 25 additions & 12 deletions packages/core/src/api/exporters/markdown/markdownExporter.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
import { Schema } from "prosemirror-model";
import rehypeParse from "rehype-parse";
import rehypeRemark from "rehype-remark";
import remarkGfm from "remark-gfm";
import remarkStringify from "remark-stringify";
import { unified } from "unified";
import { PartialBlock } from "../../../blocks/defaultBlocks";
import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema";
import {
esmDependencies,
initializeESMDependencies,
} from "../../../util/esmDependencies";
import { createExternalHTMLExporter } from "../html/externalHTMLExporter";
import { removeUnderlines } from "./removeUnderlinesRehypePlugin";
import { addSpacesToCheckboxes } from "./util/addSpacesToCheckboxesRehypePlugin";

// Needs to be sync because it's used in drag handler event (SideMenuPlugin)
// Ideally, call `await initializeESMDependencies()` before calling this function
export function cleanHTMLToMarkdown(cleanHTMLString: string) {
const markdownString = unified()
.use(rehypeParse, { fragment: true })
const deps = esmDependencies;

if (!deps) {
throw new Error(
"cleanHTMLToMarkdown requires ESM dependencies to be initialized"
);
}

const markdownString = deps.unified
.unified()
.use(deps.rehypeParse.default, { fragment: true })
.use(removeUnderlines)
.use(addSpacesToCheckboxes)
.use(rehypeRemark)
.use(remarkGfm)
.use(remarkStringify, { handlers: { text: (node) => node.value } })
.use(deps.rehypeRemark.default)
.use(deps.remarkGfm.default)
.use(deps.remarkStringify.default, {
handlers: { text: (node) => node.value },
})
.processSync(cleanHTMLString);

return markdownString.value as string;
}

export function blocksToMarkdown<
export async function blocksToMarkdown<
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema
Expand All @@ -33,7 +45,8 @@ export function blocksToMarkdown<
schema: Schema,
editor: BlockNoteEditor<BSchema, I, S>,
options: { document?: Document }
): string {
): Promise<string> {
await initializeESMDependencies();
const exporter = createExternalHTMLExporter(schema, editor);
const externalHTML = exporter.exportBlocks(blocks, options);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { Element as HASTElement, Parent as HASTParent } from "hast";
import { fromDom } from "hast-util-from-dom";
import { esmDependencies } from "../../../../util/esmDependencies";

/**
* Rehype plugin which adds a space after each checkbox input element. This is
* because remark doesn't add any spaces between the checkbox input and the text
* itself, but these are needed for correct Markdown syntax.
*/
export function addSpacesToCheckboxes() {
const deps = esmDependencies;

if (!deps) {
throw new Error(
"simplifyBlocks requires ESM dependencies to be initialized"
);
}

const helper = (tree: HASTParent) => {
if (tree.children && "length" in tree.children && tree.children.length) {
for (let i = tree.children.length - 1; i >= 0; i--) {
Expand All @@ -29,7 +37,9 @@ export function addSpacesToCheckboxes() {
nextChild.children.splice(
0,
0,
fromDom(document.createTextNode(" ")) as HASTElement
deps.hastUtilFromDom.fromDom(
document.createTextNode(" ")
) as HASTElement
);
} else {
helper(child as HASTParent);
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/api/parsers/html/util/nestedLists.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import rehypeFormat from "rehype-format";
import rehypeParse from "rehype-parse";
import rehypeStringify from "rehype-stringify";
import { unified } from "unified";
import { describe, expect, it } from "vitest";
import { initializeESMDependencies } from "../../../../util/esmDependencies";
import { nestedListsToBlockNoteStructure } from "./nestedLists";

async function testHTML(html: string) {
const deps = await initializeESMDependencies();

const htmlNode = nestedListsToBlockNoteStructure(html);

const pretty = await unified()
.use(rehypeParse, { fragment: true })
.use(rehypeFormat)
.use(rehypeStringify)
const pretty = await deps.unified
.unified()
.use(deps.rehypeParse.default, { fragment: true })
.use(deps.rehypeFormat.default)
.use(deps.rehypeStringify.default)
.process(htmlNode.innerHTML);

expect(pretty.value).toMatchSnapshot();
Expand Down
Loading