Skip to content

Commit 92952e7

Browse files
committed
feat: getContent parsing for custom blocks
1 parent 6b10846 commit 92952e7

File tree

7 files changed

+73
-32
lines changed

7 files changed

+73
-32
lines changed

packages/core/src/blks/BulletListItem/definition.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { updateBlockTr } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js";
22
import { getBlockInfoFromTransaction } from "../../api/getBlockInfoFromPos.js";
33
import { defaultProps } from "../../blocks/defaultProps.js";
4+
import { getListItemContent } from "../../blocks/ListItemBlockContent/getListItemContent.js";
45
import {
56
createBlockConfig,
67
createBlockNoteExtension,
@@ -38,12 +39,10 @@ export const definition = createBlockSpec(config).implementation(
3839

3940
return false;
4041
},
41-
// TODO how do we represent this??
42-
// // As `li` elements can contain multiple paragraphs, we need to merge their contents
43-
// // into a single one so that ProseMirror can parse everything correctly.
44-
// getContent: (node, schema) =>
45-
// getListItemContent(node, schema, this.name),
46-
// node: "bulletListItem",
42+
// As `li` elements can contain multiple paragraphs, we need to merge their contents
43+
// into a single one so that ProseMirror can parse everything correctly.
44+
parseContent: ({ el, schema }) =>
45+
getListItemContent(el, schema, "bulletListItem"),
4746
render() {
4847
const div = document.createElement("div");
4948
// We use a <p> tag, because for <li> tags we'd need a <ul> element to put

packages/core/src/blks/CheckListItem/definition.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { updateBlockTr } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js";
22
import { getBlockInfoFromTransaction } from "../../api/getBlockInfoFromPos.js";
33
import { defaultProps } from "../../blocks/defaultProps.js";
4+
import { getListItemContent } from "../../blocks/ListItemBlockContent/getListItemContent.js";
45
import {
56
createBlockConfig,
67
createBlockNoteExtension,
@@ -58,12 +59,10 @@ export const definition = createBlockSpec(config).implementation(
5859

5960
return;
6061
},
61-
// TODO how do we represent this??
62-
// // As `li` elements can contain multiple paragraphs, we need to merge their contents
63-
// // into a single one so that ProseMirror can parse everything correctly.
64-
// getContent: (node, schema) =>
65-
// getListItemContent(node, schema, this.name),
66-
// node: "bulletListItem",
62+
// As `li` elements can contain multiple paragraphs, we need to merge their contents
63+
// into a single one so that ProseMirror can parse everything correctly.
64+
parseContent: ({ el, schema }) =>
65+
getListItemContent(el, schema, "checkListItem"),
6766
render(block) {
6867
const div = document.createElement("div");
6968
const checkbox = document.createElement("input");
@@ -116,7 +115,6 @@ export const definition = createBlockSpec(config).implementation(
116115
{
117116
find: new RegExp(`\\[\\s*\\]\\s$`),
118117
replace() {
119-
console.log("trigger");
120118
return {
121119
type: "checkListItem",
122120
props: {

packages/core/src/blks/Code/definition.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ export const definition = createBlockSpec(config).implementation(
8080
return {};
8181
},
8282

83-
// TODO parsecontent
84-
8583
render(block, editor) {
8684
const wrapper = document.createDocumentFragment();
8785
const pre = document.createElement("pre");

packages/core/src/blks/NumberedListItem/definition.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { updateBlockTr } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js";
22
import { getBlockInfoFromTransaction } from "../../api/getBlockInfoFromPos.js";
33
import { defaultProps } from "../../blocks/defaultProps.js";
4+
import { getListItemContent } from "../../blocks/ListItemBlockContent/getListItemContent.js";
45
import {
56
createBlockConfig,
67
createBlockNoteExtension,
@@ -40,12 +41,10 @@ export const definition = createBlockSpec(config).implementation(
4041

4142
return false;
4243
},
43-
// TODO how do we represent this??
44-
// // As `li` elements can contain multiple paragraphs, we need to merge their contents
45-
// // into a single one so that ProseMirror can parse everything correctly.
46-
// getContent: (node, schema) =>
47-
// getListItemContent(node, schema, this.name),
48-
// node: "bulletListItem",
44+
// As `li` elements can contain multiple paragraphs, we need to merge their contents
45+
// into a single one so that ProseMirror can parse everything correctly.
46+
parseContent: ({ el, schema }) =>
47+
getListItemContent(el, schema, "numberedListItem"),
4948
render() {
5049
const div = document.createElement("div");
5150
// We use a <p> tag, because for <li> tags we'd need a <ul> element to put

packages/core/src/blocks/defaultBlockHelpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,13 @@ export const defaultBlockToHTML = <
100100
// This is used when parsing blocks like list items and table cells, as they may
101101
// contain multiple paragraphs that ProseMirror will not be able to handle
102102
// properly.
103-
export function mergeParagraphs(element: HTMLElement) {
103+
export function mergeParagraphs(element: HTMLElement, separator = "<br>") {
104104
const paragraphs = element.querySelectorAll("p");
105105
if (paragraphs.length > 1) {
106106
const firstParagraph = paragraphs[0];
107107
for (let i = 1; i < paragraphs.length; i++) {
108108
const paragraph = paragraphs[i];
109-
firstParagraph.innerHTML += "<br>" + paragraph.innerHTML;
109+
firstParagraph.innerHTML += separator + paragraph.innerHTML;
110110
paragraph.remove();
111111
}
112112
}

packages/core/src/schema/blocks/createSpec.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Editor } from "@tiptap/core";
2-
import { TagParseRule } from "@tiptap/pm/model";
2+
import { DOMParser, Fragment, TagParseRule } from "@tiptap/pm/model";
33
import { NodeView, ViewMutationRecord } from "@tiptap/pm/view";
4+
import { mergeParagraphs } from "../../blocks/defaultBlockHelpers.js";
45
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
56
import { InlineContentSchema } from "../inlineContent/types.js";
67
import { StyleSchema } from "../styles/types.js";
@@ -87,6 +88,11 @@ export function applyNonSelectableBlockFix(nodeView: NodeView, editor: Editor) {
8788
export function getParseRules(
8889
config: BlockConfig,
8990
customParseFunction: CustomBlockImplementation<any, any, any>["parse"],
91+
customParseContentFunction: CustomBlockImplementation<
92+
any,
93+
any,
94+
any
95+
>["parseContent"],
9096
) {
9197
const rules: TagParseRule[] = [
9298
{
@@ -111,6 +117,37 @@ export function getParseRules(
111117

112118
return props;
113119
},
120+
getContent:
121+
config.content === "inline" || config.content === "none"
122+
? (node, schema) => {
123+
if (customParseContentFunction) {
124+
return customParseContentFunction({
125+
el: node,
126+
schema,
127+
});
128+
}
129+
130+
if (config.content === "inline") {
131+
// Parse the blockquote content as inline content
132+
const element = node as HTMLElement;
133+
134+
// Clone to avoid modifying the original
135+
const clone = element.cloneNode(true) as HTMLElement;
136+
137+
// Merge multiple paragraphs into one with line breaks
138+
mergeParagraphs(clone, config.meta?.code ? "\n" : "<br>");
139+
140+
// Parse the content directly as a paragraph to extract inline content
141+
const parser = DOMParser.fromSchema(schema);
142+
const parsed = parser.parse(clone, {
143+
topNode: schema.nodes.paragraph.create(),
144+
});
145+
146+
return parsed.content;
147+
}
148+
return Fragment.empty;
149+
}
150+
: undefined,
114151
});
115152
}
116153
// getContent(node, schema) {
@@ -147,19 +184,27 @@ export function createBlockSpec<
147184
name: blockConfig.type as T["type"],
148185
content: (blockConfig.content === "inline"
149186
? "inline*"
150-
: "") as T["content"] extends "inline" ? "inline*" : "",
187+
: blockConfig.content === "none"
188+
? ""
189+
: blockConfig.content) as T["content"] extends "inline"
190+
? "inline*"
191+
: "",
151192
group: "blockContent",
152-
selectable: blockConfig.isSelectable ?? true,
193+
selectable: blockConfig.meta?.selectable ?? true,
153194
isolating: true,
154195
code: blockConfig.meta?.code ?? false,
155-
defining: blockConfig.meta?.defining ?? false,
196+
defining: blockConfig.meta?.defining ?? true,
156197
priority,
157198
addAttributes() {
158199
return propsToAttributes(blockConfig.propSchema);
159200
},
160201

161202
parseHTML() {
162-
return getParseRules(blockConfig, blockImplementation.parse);
203+
return getParseRules(
204+
blockConfig,
205+
blockImplementation.parse,
206+
(blockImplementation as any).parseContent,
207+
);
163208
},
164209

165210
renderHTML({ HTMLAttributes }) {

packages/core/src/schema/blocks/playground.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Props, PropSchema } from "../../schema/index.js";
33
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
44
import { Block } from "../../blocks/defaultBlocks.js";
55
import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
6+
import { Fragment, Schema } from "prosemirror-model";
67

78
export type BlockDefs = Record<string, BlockConfig<string, PropSchema>>;
89

@@ -105,10 +106,11 @@ export interface BlockImplementation<
105106
*/
106107
runsBefore?: string[];
107108

108-
// TODO there needs to be simper way to do this, it is a bit of a gap to force them to bridge html to block content
109-
// parseContent?: (
110-
// el: HTMLElement,
111-
// ) => Block<Record<TName, BlockConfig<TName, TProps>>>["children"];
109+
/**
110+
* Advanced parsing function that controls how content within the block is parsed.
111+
* This is not recommended to use, and is only useful for advanced use cases.
112+
*/
113+
parseContent?: (options: { el: HTMLElement; schema: Schema }) => Fragment;
112114

113115
// // TODO this should be only on extensions, not on the block config
114116
// /**

0 commit comments

Comments
 (0)