Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
22 changes: 21 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,29 @@ To run the project, open the command line in the project's root directory and en
pnpm install

# Start the example project
pnpm start
pnpm dev
```

## Commands

All commands are run from the project root with [`pnpm`](https://pnpm.io), which
wraps the `vp` ([vite-plus](https://vite-plus.dev)) task runner. The ones you'll
use day to day:

| Command | Description |
| ---------------- | ---------------------------------------------------------- |
| `pnpm install` | Install all dependencies. |
| `pnpm dev` | Start the example editor with live reload. |
| `pnpm start` | Build the packages, then preview the example editor. |
| `pnpm test` | Run the unit tests across all packages. |
| `pnpm lint` | Lint and type-check the codebase. Run this before pushing. |
| `pnpm run check` | Auto-fix lint and formatting issues across the project. |
| `pnpm build` | Build all packages. |
| `pnpm e2e` | Run the Playwright end-to-end tests. |

To run the unit tests for a single package, run `pnpm test` from inside that
package's directory; append `-u` to update snapshots.

## Adding packages

- Add the dependency to the relevant `package.json` file (packages/xxx/package.json)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function setTextCursorPosition(

const info = getBlockInfo(posInfo);

const contentType: "none" | "inline" | "table" =
const contentType: "none" | "inline" | "table" | "plain" =
schema.blockSchema[info.blockNoteType]!.content;

if (info.isBlockContainer) {
Expand All @@ -81,7 +81,7 @@ export function setTextCursorPosition(
return;
}

if (contentType === "inline") {
if (contentType === "inline" || contentType === "plain") {
if (placement === "start") {
tr.setSelection(
TextSelection.create(tr.doc, blockContent.beforePos + 1),
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/api/nodeConversions/nodeToBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,11 @@ export function nodeToBlock<
inlineContentSchema,
styleSchema,
);
} else if (blockConfig.content === "plain") {
if (!blockInfo.isBlockContainer) {
throw new Error("impossible");
}
content = blockInfo.blockContent.node.textContent;
} else if (blockConfig.content === "none") {
content = undefined;
} else {
Expand Down
14 changes: 6 additions & 8 deletions packages/core/src/blocks/Code/block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe("Code block input rule", () => {

const block = editor.document[0];
expect(block.type).toBe("codeBlock");
expect(block.content).toEqual([]);
expect(block.content).toBe("");
});

it("converts ```ts + Enter into a codeBlock", () => {
Expand All @@ -135,7 +135,7 @@ describe("Code block input rule", () => {
const block = editor.document[0];
expect(block.type).toBe("codeBlock");
expect((block.props as any).language).toBe("ts");
expect(block.content).toEqual([]);
expect(block.content).toBe("");
});

it("converts ``` + Enter into a codeBlock with empty language", () => {
Expand Down Expand Up @@ -186,9 +186,8 @@ describe("Code block input rule", () => {
const after = editor.document[0];
expect(after.type).toBe("codeBlock");
expect(after.id).toBe(block.id);
expect(
(after.content as Array<{ type: string; text: string }>)[0].text,
).toBe("hello");
// The code block holds plain (string) content.
expect(after.content).toBe("hello");
});

it("places cursor inside the new code block after Enter conversion", () => {
Expand All @@ -205,9 +204,8 @@ describe("Code block input rule", () => {
const after = editor.document[0];
expect(after.type).toBe("codeBlock");
expect(after.id).toBe(block.id);
expect(
(after.content as Array<{ type: string; text: string }>)[0].text,
).toBe("world");
// The code block holds plain (string) content.
expect(after.content).toBe("world");
});

it("Enter inside an existing code block does not retrigger conversion", () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/blocks/Code/block.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { HighlighterGeneric } from "@shikijs/types";
import { DOMParser } from "@tiptap/pm/model";
import { createExtension } from "../../editor/BlockNoteExtension.js";
import { createBlockConfig, createBlockSpec } from "../../schema/index.js";
import { lazyShikiPlugin } from "./shiki.js";
import { DOMParser } from "@tiptap/pm/model";

export type CodeBlockOptions = {
/**
Expand Down Expand Up @@ -62,7 +62,7 @@ export const createCodeBlockConfig = createBlockConfig(
default: defaultLanguage,
},
},
content: "inline",
content: "plain",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}) as const,
);

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/comments/mark.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Mark, mergeAttributes } from "@tiptap/core";

import { NON_FORMATTING_MARK_GROUP } from "../schema/markGroups.js";

export const CommentMark = Mark.create({
name: "comment",
excludes: "",
inclusive: false,
keepOnSplit: true,
// Allowed on "plain" blocks (e.g. code blocks) via this group.
group: NON_FORMATTING_MARK_GROUP,

addAttributes() {
// Return an object with attribute configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Mark } from "@tiptap/core";
import { MarkSpec } from "prosemirror-model";

import { NON_FORMATTING_MARK_GROUP } from "../../../schema/markGroups.js";

// This copies the marks from @handlewithcare/prosemirror-suggest-changes,
// but uses the Tiptap Mark API instead so we can use them in BlockNote

Expand All @@ -10,6 +12,7 @@ export const SuggestionAddMark = Mark.create({
name: "insertion",
inclusive: false,
excludes: "deletion modification insertion",
group: NON_FORMATTING_MARK_GROUP,
addAttributes() {
return {
id: { default: null, validate: "number" }, // note: validate is supported in prosemirror but not in tiptap, so this doesn't actually work (considered not critical)
Expand Down Expand Up @@ -55,6 +58,7 @@ export const SuggestionDeleteMark = Mark.create({
name: "deletion",
inclusive: false,
excludes: "insertion modification deletion",
group: NON_FORMATTING_MARK_GROUP,
addAttributes() {
return {
id: { default: null, validate: "number" }, // note: validate is supported in prosemirror but not in tiptap
Expand Down Expand Up @@ -103,6 +107,7 @@ export const SuggestionModificationMark = Mark.create({
name: "modification",
inclusive: false,
excludes: "deletion insertion",
group: NON_FORMATTING_MARK_GROUP,
addAttributes() {
// note: validate is supported in prosemirror but not in tiptap
return {
Expand Down
47 changes: 37 additions & 10 deletions packages/core/src/schema/blocks/createSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Extension,
ExtensionFactoryInstance,
} from "../../editor/BlockNoteExtension.js";
import { NON_FORMATTING_MARK_GROUP } from "../markGroups.js";
import { PropSchema } from "../propTypes.js";
import {
getBlockFromPos,
Expand Down Expand Up @@ -44,7 +45,7 @@ export function applyNonSelectableBlockFix(nodeView: NodeView, editor: Editor) {
export function getParseRules<
TName extends string,
TProps extends PropSchema,
TContent extends "inline" | "none" | "table",
TContent extends "inline" | "none" | "table" | "plain",
>(
config: BlockConfig<TName, TProps, TContent>,
implementation: BlockImplementation<TName, TProps, TContent>,
Expand Down Expand Up @@ -75,7 +76,9 @@ export function getParseRules<
// Because we do the parsing ourselves, we want to preserve whitespace for content we've parsed
preserveWhitespace: true,
getContent:
config.content === "inline" || config.content === "none"
config.content === "inline" ||
config.content === "none" ||
config.content === "plain"
? (node, schema) => {
if (implementation.parseContent) {
const result = implementation.parseContent({
Expand All @@ -89,6 +92,13 @@ export function getParseRules<
}
}

if (config.content === "plain") {
// Plain blocks hold unstyled text only, so we parse the
// element's text content directly into a single text node.
const text = (node as HTMLElement).textContent ?? "";
return text ? Fragment.from(schema.text(text)) : Fragment.empty;
Comment thread
YousefED marked this conversation as resolved.
Outdated
}

if (config.content === "inline") {
// Parse the inline content if it exists
const element = node as HTMLElement;
Expand Down Expand Up @@ -140,7 +150,7 @@ export function getParseRules<
export function addNodeAndExtensionsToSpec<
TName extends string,
TProps extends PropSchema,
TContent extends "inline" | "none" | "table",
TContent extends "inline" | "none" | "table" | "plain",
>(
blockConfig: BlockConfig<TName, TProps, TContent>,
blockImplementation: BlockImplementation<TName, TProps, TContent>,
Expand All @@ -153,9 +163,22 @@ export function addNodeAndExtensionsToSpec<
name: blockConfig.type,
content: (blockConfig.content === "inline"
? "inline*"
: blockConfig.content === "none"
? ""
: blockConfig.content) as TContent extends "inline" ? "inline*" : "",
: blockConfig.content === "plain"
? "text*"
Comment thread
YousefED marked this conversation as resolved.
: blockConfig.content === "none"
? ""
: blockConfig.content) as TContent extends "inline"
? "inline*"
: TContent extends "plain"
? "text*"
: "",
// "plain" blocks hold unstyled text, so they disallow formatting marks.
// They still allow the non-formatting marks (comments and
// suggestions/diffs) — those annotate content without changing it and are
// ignored by the block model. The group's always-present suggestion marks
// keep this reference valid even when comments aren't configured.
marks:
blockConfig.content === "plain" ? NON_FORMATTING_MARK_GROUP : undefined,
group: "blockContent",
selectable: blockImplementation.meta?.selectable ?? true,
isolating: blockImplementation.meta?.isolating ?? true,
Expand All @@ -180,7 +203,11 @@ export function addNodeAndExtensionsToSpec<
return wrapInBlockStructure(
{
dom: div,
contentDOM: blockConfig.content === "inline" ? div : undefined,
contentDOM:
blockConfig.content === "inline" ||
blockConfig.content === "plain"
? div
: undefined,
},
blockConfig.type,
{},
Expand Down Expand Up @@ -304,7 +331,7 @@ export function createBlockConfig<
export function createBlockSpec<
const TName extends string,
const TProps extends PropSchema,
const TContent extends "inline" | "none",
const TContent extends "inline" | "none" | "plain",
const TOptions extends Partial<Record<string, any>> | undefined = undefined,
>(
blockConfigOrCreator: BlockConfig<TName, TProps, TContent>,
Expand All @@ -323,7 +350,7 @@ export function createBlockSpec<
export function createBlockSpec<
const TName extends string,
const TProps extends PropSchema,
const TContent extends "inline" | "none",
const TContent extends "inline" | "none" | "plain",
const BlockConf extends BlockConfig<TName, TProps, TContent>,
const TOptions extends Partial<Record<string, any>>,
>(
Expand All @@ -349,7 +376,7 @@ export function createBlockSpec<
export function createBlockSpec<
const TName extends string,
const TProps extends PropSchema,
const TContent extends "inline" | "none",
const TContent extends "inline" | "none" | "plain",
const TOptions extends Partial<Record<string, any>> | undefined = undefined,
>(
blockConfigOrCreator: BlockConfigOrCreator<TName, TProps, TContent, TOptions>,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/schema/blocks/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export function createBlockSpecFromTiptapNode<
const T extends {
node: Node;
type: string;
content: "inline" | "table" | "none";
content: "inline" | "table" | "none" | "plain";
},
P extends PropSchema,
>(
Expand Down
Loading
Loading