diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4f0f140cad..165dad4e01 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,7 +21,7 @@ updates: - dependency-name: "@tiptap/extension-code" - dependency-name: "@tiptap/extension-horizontal-rule" - dependency-name: "@tiptap/extension-italic" - - dependency-name: "@tiptap/extension-link" + - dependency-name: "@tiptap/extension-paragraph" - dependency-name: "@tiptap/extension-strike" - dependency-name: "@tiptap/extension-text" @@ -40,6 +40,12 @@ updates: # yjs packages - dependency-name: "yjs" - dependency-name: "y-prosemirror" + ignore: + # Hono packages are used only in the demo AI server and are not part of + # the main editor/runtime surface area. + - dependency-name: "hono" + - dependency-name: "@hono/node-server" + - dependency-name: "@hono/*" groups: editor-dependencies: patterns: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d84674e284..91b5ca0414 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,7 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN }} NX_SELF_HOSTED_REMOTE_CACHE_SERVER: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_SERVER }} + pnpm_config_store_dir: ./node_modules/.pnpm-store jobs: build: diff --git a/.github/workflows/fresh-install-tests.yml b/.github/workflows/fresh-install-tests.yml index a366d7d47a..6d6ed4a452 100644 --- a/.github/workflows/fresh-install-tests.yml +++ b/.github/workflows/fresh-install-tests.yml @@ -1,13 +1,14 @@ name: Fresh Install Tests -# Periodically tests BlockNote with the latest versions of its dependencies -# (within declared ranges), without a lockfile. This catches breakage when a +# Periodically tests BlockNote with the latest versions of its production +# dependencies (within declared semver ranges). This catches breakage when a # new release of a dep like @tiptap/* or prosemirror-* ships and conflicts # with BlockNote's declared ranges — the kind of failure a user would hit when # running `npm install @blocknote/react` in a fresh project. # -# DevDependencies (vitest, vite, typescript, etc.) are still bounded by their -# declared ranges in package.json; only prod/peer deps get freshly resolved. +# Only production dependencies of published (non-private) packages are updated. +# DevDependencies (vitest, vite, typescript, etc.) stay pinned to the lockfile, +# so test tooling churn doesn't cause false positives. on: schedule: @@ -16,6 +17,7 @@ on: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + pnpm_config_store_dir: ./node_modules/.pnpm-store jobs: fresh-install-unit-tests: @@ -24,32 +26,119 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v6 + - id: checkout + uses: actions/checkout@v6 - - name: Install pnpm + - id: install_pnpm + name: Install pnpm uses: pnpm/action-setup@v5 - - uses: actions/setup-node@v6 + - id: setup_node + uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" - # Intentionally no pnpm cache — we want a genuinely fresh install + # Intentionally no pnpm cache — we want fresh prod dep resolution - - name: Remove lockfile to force fresh dep resolution - # Removing pnpm-lock.yaml causes pnpm to resolve all dependencies to - # the latest versions that satisfy the ranges declared in package.json - # (including pnpm-workspace.yaml overrides). This is equivalent to what - # a new user experiences when installing BlockNote in a blank project. - run: rm pnpm-lock.yaml + - id: install_dependencies + name: Install dependencies + run: pnpm install - - name: Install dependencies - run: pnpm install --no-frozen-lockfile + - id: update_prod_deps + name: Update prod deps of published packages + # Resolves production dependencies of every published (non-private) + # workspace package to the latest version within their declared semver + # ranges. This simulates what a user gets when running + # `npm install @blocknote/react` in a fresh project. + # DevDependencies are left at their lockfile versions. + run: | + FILTERS=$(node -e " + const fs = require('fs'); + const path = require('path'); + fs.readdirSync('packages').forEach(dir => { + try { + const pkg = JSON.parse(fs.readFileSync(path.join('packages', dir, 'package.json'), 'utf8')); + if (!pkg.private && pkg.name) process.stdout.write('--filter ' + pkg.name + ' '); + } catch {} + }); + ") + echo "Updating prod deps for: $FILTERS" + eval pnpm update --prod $FILTERS - - name: Build packages + - id: dedupe_deps + name: Dedupe transitive dependencies + # After bumping the publishable packages' prod deps, collapse any + # duplicate transitive resolutions (e.g. @tiptap/core + @tiptap/pm) + # that would otherwise differ between the updated publishable packages + # and the un-updated examples/playground. Without this, TypeScript + # treats the two copies' exports as unrelated types and example-editor + # fails to build (TS2322 on Extension vs AnyExtension). + # Dedupe only rewrites the lockfile — it does NOT modify package.json, + # so the examples' "@blocknote/*": "latest" specs (which is what + # CodeSandbox users see) stay intact. + run: pnpm dedupe + + - id: build_packages + name: Build packages run: pnpm run build env: NX_SKIP_NX_CACHE: "true" - - name: Run unit tests + - id: run_unit_tests + name: Run unit tests run: pnpm run test env: NX_SKIP_NX_CACHE: "true" + + - name: Notify Slack on workflow failure + if: ${{ failure() }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + REPOSITORY: ${{ github.repository }} + WORKFLOW: ${{ github.workflow }} + RUN_ID: ${{ github.run_id }} + RUN_NUMBER: ${{ github.run_number }} + RUN_ATTEMPT: ${{ github.run_attempt }} + BRANCH: ${{ github.ref_name }} + run: | + if [ -z "$SLACK_WEBHOOK_URL" ]; then + echo "SLACK_WEBHOOK_URL is not configured; skipping Slack notification." + exit 0 + fi + + failed_step="Unknown step" + if [ "${{ steps.checkout.outcome }}" = "failure" ]; then + failed_step="Checkout repository" + elif [ "${{ steps.install_pnpm.outcome }}" = "failure" ]; then + failed_step="Install pnpm" + elif [ "${{ steps.setup_node.outcome }}" = "failure" ]; then + failed_step="Setup Node.js" + elif [ "${{ steps.install_dependencies.outcome }}" = "failure" ]; then + failed_step="Install dependencies" + elif [ "${{ steps.update_prod_deps.outcome }}" = "failure" ]; then + failed_step="Update prod deps of published packages" + elif [ "${{ steps.dedupe_deps.outcome }}" = "failure" ]; then + failed_step="Dedupe transitive dependencies" + elif [ "${{ steps.build_packages.outcome }}" = "failure" ]; then + failed_step="Build packages" + elif [ "${{ steps.run_unit_tests.outcome }}" = "failure" ]; then + failed_step="Run unit tests" + fi + + run_url="https://github.com/${REPOSITORY}/actions/runs/${RUN_ID}" + message=$(printf '%s\n%s\n%s\n%s' \ + ":warning: Fresh Install Tests failed in *${REPOSITORY}* on branch *${BRANCH}*." \ + "*Workflow:* ${WORKFLOW}" \ + "*Run:* <${run_url}|#${RUN_NUMBER} (attempt ${RUN_ATTEMPT})>" \ + "*Failed step:* ${failed_step}") + payload=$(jq --compact-output --null-input --arg text "$message" '{text: $text}') + + curl -sS -X POST \ + --fail \ + --retry 4 \ + --retry-all-errors \ + --retry-max-time 60 \ + --connect-timeout 10 \ + --max-time 30 \ + -H "Content-type: application/json" \ + --data "$payload" \ + "$SLACK_WEBHOOK_URL" diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index df34bafa81..280d5a5af1 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -17,6 +17,7 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN }} NX_SELF_HOSTED_REMOTE_CACHE_SERVER: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_SERVER }} + pnpm_config_store_dir: ./node_modules/.pnpm-store jobs: publish: diff --git a/CHANGELOG.md b/CHANGELOG.md index 85c250a9b8..112608f8ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,112 @@ +## 0.51.0 (2026-05-14) + +### 🚀 Features + +- Trailing block extension rewrite ([#2733](https://github.com/TypeCellOS/BlockNote/pull/2733)) +- **markdown:** replace unified.js with custom markdown parser/serializer ([#2624](https://github.com/TypeCellOS/BlockNote/pull/2624)) +- **react:** configurable portal targets for floating UI ([#2729](https://github.com/TypeCellOS/BlockNote/pull/2729), [#2692](https://github.com/TypeCellOS/BlockNote/issues/2692)) + +### 🩹 Fixes + +- Pasting plain text from VSCode (BLO-366) ([#2713](https://github.com/TypeCellOS/BlockNote/pull/2713)) +- Parse new lines in `text/plain` as line breaks (BLO-1170) ([#2712](https://github.com/TypeCellOS/BlockNote/pull/2712)) +- Code block PDF export (BLO-987) ([#2725](https://github.com/TypeCellOS/BlockNote/pull/2725)) +- Formatting toolbar opening when inserting file block with `trailingBlock: false` (BLO-860) ([#2704](https://github.com/TypeCellOS/BlockNote/pull/2704)) +- numbered list item decorations missed on initial render ([#2734](https://github.com/TypeCellOS/BlockNote/pull/2734)) +- flicker-free mobile formatting toolbar via CSS custom properties ([#2617](https://github.com/TypeCellOS/BlockNote/pull/2617), [#2616](https://github.com/TypeCellOS/BlockNote/issues/2616)) +- add `bn-thread-orphaned` CSS class to distinguish orphaned threads ([#2737](https://github.com/TypeCellOS/BlockNote/pull/2737), [#2735](https://github.com/TypeCellOS/BlockNote/issues/2735)) +- set width attribute on image and video elements in editor render ([#2740](https://github.com/TypeCellOS/BlockNote/pull/2740), [#2726](https://github.com/TypeCellOS/BlockNote/issues/2726)) +- **a11y:** use figure/figcaption for media block captions ([#2717](https://github.com/TypeCellOS/BlockNote/pull/2717)) +- **ai:** loosen serialization of blocks in columns ([#2716](https://github.com/TypeCellOS/BlockNote/pull/2716), [#2718](https://github.com/TypeCellOS/BlockNote/pull/2718)) +- **core:** trigger codeblock input rule on Enter and place cursor inside ([#2686](https://github.com/TypeCellOS/BlockNote/pull/2686)) +- **core:** preserve list item type when pasting into empty list items ([#2722](https://github.com/TypeCellOS/BlockNote/pull/2722), [#2330](https://github.com/TypeCellOS/BlockNote/issues/2330)) +- **core:** unmount editors in transformPasted tests to prevent unhandled error ([e62880b21](https://github.com/TypeCellOS/BlockNote/commit/e62880b21)) +- **drag-n-drop:** support PDF block drag & drop (BLO-893) ([#2714](https://github.com/TypeCellOS/BlockNote/pull/2714)) +- **i18:** improve french translation for empty toggle list ([#2721](https://github.com/TypeCellOS/BlockNote/pull/2721)) +- **markdown:** emit tight lists when serializing blocks to markdown ([#2715](https://github.com/TypeCellOS/BlockNote/pull/2715)) +- **markdown:** skip placeholder text for empty files ([#434](https://github.com/TypeCellOS/BlockNote/pull/434), [#2719](https://github.com/TypeCellOS/BlockNote/pull/2719)) +- **markdown:** stable round-trip for tables, captions, and audio ([#2720](https://github.com/TypeCellOS/BlockNote/pull/2720)) +- **tests:** stabilize webkit keyboard handler tests with programmatic cursor positioning ([#2746](https://github.com/TypeCellOS/BlockNote/pull/2746)) + +### ❤️ Thank You + +- Cyril G +- Manuel Raynaud @lunika +- Matthew Lipski @matthewlipski +- Movm +- Nick Perez +- Nick the Sick @nperez0111 + +## 0.50.0 (2026-05-04) + +### 🚀 Features + +- Dark mode styling for file block wrapper component (BLO-866) ([#2680](https://github.com/TypeCellOS/BlockNote/pull/2680)) +- Drag hendle menu delete button removes all other blocks in selection (BLO-1007) ([#2683](https://github.com/TypeCellOS/BlockNote/pull/2683)) +- Enter moves selection to cell below in tables (BLO-1006) ([#2685](https://github.com/TypeCellOS/BlockNote/pull/2685)) +- additional heading top padding (BLO-1008) ([#2690](https://github.com/TypeCellOS/BlockNote/pull/2690)) +- Code mark input rule edge case (BLO-938) ([#2698](https://github.com/TypeCellOS/BlockNote/pull/2698)) +- **mantine:** upgrade @mantine/core and @mantine/hooks to v9.0.2 ([#2655](https://github.com/TypeCellOS/BlockNote/pull/2655)) + +### 🩹 Fixes + +- Hardcoded strings in comment components (BLO-1033) ([#2681](https://github.com/TypeCellOS/BlockNote/pull/2681)) +- Color naming & CSS (BLO-946) ([#2684](https://github.com/TypeCellOS/BlockNote/pull/2684)) +- link HTML attributes (BLO-915) ([#2687](https://github.com/TypeCellOS/BlockNote/pull/2687)) +- guard hideMenuIfNotFrozen against undefined view state ([#2694](https://github.com/TypeCellOS/BlockNote/pull/2694), [#2699](https://github.com/TypeCellOS/BlockNote/pull/2699)) +- Clicking comment overlapping link opens link (BLO-1091) ([#2696](https://github.com/TypeCellOS/BlockNote/pull/2696)) +- prevent table row drag from moving an extra adjacent row ([#2703](https://github.com/TypeCellOS/BlockNote/pull/2703)) +- **clipboard:** use ProseMirror selection state for Shadow DOM compatibility ([#2677](https://github.com/TypeCellOS/BlockNote/pull/2677)) + +### ❤️ Thank You + +- jt_fox @LimChaeJune +- Matthew Lipski @matthewlipski +- Nick Perez +- Wieland Lindenthal +- Yousef + +## 0.49.0 (2026-04-24) + +### 🚀 Features + +- simplify links by inlining it to BlockNote ([#2623](https://github.com/TypeCellOS/BlockNote/pull/2623)) +- add Unicode quotation mark input rule for quote blocks ([#2673](https://github.com/TypeCellOS/BlockNote/pull/2673)) + +### 🩹 Fixes + +- Inserting link removes comment & add comment button click buggy ([#2620](https://github.com/TypeCellOS/BlockNote/pull/2620), [#2573](https://github.com/TypeCellOS/BlockNote/issues/2573)) +- `useEditorDOMElement` hook ([#2619](https://github.com/TypeCellOS/BlockNote/pull/2619)) +- text color was not applying to table block ([#2663](https://github.com/TypeCellOS/BlockNote/pull/2663)) +- Drag preview blocking drops when overlapping the editor (BLO-996) ([#2670](https://github.com/TypeCellOS/BlockNote/pull/2670)) +- Drag & drop of blocks without inline content opens formatting toolbar (BLO-1116) ([#2628](https://github.com/TypeCellOS/BlockNote/pull/2628), [#2603](https://github.com/TypeCellOS/BlockNote/issues/2603)) +- save file caption/name on every keystroke instead of on close ([#2575](https://github.com/TypeCellOS/BlockNote/pull/2575)) +- prevent FloatingFocusManager from resetting editor selection ([#2525](https://github.com/TypeCellOS/BlockNote/pull/2525), [#2664](https://github.com/TypeCellOS/BlockNote/pull/2664)) + +### ❤️ Thank You + +- Matthew Lipski @matthewlipski +- miadnguyen @miadnguyen +- mianguyen +- Nick Perez +- Yousef + +## 0.48.1 (2026-04-16) + +### 🩹 Fixes + +- make CustomChange compatible with prosemirror-changeset 2.4.1 ([#2647](https://github.com/TypeCellOS/BlockNote/pull/2647)) +- **deps:** upgrade nx to 22.6.5 to resolve axios security vulnerability (CVE-2025-62718) ([c1ef3018a](https://github.com/TypeCellOS/BlockNote/commit/c1ef3018a)) +- **deps:** upgrade nx to 22.6.5 to resolve axios security vulnerability ([#2653](https://github.com/TypeCellOS/BlockNote/pull/2653)) +- **docx-exporter:** omit w:lang when no locale provided instead of defaulting to en-US ([#2651](https://github.com/TypeCellOS/BlockNote/pull/2651)) + +### ❤️ Thank You + +- Claude Opus 4.6 (1M context) +- Nick Perez +- Nick the Sick +- Stephan Meijer @StephanMeijer + ## 0.48.0 (2026-04-13) ### 🚀 Features diff --git a/docs/content/docs/features/blocks/inline-content.mdx b/docs/content/docs/features/blocks/inline-content.mdx index ca7799d841..a22e93f19c 100644 --- a/docs/content/docs/features/blocks/inline-content.mdx +++ b/docs/content/docs/features/blocks/inline-content.mdx @@ -79,6 +79,55 @@ type Link = { }; ``` +### Customizing Links + +You can customize how links are rendered and how they respond to clicks with the `links` editor option. + +```ts +const editor = BlockNoteEditor.create({ + links: { + HTMLAttributes: { + class: "my-link-class", + target: "_blank", + }, + onClick: (event) => { + // Custom click logic, e.g. routing without a page reload. + }, + }, +}); +``` + +#### `HTMLAttributes` + +Additional HTML attributes that should be added to rendered link elements. + +```ts +const editor = BlockNoteEditor.create({ + links: { + HTMLAttributes: { + class: "my-link-class", + target: "_blank", + }, + }, +}); +``` + +#### `onClick` + +Custom handler invoked when a link is clicked. If left `undefined`, links are opened in a new window on click (the default behavior). If provided, that default behavior is disabled and this function is called instead. + +Returning `false` will let BlockNote run other click handlers after this one. Returning `true` or nothing (the default) marks the event as handled. + +```ts +const editor = BlockNoteEditor.create({ + links: { + onClick: (event) => { + // Do something when a link is clicked. + }, + }, +}); +``` + ## Default Styles The default text formatting options in BlockNote are represented by the `Styles` in the default schema: diff --git a/docs/content/docs/features/import/markdown.mdx b/docs/content/docs/features/import/markdown.mdx index e95d71dabd..3106fd8541 100644 --- a/docs/content/docs/features/import/markdown.mdx +++ b/docs/content/docs/features/import/markdown.mdx @@ -15,6 +15,12 @@ imageTitle: Markdown Import BlockNote can import Markdown content into Block objects. Note that this is considered "lossy", as not all Markdown structures can be entirely represented as BlockNote blocks. + + **BlockNote ships a minimal Markdown parser.** It covers the common subset used by most users (CommonMark + GFM basics: headings, paragraphs, lists, task lists, tables, code, blockquotes, links, images, emphasis, strikethrough, hard breaks). + + There are many Markdown specifications (CommonMark, GFM, MDX, Pandoc, and various dialect-specific extensions) and supporting all of them inside a rich text editor is not a goal of BlockNote. **If you need to handle Markdown beyond this minimal subset, parse it to HTML yourself with a parser of your choice (e.g. [`marked`](https://github.com/markedjs/marked), [`markdown-it`](https://github.com/markdown-it/markdown-it), or [`remark`](https://github.com/remarkjs/remark)) and pass the resulting HTML to [`tryParseHTMLToBlocks`](/docs/features/import) instead.** BlockNote's HTML interoperability is much broader, since HTML is the format the editor uses internally for arbitrary pastes. + + ## Markdown to Blocks Use `tryParseMarkdownToBlocks` to try parsing a Markdown string into `Block` objects: diff --git a/docs/content/docs/foundations/supported-formats.mdx b/docs/content/docs/foundations/supported-formats.mdx index 4b5c89e71c..c5e302e19a 100644 --- a/docs/content/docs/foundations/supported-formats.mdx +++ b/docs/content/docs/foundations/supported-formats.mdx @@ -165,6 +165,20 @@ export default function App() { BlockNote also supports converting to and from Markdown. However, converting to and from Markdown is a **lossy** conversion. + + BlockNote ships a **minimal** Markdown parser/serializer that targets the + common CommonMark + GFM subset (headings, paragraphs, lists, task lists, + tables, code, blockquotes, links, images, emphasis, strikethrough, hard + breaks). Supporting every Markdown dialect (CommonMark, GFM, MDX, Pandoc, + and various extensions) is not a goal for the editor. If your use case + requires Markdown features beyond this subset, **parse the Markdown to + HTML yourself** (with a library like [`marked`](https://github.com/markedjs/marked), + [`markdown-it`](https://github.com/markdown-it/markdown-it), or + [`remark`](https://github.com/remarkjs/remark)) and feed the resulting + HTML to `editor.tryParseHTMLToBlocks` — HTML is the format BlockNote uses + for arbitrary pastes and has much broader interoperability. + + ### Saving as Markdown To convert the document to a Markdown string, you can use `editor.blocksToMarkdownLossy()`: diff --git a/docs/content/docs/react/components/index.mdx b/docs/content/docs/react/components/index.mdx index 72deabe9ab..1b6e18d3c0 100644 --- a/docs/content/docs/react/components/index.mdx +++ b/docs/content/docs/react/components/index.mdx @@ -14,3 +14,23 @@ BlockNote includes a number of UI Components (like menus and toolbars) that can {/* - [Image Toolbar](/docs/react/components/image-toolbar) */} + +## Configuring Portal Targets + +By default, all floating UI elements (toolbars, menus, table handles, etc.) portal into the editor's `bn-container` so they stay scoped to the editor. If your layout needs them to escape — e.g. an `overflow: hidden` ancestor that would clip large dropdowns, or a host modal with its own stacking context — pass a `portalElements` prop to `BlockNoteView`: + +```tsx + +``` + +Keys mirror the default UI flags (`formattingToolbar`, `linkToolbar`, `slashMenu`, `emojiPicker`, `sideMenu`, `filePanel`, `tableHandles`, `comments`). Manually-mounted Controllers also accept a `portalElement` prop that takes precedence over the map. See the [Portal Targets example](/examples/ui-components/portal-elements). + +Note: changing `portalElements.default` after mount requires remounting the editor (`editor.mount()` consults it once); per-element keys update reactively. diff --git a/docs/content/docs/react/components/suggestion-menus.mdx b/docs/content/docs/react/components/suggestion-menus.mdx index db9b8421a6..c44f18af45 100644 --- a/docs/content/docs/react/components/suggestion-menus.mdx +++ b/docs/content/docs/react/components/suggestion-menus.mdx @@ -58,6 +58,70 @@ Passing `slashMenu={false}` to `BlockNoteView` tells BlockNote not to show the d `getItems` should return the items that need to be shown in the Slash Menu, based on a `query` entered by the user (anything the user types after the `triggerCharacter`). In this case, we simply append the "Hello World" item to the default Slash Menu items, and use `filterSuggestionItems` to filter the full list of items based on the user query. +### Item Grouping & Ordering + +Slash Menu items are rendered in the same order as the items returned from `getItems`. Adjacent items which share the same `group` attribute are rendered together in the same group under a single label. + +#### Ordering + +Items appear in the menu in the exact order of the array. Reordering the array reorders the menu: + +```typescript +getItems={async (query) => + filterSuggestionItems( + [ + insertHelloWorldItem(editor), // Shown first + ...getDefaultReactSlashMenuItems(editor), // Shown after + ], + query, + ) +} +``` + +#### Grouping + +Items with the same `group` attribute must be **adjacent** in the array to be rendered as one group. If items with the same `group` are separated by items with a different `group`, they will be rendered as two separate groups, each with their own label: + +```typescript +// Renders as a single "Basic" group: +[ + { title: "Item A", group: "Basic", /* ... */ }, + { title: "Item B", group: "Basic", /* ... */ }, + { title: "Item C", group: "Other", /* ... */ }, +] + +// Renders as two separate "Basic" groups, with "Other" between them: +[ + { title: "Item A", group: "Basic", /* ... */ }, + { title: "Item C", group: "Other", /* ... */ }, + { title: "Item B", group: "Basic", /* ... */ }, +] +``` + +#### Finding, Inserting, Removing & Reordering Items + +Use regular array operations to manipulate items. For example, to insert a custom item directly after the default `Heading 1` item: + +```typescript +const items = getDefaultReactSlashMenuItems(editor); +const headingIndex = items.findIndex((item) => item.title === "Heading 1"); +items.splice(headingIndex + 1, 0, insertHelloWorldItem(editor)); +``` + +To remove an item: + +```typescript +const items = getDefaultReactSlashMenuItems(editor).filter( + (item) => item.title !== "Heading 1", +); +``` + +To reorder items, sort or rearrange the array however you'd like before returning it from `getItems`. + +The demo below combines these techniques to render only the "Basic blocks" and "Headings" groups, with their order swapped: + + + ### Replacing the Slash Menu Component You can replace the React component used for the Slash Menu with your own, as you can see in the demo below. diff --git a/docs/content/docs/react/overview.mdx b/docs/content/docs/react/overview.mdx index da09fe2a3c..b85d29ab21 100644 --- a/docs/content/docs/react/overview.mdx +++ b/docs/content/docs/react/overview.mdx @@ -45,8 +45,8 @@ The `` component is used to render the editor. It also provides a ### Props - diff --git a/docs/content/docs/reference/editor/manipulating-content.mdx b/docs/content/docs/reference/editor/manipulating-content.mdx index 6c2df88dc7..1a9c97c222 100644 --- a/docs/content/docs/reference/editor/manipulating-content.mdx +++ b/docs/content/docs/reference/editor/manipulating-content.mdx @@ -252,11 +252,13 @@ editor.replaceBlocks( #### Reordering Blocks ```typescript -moveBlocksUp(): void -moveBlocksDown(): void +moveBlocksUp(blockIdentifier?: BlockIdentifier): void +moveBlocksDown(blockIdentifier?: BlockIdentifier): void ``` -Moves the currently selected blocks up or down in the document. +Moves the currently selected blocks up or down in the document. If a +`blockIdentifier` is provided, that block is moved instead of the selection, +and the selection is left unchanged. ```typescript // Move selected blocks up @@ -264,6 +266,12 @@ editor.moveBlocksUp(); // Move selected blocks down editor.moveBlocksDown(); + +// Move a specific block up, without changing the selection +editor.moveBlocksUp("block-123"); + +// Move a specific block down, without changing the selection +editor.moveBlocksDown("block-123"); ``` ### Nesting Blocks diff --git a/docs/content/docs/reference/editor/overview.mdx b/docs/content/docs/reference/editor/overview.mdx index 5230aeff02..086d91c754 100644 --- a/docs/content/docs/reference/editor/overview.mdx +++ b/docs/content/docs/reference/editor/overview.mdx @@ -113,8 +113,8 @@ editor.pasteMarkdown("# Hello\n\nThis is **bold** text."); The editor can be configured with the following options when using `BlockNoteEditor.create`: - diff --git a/docs/package.json b/docs/package.json index a650645353..3e64e6b44b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -39,9 +39,8 @@ "@liveblocks/react-blocknote": "^3.17.0", "@liveblocks/react-tiptap": "^3.17.0", "@liveblocks/react-ui": "^3.17.0", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "@marsidev/react-turnstile": "^1.4.2", "@mui/icons-material": "^5.16.1", "@mui/material": "^5.16.1", @@ -52,11 +51,11 @@ "@react-email/render": "^2.0.4", "@react-pdf/renderer": "^4.3.0", "@sentry/nextjs": "^10.34.0", - "@shikijs/core": "^3.19.0", - "@shikijs/engine-javascript": "^3.19.0", - "@shikijs/langs-precompiled": "^3.19.0", - "@shikijs/themes": "^3.19.0", - "@shikijs/types": "^3.19.0", + "@shikijs/core": "^4", + "@shikijs/engine-javascript": "^4", + "@shikijs/langs-precompiled": "^4", + "@shikijs/themes": "^4", + "@shikijs/types": "^4", "@tiptap/core": "^3.13.0", "@uppy/core": "^3.13.1", "@uppy/dashboard": "^3.9.1", @@ -83,18 +82,18 @@ "fumadocs-ui": "npm:@fumadocs/base-ui@16.5.0", "lucide-react": "^0.562.0", "motion": "^12.28.1", - "next": "^16.1.6", + "next": "^16.2.6", "next-themes": "^0.4.6", "nodemailer": "^7.0.12", "pg": "^8.17.1", - "react": "^19.2.3", - "react-dom": "^19.2.3", + "react": "^19.2.5", + "react-dom": "^19.2.5", "react-email": "^5.2.5", "react-github-btn": "^1.4.0", "react-icons": "^5.5.0", "react-use-measure": "^2.1.7", "scroll-into-view-if-needed": "^3.1.0", - "shiki": "^3.21.0", + "shiki": "^4", "tailwind-merge": "^3.4.0", "y-partykit": "^0.0.25", "yjs": "^13.6.27", @@ -123,7 +122,7 @@ "@types/react-dom": "^19.2.3", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.39.2", - "eslint-config-next": "^16.1.6", + "eslint-config-next": "^16.2.6", "next-validate-link": "^1.6.4", "postcss": "^8.5.6", "serve": "^14.2.6", diff --git a/examples/01-basic/01-minimal/package.json b/examples/01-basic/01-minimal/package.json index af406c6b8f..26f63572f5 100644 --- a/examples/01-basic/01-minimal/package.json +++ b/examples/01-basic/01-minimal/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/02-block-objects/package.json b/examples/01-basic/02-block-objects/package.json index 8b3ca5bf72..908df7ca16 100644 --- a/examples/01-basic/02-block-objects/package.json +++ b/examples/01-basic/02-block-objects/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/02-block-objects/src/App.tsx b/examples/01-basic/02-block-objects/src/App.tsx index c50da7ddba..c3d623f2e8 100644 --- a/examples/01-basic/02-block-objects/src/App.tsx +++ b/examples/01-basic/02-block-objects/src/App.tsx @@ -26,9 +26,6 @@ export default function App() { type: "paragraph", content: "This is a paragraph block", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/01-basic/03-multi-column/package.json b/examples/01-basic/03-multi-column/package.json index dbcada18f2..2ce018ce9c 100644 --- a/examples/01-basic/03-multi-column/package.json +++ b/examples/01-basic/03-multi-column/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-multi-column": "latest" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/03-multi-column/src/App.tsx b/examples/01-basic/03-multi-column/src/App.tsx index 3a0d10d8d9..c688406214 100644 --- a/examples/01-basic/03-multi-column/src/App.tsx +++ b/examples/01-basic/03-multi-column/src/App.tsx @@ -90,9 +90,6 @@ export default function App() { }, ], }, - { - type: "paragraph", - }, ], }); diff --git a/examples/01-basic/04-default-blocks/package.json b/examples/01-basic/04-default-blocks/package.json index bec32ddecb..8546777d34 100644 --- a/examples/01-basic/04-default-blocks/package.json +++ b/examples/01-basic/04-default-blocks/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/04-default-blocks/src/App.tsx b/examples/01-basic/04-default-blocks/src/App.tsx index d71f676c18..0d55d1af3d 100644 --- a/examples/01-basic/04-default-blocks/src/App.tsx +++ b/examples/01-basic/04-default-blocks/src/App.tsx @@ -145,9 +145,6 @@ export default function App() { }, ], }, - { - type: "paragraph", - }, ], }); diff --git a/examples/01-basic/05-removing-default-blocks/package.json b/examples/01-basic/05-removing-default-blocks/package.json index 4970fe877a..72c8e366f4 100644 --- a/examples/01-basic/05-removing-default-blocks/package.json +++ b/examples/01-basic/05-removing-default-blocks/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/06-block-manipulation/package.json b/examples/01-basic/06-block-manipulation/package.json index 362df04dd3..9f4c9b0764 100644 --- a/examples/01-basic/06-block-manipulation/package.json +++ b/examples/01-basic/06-block-manipulation/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/07-selection-blocks/package.json b/examples/01-basic/07-selection-blocks/package.json index 8bcccee05f..13106a8e6d 100644 --- a/examples/01-basic/07-selection-blocks/package.json +++ b/examples/01-basic/07-selection-blocks/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/07-selection-blocks/src/App.tsx b/examples/01-basic/07-selection-blocks/src/App.tsx index 5251754f5a..811b14e328 100644 --- a/examples/01-basic/07-selection-blocks/src/App.tsx +++ b/examples/01-basic/07-selection-blocks/src/App.tsx @@ -21,9 +21,6 @@ export default function App() { type: "paragraph", content: "Select different blocks to see the JSON change below", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/01-basic/08-ariakit/package.json b/examples/01-basic/08-ariakit/package.json index 75c7713c06..2e9ff90086 100644 --- a/examples/01-basic/08-ariakit/package.json +++ b/examples/01-basic/08-ariakit/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/09-shadcn/package.json b/examples/01-basic/09-shadcn/package.json index 2968b91d44..970aee759f 100644 --- a/examples/01-basic/09-shadcn/package.json +++ b/examples/01-basic/09-shadcn/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "tailwindcss": "^4.1.14", @@ -28,7 +27,7 @@ "@tailwindcss/vite": "^4.1.14", "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/10-localization/package.json b/examples/01-basic/10-localization/package.json index 39828f74fd..5431f4de04 100644 --- a/examples/01-basic/10-localization/package.json +++ b/examples/01-basic/10-localization/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/11-custom-placeholder/package.json b/examples/01-basic/11-custom-placeholder/package.json index 4cde94a58d..b3ca292bfb 100644 --- a/examples/01-basic/11-custom-placeholder/package.json +++ b/examples/01-basic/11-custom-placeholder/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/12-multi-editor/package.json b/examples/01-basic/12-multi-editor/package.json index ab6428dc8b..2408f1d822 100644 --- a/examples/01-basic/12-multi-editor/package.json +++ b/examples/01-basic/12-multi-editor/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/12-multi-editor/src/App.tsx b/examples/01-basic/12-multi-editor/src/App.tsx index 0aa2b11810..994e98830f 100644 --- a/examples/01-basic/12-multi-editor/src/App.tsx +++ b/examples/01-basic/12-multi-editor/src/App.tsx @@ -35,9 +35,6 @@ export default function App() { type: "paragraph", content: "This is a block in the first editor", }, - { - type: "paragraph", - }, ]} /> diff --git a/examples/01-basic/13-custom-paste-handler/package.json b/examples/01-basic/13-custom-paste-handler/package.json index 15dc905cb7..8f2fd39c02 100644 --- a/examples/01-basic/13-custom-paste-handler/package.json +++ b/examples/01-basic/13-custom-paste-handler/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/14-editor-scrollable/package.json b/examples/01-basic/14-editor-scrollable/package.json index df436a2643..395196edc9 100644 --- a/examples/01-basic/14-editor-scrollable/package.json +++ b/examples/01-basic/14-editor-scrollable/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/15-shadowdom/package.json b/examples/01-basic/15-shadowdom/package.json index 07d63662c1..bb80c17b37 100644 --- a/examples/01-basic/15-shadowdom/package.json +++ b/examples/01-basic/15-shadowdom/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/16-read-only-editor/package.json b/examples/01-basic/16-read-only-editor/package.json index bfefd7dc58..c746366d56 100644 --- a/examples/01-basic/16-read-only-editor/package.json +++ b/examples/01-basic/16-read-only-editor/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/01-basic/16-read-only-editor/src/App.tsx b/examples/01-basic/16-read-only-editor/src/App.tsx index ab0ae5cfb4..b7a1ef9d00 100644 --- a/examples/01-basic/16-read-only-editor/src/App.tsx +++ b/examples/01-basic/16-read-only-editor/src/App.tsx @@ -145,9 +145,6 @@ export default function App() { }, ], }, - { - type: "paragraph", - }, ], }); diff --git a/examples/01-basic/17-no-trailing-block/.bnexample.json b/examples/01-basic/17-no-trailing-block/.bnexample.json new file mode 100644 index 0000000000..e9c8bcb27b --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/.bnexample.json @@ -0,0 +1,6 @@ +{ + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": ["Basic"] +} diff --git a/examples/01-basic/17-no-trailing-block/README.md b/examples/01-basic/17-no-trailing-block/README.md new file mode 100644 index 0000000000..9c63e46fdd --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/README.md @@ -0,0 +1,7 @@ +# No Trailing Block + +This example shows how to disable the automatic creation of a trailing block at the end of the editor by setting the `trailingBlock` option to `false`. + +**Relevant Docs:** + +- [Editor Setup](/docs/getting-started/editor-setup) diff --git a/examples/01-basic/17-no-trailing-block/index.html b/examples/01-basic/17-no-trailing-block/index.html new file mode 100644 index 0000000000..a86933f050 --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/index.html @@ -0,0 +1,14 @@ + + + + + No Trailing Block + + + +
+ + + diff --git a/examples/01-basic/17-no-trailing-block/main.tsx b/examples/01-basic/17-no-trailing-block/main.tsx new file mode 100644 index 0000000000..677c7f7eed --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./src/App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/01-basic/17-no-trailing-block/package.json b/examples/01-basic/17-no-trailing-block/package.json new file mode 100644 index 0000000000..892bec6a77 --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/package.json @@ -0,0 +1,30 @@ +{ + "name": "@blocknote/example-basic-no-trailing-block", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "type": "module", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build:prod": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", + "@blocknote/mantine": "latest", + "@blocknote/react": "latest", + "@blocknote/shadcn": "latest", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@types/react": "^19.2.3", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" + } +} \ No newline at end of file diff --git a/examples/01-basic/17-no-trailing-block/src/App.tsx b/examples/01-basic/17-no-trailing-block/src/App.tsx new file mode 100644 index 0000000000..ac51ec74b3 --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/src/App.tsx @@ -0,0 +1,14 @@ +import "@blocknote/core/fonts/inter.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { useCreateBlockNote } from "@blocknote/react"; + +export default function App() { + // Creates a new editor instance. + const editor = useCreateBlockNote({ + trailingBlock: false, + }); + + // Renders the editor instance using a React component. + return ; +} diff --git a/examples/01-basic/17-no-trailing-block/tsconfig.json b/examples/01-basic/17-no-trailing-block/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/01-basic/17-no-trailing-block/vite.config.ts b/examples/01-basic/17-no-trailing-block/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/01-basic/17-no-trailing-block/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/examples/01-basic/testing/package.json b/examples/01-basic/testing/package.json index 909248d480..11ac43e993 100644 --- a/examples/01-basic/testing/package.json +++ b/examples/01-basic/testing/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/02-backend/01-file-uploading/package.json b/examples/02-backend/01-file-uploading/package.json index cd75ed5462..b12ea90ff6 100644 --- a/examples/02-backend/01-file-uploading/package.json +++ b/examples/02-backend/01-file-uploading/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/02-backend/01-file-uploading/src/App.tsx b/examples/02-backend/01-file-uploading/src/App.tsx index c5805f0c40..982d0f37a6 100644 --- a/examples/02-backend/01-file-uploading/src/App.tsx +++ b/examples/02-backend/01-file-uploading/src/App.tsx @@ -33,9 +33,6 @@ export default function App() { { type: "image", }, - { - type: "paragraph", - }, ], uploadFile, }); diff --git a/examples/02-backend/02-saving-loading/package.json b/examples/02-backend/02-saving-loading/package.json index 95f0aa24f7..b38ebfcd69 100644 --- a/examples/02-backend/02-saving-loading/package.json +++ b/examples/02-backend/02-saving-loading/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/02-backend/03-s3/package.json b/examples/02-backend/03-s3/package.json index bd1a31aa01..0ff100fa90 100644 --- a/examples/02-backend/03-s3/package.json +++ b/examples/02-backend/03-s3/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@aws-sdk/client-s3": "^3.609.0", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/02-backend/03-s3/src/App.tsx b/examples/02-backend/03-s3/src/App.tsx index 867933afc0..6f6731ec7e 100644 --- a/examples/02-backend/03-s3/src/App.tsx +++ b/examples/02-backend/03-s3/src/App.tsx @@ -81,9 +81,6 @@ export default function App() { { type: "image", }, - { - type: "paragraph", - }, ], uploadFile: async (file) => { /** diff --git a/examples/02-backend/04-rendering-static-documents/package.json b/examples/02-backend/04-rendering-static-documents/package.json index 17927198a8..619434055e 100644 --- a/examples/02-backend/04-rendering-static-documents/package.json +++ b/examples/02-backend/04-rendering-static-documents/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/server-util": "latest" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/02-backend/04-rendering-static-documents/src/App.tsx b/examples/02-backend/04-rendering-static-documents/src/App.tsx index b6ffd098a0..3a5db9891b 100644 --- a/examples/02-backend/04-rendering-static-documents/src/App.tsx +++ b/examples/02-backend/04-rendering-static-documents/src/App.tsx @@ -22,17 +22,17 @@ This example has the HTML hard-coded, but shows at least how the document will b export default function App() { // This HTML is generated by the ServerBlockNoteEditor.blocksToFullHTML method const html = `
-
-
-
+
+
+

Heading 2

-
-
-
+
+
+

Paragraph

diff --git a/examples/03-ui-components/01-ui-elements-remove/package.json b/examples/03-ui-components/01-ui-elements-remove/package.json index 70aa1a2265..429291840f 100644 --- a/examples/03-ui-components/01-ui-elements-remove/package.json +++ b/examples/03-ui-components/01-ui-elements-remove/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/01-ui-elements-remove/src/App.tsx b/examples/03-ui-components/01-ui-elements-remove/src/App.tsx index bb4c1176da..6f8b39a75f 100644 --- a/examples/03-ui-components/01-ui-elements-remove/src/App.tsx +++ b/examples/03-ui-components/01-ui-elements-remove/src/App.tsx @@ -21,9 +21,6 @@ export default function App() { content: "Try making text bold with Ctrl+B/Cmd+B or undo with Ctrl+Z/Cmd+Z.", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/package.json b/examples/03-ui-components/02-formatting-toolbar-buttons/package.json index bbdc0d592c..430ff81344 100644 --- a/examples/03-ui-components/02-formatting-toolbar-buttons/package.json +++ b/examples/03-ui-components/02-formatting-toolbar-buttons/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/src/App.tsx b/examples/03-ui-components/02-formatting-toolbar-buttons/src/App.tsx index b5a5abb6bc..f6bb83c2e1 100644 --- a/examples/03-ui-components/02-formatting-toolbar-buttons/src/App.tsx +++ b/examples/03-ui-components/02-formatting-toolbar-buttons/src/App.tsx @@ -18,6 +18,39 @@ import { import { BlueButton } from "./BlueButton"; +const CustomFormattingToolbar = () => ( + + + + {/* Extra button to toggle blue text & background */} + + + + + + + + + + {/* Extra button to toggle code styles */} + + + + + + + + + + + + + +); + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -71,70 +104,13 @@ export default function App() { content: "Notice that the buttons don't appear when the image block above is selected, as it has no inline content.", }, - { - type: "paragraph", - }, ], }); // Renders the editor instance. return ( - ( - - - - {/* Extra button to toggle blue text & background */} - - - - - - - - - - {/* Extra button to toggle code styles */} - - - - - - - - - - - - - - )} - /> + ); } diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/.bnexample.json b/examples/03-ui-components/03-formatting-toolbar-block-type-items/.bnexample.json index 73a254bea5..9d554dc51c 100644 --- a/examples/03-ui-components/03-formatting-toolbar-block-type-items/.bnexample.json +++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/.bnexample.json @@ -10,7 +10,7 @@ "Custom Schemas" ], "dependencies": { - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "react-icons": "^5.5.0" } } diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json b/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json index 64b3352fb2..4b550aaab2 100644 --- a/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json +++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx index 2ee1da2771..97b5836bfb 100644 --- a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx +++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx @@ -5,6 +5,7 @@ import "@blocknote/mantine/style.css"; import { FormattingToolbarController, blockTypeSelectItems, + useBlockNoteEditor, useCreateBlockNote, BlockTypeSelectItem, FormattingToolbar, @@ -24,6 +25,31 @@ const schema = BlockNoteSchema.create({ }, }); +const CustomFormattingToolbar = () => { + const editor = useBlockNoteEditor< + typeof schema.blockSchema, + typeof schema.inlineContentSchema, + typeof schema.styleSchema + >(); + + return ( + // Uses the default Formatting Toolbar. + + ); +}; + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -43,9 +69,6 @@ export default function App() { content: "Or select text in this alert - the Block Type Select also appears", }, - { - type: "paragraph", - }, ], }); @@ -53,24 +76,7 @@ export default function App() { return ( {/* Replaces the default Formatting Toolbar */} - ( - // Uses the default Formatting Toolbar. - - )} - /> + ); } diff --git a/examples/03-ui-components/04-side-menu-buttons/package.json b/examples/03-ui-components/04-side-menu-buttons/package.json index c3f63ce21a..c3de6a8993 100644 --- a/examples/03-ui-components/04-side-menu-buttons/package.json +++ b/examples/03-ui-components/04-side-menu-buttons/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/04-side-menu-buttons/src/App.tsx b/examples/03-ui-components/04-side-menu-buttons/src/App.tsx index 96ef099ef3..29a79fbbc9 100644 --- a/examples/03-ui-components/04-side-menu-buttons/src/App.tsx +++ b/examples/03-ui-components/04-side-menu-buttons/src/App.tsx @@ -5,11 +5,20 @@ import { DragHandleButton, SideMenu, SideMenuController, + SideMenuProps, useCreateBlockNote, } from "@blocknote/react"; import { RemoveBlockButton } from "./RemoveBlockButton"; +const CustomSideMenu = (props: SideMenuProps) => ( + + {/* Button which removes the hovered block. */} + + + +); + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -26,24 +35,13 @@ export default function App() { type: "paragraph", content: "Click it to remove the hovered block", }, - { - type: "paragraph", - }, ], }); // Renders the editor instance. return ( - ( - - {/* Button which removes the hovered block. */} - - - - )} - /> + ); } diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/package.json b/examples/03-ui-components/05-side-menu-drag-handle-items/package.json index 1f9c75200d..15cea76cc7 100644 --- a/examples/03-ui-components/05-side-menu-drag-handle-items/package.json +++ b/examples/03-ui-components/05-side-menu-drag-handle-items/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx b/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx index 0ff3a07174..ec50019e0c 100644 --- a/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx +++ b/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx @@ -7,6 +7,7 @@ import { RemoveBlockItem, SideMenu, SideMenuController, + SideMenuProps, useCreateBlockNote, } from "@blocknote/react"; @@ -24,6 +25,10 @@ const CustomDragHandleMenu = () => ( ); +const CustomSideMenu = (props: SideMenuProps) => ( + +); + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -41,20 +46,13 @@ export default function App() { content: "Try resetting this block's type using the new Drag Handle Menu item", }, - { - type: "paragraph", - }, ], }); // Renders the editor instance. return ( - ( - - )} - /> + ); } diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json index 4e1e364c94..0bddd94672 100644 --- a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json +++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/src/App.tsx b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/src/App.tsx index c5f65cfdd8..906000401a 100644 --- a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/src/App.tsx +++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/src/App.tsx @@ -56,9 +56,6 @@ export default function App() { type: "paragraph", content: "Notice the new 'Insert Hello World' item - try it out!", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json index e47e7b3eb0..b7226bd31d 100644 --- a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json +++ b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/src/App.tsx b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/src/App.tsx index 593e3703cc..c7af9b48a1 100644 --- a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/src/App.tsx +++ b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/src/App.tsx @@ -48,9 +48,6 @@ export default function App() { type: "paragraph", content: "It's been replaced with a custom component", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json index 1da0bd93ff..1ce117e49c 100644 --- a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json +++ b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/src/App.tsx b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/src/App.tsx index 10e0b7f43b..fe4f1e4ed4 100644 --- a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/src/App.tsx +++ b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/src/App.tsx @@ -22,9 +22,6 @@ export default function App() { type: "paragraph", content: "There are now 5 columns instead of 10", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json index 305056b4ec..4bd33e10e4 100644 --- a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json +++ b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/src/App.tsx b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/src/App.tsx index 56e91935f0..88379a6dd0 100644 --- a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/src/App.tsx +++ b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/src/App.tsx @@ -53,9 +53,6 @@ export default function App() { type: "paragraph", content: "It's been replaced with a custom component", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json b/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json index c1fef484c3..637ce38042 100644 --- a/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json +++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/src/App.tsx b/examples/03-ui-components/10-suggestion-menus-grid-mentions/src/App.tsx index 0c2f9f6c21..788ec00ed8 100644 --- a/examples/03-ui-components/10-suggestion-menus-grid-mentions/src/App.tsx +++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/src/App.tsx @@ -76,9 +76,6 @@ export function App() { type: "paragraph", content: "Press the '@' key to open the mentions menu and add another", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/03-ui-components/11-uppy-file-panel/package.json b/examples/03-ui-components/11-uppy-file-panel/package.json index 8955ce5b72..f5de4f080a 100644 --- a/examples/03-ui-components/11-uppy-file-panel/package.json +++ b/examples/03-ui-components/11-uppy-file-panel/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@uppy/core": "^3.13.1", @@ -37,7 +36,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/11-uppy-file-panel/src/App.tsx b/examples/03-ui-components/11-uppy-file-panel/src/App.tsx index 8f4ed37a5b..9f01a3c30b 100644 --- a/examples/03-ui-components/11-uppy-file-panel/src/App.tsx +++ b/examples/03-ui-components/11-uppy-file-panel/src/App.tsx @@ -5,6 +5,7 @@ import { FilePanelController, FormattingToolbar, FormattingToolbarController, + FormattingToolbarProps, getFormattingToolbarItems, useCreateBlockNote, } from "@blocknote/react"; @@ -12,6 +13,18 @@ import { import { FileReplaceButton } from "./FileReplaceButton"; import { uploadFile, UppyFilePanel } from "./UppyFilePanel"; +const CustomFormattingToolbar = (props: FormattingToolbarProps) => { + // Replaces default file replace button with one that opens Uppy. + const items = getFormattingToolbarItems(); + items.splice( + items.findIndex((c) => c.key === "replaceFileButton"), + 1, + , + ); + + return {items}; +}; + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -27,9 +40,6 @@ export default function App() { { type: "image", }, - { - type: "paragraph", - }, ], uploadFile, }); @@ -37,19 +47,7 @@ export default function App() { // Renders the editor instance using a React component. return ( - { - // Replaces default file replace button with one that opens Uppy. - const items = getFormattingToolbarItems(); - items.splice( - items.findIndex((c) => c.key === "replaceFileButton"), - 1, - , - ); - - return {items}; - }} - /> + {/* Replaces default file panel with Uppy one. */} diff --git a/examples/03-ui-components/12-static-formatting-toolbar/package.json b/examples/03-ui-components/12-static-formatting-toolbar/package.json index 8b455ffe0e..180de05193 100644 --- a/examples/03-ui-components/12-static-formatting-toolbar/package.json +++ b/examples/03-ui-components/12-static-formatting-toolbar/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/12-static-formatting-toolbar/src/App.tsx b/examples/03-ui-components/12-static-formatting-toolbar/src/App.tsx index fa61f64dc6..7889f0b342 100644 --- a/examples/03-ui-components/12-static-formatting-toolbar/src/App.tsx +++ b/examples/03-ui-components/12-static-formatting-toolbar/src/App.tsx @@ -17,9 +17,6 @@ export default function App() { type: "paragraph", content: "Check out the static formatting toolbar above!", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/03-ui-components/13-custom-ui/package.json b/examples/03-ui-components/13-custom-ui/package.json index 1ea4e67783..ef56dc0bef 100644 --- a/examples/03-ui-components/13-custom-ui/package.json +++ b/examples/03-ui-components/13-custom-ui/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@mui/icons-material": "^5.16.1", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/13-custom-ui/src/App.tsx b/examples/03-ui-components/13-custom-ui/src/App.tsx index dd229b1bdd..40974fedbc 100644 --- a/examples/03-ui-components/13-custom-ui/src/App.tsx +++ b/examples/03-ui-components/13-custom-ui/src/App.tsx @@ -27,9 +27,6 @@ export default function App() { type: "paragraph", content: "Welcome to this demo!", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json index 60265f345c..6c2d53320d 100644 --- a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/src/App.tsx b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/src/App.tsx index 62c82ddfe8..47d59e453c 100644 --- a/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/src/App.tsx +++ b/examples/03-ui-components/14-experimental-mobile-formatting-toolbar/src/App.tsx @@ -21,9 +21,6 @@ export default function App() { content: "Check out the experimental mobile formatting toolbar by selecting some text (best experienced on a mobile device).", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/03-ui-components/15-advanced-tables/package.json b/examples/03-ui-components/15-advanced-tables/package.json index 38e68d10e4..cf8cbdb701 100644 --- a/examples/03-ui-components/15-advanced-tables/package.json +++ b/examples/03-ui-components/15-advanced-tables/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/16-link-toolbar-buttons/package.json b/examples/03-ui-components/16-link-toolbar-buttons/package.json index 8e5263cf56..0480356acc 100644 --- a/examples/03-ui-components/16-link-toolbar-buttons/package.json +++ b/examples/03-ui-components/16-link-toolbar-buttons/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/16-link-toolbar-buttons/src/App.tsx b/examples/03-ui-components/16-link-toolbar-buttons/src/App.tsx index 1714e18a0d..52504b56bf 100644 --- a/examples/03-ui-components/16-link-toolbar-buttons/src/App.tsx +++ b/examples/03-ui-components/16-link-toolbar-buttons/src/App.tsx @@ -6,12 +6,32 @@ import { EditLinkButton, LinkToolbar, LinkToolbarController, + LinkToolbarProps, OpenLinkButton, useCreateBlockNote, } from "@blocknote/react"; import { AlertButton } from "./AlertButton"; +const CustomLinkToolbar = (props: LinkToolbarProps) => ( + + + + + {/* Extra button to open alert. */} + + +); + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -40,35 +60,13 @@ export default function App() { }, ], }, - { - type: "paragraph", - }, ], }); // Renders the editor instance. return ( - ( - - - - - {/* Extra button to open alert. */} - - - )} - /> + ); } diff --git a/examples/03-ui-components/17-advanced-tables-2/package.json b/examples/03-ui-components/17-advanced-tables-2/package.json index 0b759efc24..72643d1605 100644 --- a/examples/03-ui-components/17-advanced-tables-2/package.json +++ b/examples/03-ui-components/17-advanced-tables-2/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/18-drag-n-drop/package.json b/examples/03-ui-components/18-drag-n-drop/package.json index 354348292d..e925200fe1 100644 --- a/examples/03-ui-components/18-drag-n-drop/package.json +++ b/examples/03-ui-components/18-drag-n-drop/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/.bnexample.json b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/.bnexample.json new file mode 100644 index 0000000000..8aa4573b06 --- /dev/null +++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/.bnexample.json @@ -0,0 +1,12 @@ +{ + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Blocks", + "UI Components", + "Suggestion Menus", + "Slash Menu" + ] +} diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/README.md b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/README.md new file mode 100644 index 0000000000..76f3061571 --- /dev/null +++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/README.md @@ -0,0 +1,11 @@ +# Slash Menu Grouping & Ordering + +In this example, we filter and reorder the default Slash Menu items so that only the "Basic blocks" and "Headings" groups are shown, with "Basic blocks" appearing first. + +**Try it out:** Press the "/" key to open the Slash Menu and see the reordered groups! + +**Relevant Docs:** + +- [Item Grouping & Ordering](/docs/react/components/suggestion-menus) +- [Changing Slash Menu Items](/docs/react/components/suggestion-menus) +- [Editor Setup](/docs/getting-started/editor-setup) diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/index.html b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/index.html new file mode 100644 index 0000000000..405a9fc360 --- /dev/null +++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/index.html @@ -0,0 +1,14 @@ + + + + + Slash Menu Grouping & Ordering + + + +
+ + + diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/main.tsx b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/main.tsx new file mode 100644 index 0000000000..677c7f7eed --- /dev/null +++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./src/App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/package.json b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/package.json new file mode 100644 index 0000000000..551efbea33 --- /dev/null +++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/package.json @@ -0,0 +1,30 @@ +{ + "name": "@blocknote/example-ui-components-suggestion-menus-grouping-ordering", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "type": "module", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build:prod": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", + "@blocknote/mantine": "latest", + "@blocknote/react": "latest", + "@blocknote/shadcn": "latest", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@types/react": "^19.2.3", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" + } +} \ No newline at end of file diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/src/App.tsx b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/src/App.tsx new file mode 100644 index 0000000000..93480ff122 --- /dev/null +++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/src/App.tsx @@ -0,0 +1,60 @@ +import { BlockNoteEditor } from "@blocknote/core"; +import { filterSuggestionItems } from "@blocknote/core/extensions"; +import "@blocknote/core/fonts/inter.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { + DefaultReactSuggestionItem, + getDefaultReactSlashMenuItems, + SuggestionMenuController, + useCreateBlockNote, +} from "@blocknote/react"; + +// Returns the default Slash Menu items, keeping only the "Basic blocks" and +// "Headings" groups, with "Basic blocks" listed before "Headings". +const getCustomSlashMenuItems = ( + editor: BlockNoteEditor, +): DefaultReactSuggestionItem[] => { + const defaultItems = getDefaultReactSlashMenuItems(editor); + + const basicBlocks = defaultItems.filter( + (item) => item.group === "Basic blocks", + ); + const headings = defaultItems.filter((item) => item.group === "Headings"); + + return [...basicBlocks, ...headings]; +}; + +export default function App() { + // Creates a new editor instance. + const editor = useCreateBlockNote({ + initialContent: [ + { + type: "paragraph", + content: "Welcome to this demo!", + }, + { + type: "paragraph", + content: "Press the '/' key to open the Slash Menu", + }, + { + type: "paragraph", + content: + "Notice that only 'Basic blocks' and 'Headings' are shown, in that order", + }, + ], + }); + + // Renders the editor instance. + return ( + + + filterSuggestionItems(getCustomSlashMenuItems(editor), query) + } + /> + + ); +} diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/tsconfig.json b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/vite.config.ts b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/examples/03-ui-components/20-portal-elements/.bnexample.json b/examples/03-ui-components/20-portal-elements/.bnexample.json new file mode 100644 index 0000000000..40dfffd4d9 --- /dev/null +++ b/examples/03-ui-components/20-portal-elements/.bnexample.json @@ -0,0 +1,6 @@ +{ + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": ["UI Components", "Advanced"] +} diff --git a/examples/03-ui-components/20-portal-elements/README.md b/examples/03-ui-components/20-portal-elements/README.md new file mode 100644 index 0000000000..b95bcaf7e0 --- /dev/null +++ b/examples/03-ui-components/20-portal-elements/README.md @@ -0,0 +1,16 @@ +# Configuring Portal Targets + +By default, BlockNote's floating UI elements (formatting toolbar, slash menu, table handles, etc.) mount inside the editor's `bn-container`. The `portalElements` prop on `BlockNoteView` lets you change that — globally via `default`, or per element by key. + +This example renders two editors side-by-side, both wrapped in a small `overflow: hidden` container. The left editor uses the default — the slash menu is clipped by the editor's bounds. The right editor passes `portalElements={{ default: document.body }}` so floating UI escapes the wrapper and renders fully. + +```tsx + +``` + +**Relevant Docs:** + +- [UI Components](/docs/react/components) diff --git a/examples/03-ui-components/20-portal-elements/index.html b/examples/03-ui-components/20-portal-elements/index.html new file mode 100644 index 0000000000..e43d537d45 --- /dev/null +++ b/examples/03-ui-components/20-portal-elements/index.html @@ -0,0 +1,14 @@ + + + + + Configuring Portal Targets + + + +
+ + + diff --git a/examples/03-ui-components/20-portal-elements/main.tsx b/examples/03-ui-components/20-portal-elements/main.tsx new file mode 100644 index 0000000000..677c7f7eed --- /dev/null +++ b/examples/03-ui-components/20-portal-elements/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./src/App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/03-ui-components/20-portal-elements/package.json b/examples/03-ui-components/20-portal-elements/package.json new file mode 100644 index 0000000000..2ecd0a811d --- /dev/null +++ b/examples/03-ui-components/20-portal-elements/package.json @@ -0,0 +1,30 @@ +{ + "name": "@blocknote/example-ui-components-portal-elements", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "type": "module", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build:prod": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", + "@blocknote/mantine": "latest", + "@blocknote/react": "latest", + "@blocknote/shadcn": "latest", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@types/react": "^19.2.3", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" + } +} \ No newline at end of file diff --git a/examples/03-ui-components/20-portal-elements/src/App.tsx b/examples/03-ui-components/20-portal-elements/src/App.tsx new file mode 100644 index 0000000000..0434ff819b --- /dev/null +++ b/examples/03-ui-components/20-portal-elements/src/App.tsx @@ -0,0 +1,58 @@ +import "@blocknote/core/fonts/inter.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { useCreateBlockNote, type PortalElementsMap } from "@blocknote/react"; + +import "./styles.css"; + +const initialContent = [ + { + type: "paragraph" as const, + content: "Click in this editor and press / to open the slash menu.", + }, + { + type: "paragraph" as const, + content: + "Notice whether the menu fits inside the box or escapes it.", + }, + { + type: "paragraph" as const, + }, +]; + +function PortalDemoEditor({ + label, + description, + portalElements, +}: { + label: string; + description: string; + portalElements?: PortalElementsMap; +}) { + const editor = useCreateBlockNote({ initialContent }); + return ( +
+
{label}
+
{description}
+
+ +
+
+ ); +} + +export default function App() { + return ( +
+ + +
+ ); +} diff --git a/examples/03-ui-components/20-portal-elements/src/styles.css b/examples/03-ui-components/20-portal-elements/src/styles.css new file mode 100644 index 0000000000..8cf28385d0 --- /dev/null +++ b/examples/03-ui-components/20-portal-elements/src/styles.css @@ -0,0 +1,68 @@ +.views { + container-name: views; + container-type: inline-size; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; + padding: 8px; +} + +/* + * Each view is intentionally shorter than the slash menu so the clipping + * vs escaping behaviour is visible at a glance. + */ +.view-wrapper { + display: flex; + flex-direction: column; + height: 260px; + width: 100%; +} + +@container views (width > 1024px) { + .view-wrapper { + width: calc(50% - 4px); + } +} + +.view-label { + color: #0090ff; + display: flex; + font-size: 12px; + font-weight: bold; + justify-content: space-between; + margin-inline: 16px; +} + +.view-description { + color: #0090ff; + font-size: 12px; + margin: 2px 16px 0; +} + +/* + * `position: relative` is what actually makes `overflow: hidden` clip the + * absolutely-positioned floating UI. Without it the popover's containing + * block is the viewport and the clip is bypassed. + */ +.view { + border: solid #0090ff 1px; + border-radius: 16px; + flex: 1; + height: 0; + padding: 8px; + position: relative; + overflow: hidden; +} + +.view .bn-container { + height: 100%; + margin: 0; + max-width: none; + padding: 0; +} + +.view .bn-editor { + height: 100%; + overflow: auto; +} diff --git a/examples/03-ui-components/20-portal-elements/tsconfig.json b/examples/03-ui-components/20-portal-elements/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/03-ui-components/20-portal-elements/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/03-ui-components/20-portal-elements/vite.config.ts b/examples/03-ui-components/20-portal-elements/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/03-ui-components/20-portal-elements/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/examples/04-theming/01-theming-dom-attributes/package.json b/examples/04-theming/01-theming-dom-attributes/package.json index e507bf0891..b577ecace2 100644 --- a/examples/04-theming/01-theming-dom-attributes/package.json +++ b/examples/04-theming/01-theming-dom-attributes/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/04-theming/01-theming-dom-attributes/src/App.tsx b/examples/04-theming/01-theming-dom-attributes/src/App.tsx index 7068e12388..ccf55a3d5f 100644 --- a/examples/04-theming/01-theming-dom-attributes/src/App.tsx +++ b/examples/04-theming/01-theming-dom-attributes/src/App.tsx @@ -44,9 +44,6 @@ export default function App() { }, ], }, - { - type: "paragraph", - }, ], }); diff --git a/examples/04-theming/02-changing-font/package.json b/examples/04-theming/02-changing-font/package.json index 012ea4a389..36fbc361ab 100644 --- a/examples/04-theming/02-changing-font/package.json +++ b/examples/04-theming/02-changing-font/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/04-theming/02-changing-font/src/App.tsx b/examples/04-theming/02-changing-font/src/App.tsx index 4ef3f9a3dd..3626ef9e32 100644 --- a/examples/04-theming/02-changing-font/src/App.tsx +++ b/examples/04-theming/02-changing-font/src/App.tsx @@ -17,9 +17,6 @@ export default function App() { type: "paragraph", content: "You'll see that the font has been changed to Comic Sans MS", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/04-theming/03-theming-css/package.json b/examples/04-theming/03-theming-css/package.json index 946f29cfab..56ae428ea5 100644 --- a/examples/04-theming/03-theming-css/package.json +++ b/examples/04-theming/03-theming-css/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/04-theming/03-theming-css/src/App.tsx b/examples/04-theming/03-theming-css/src/App.tsx index 493ae7d4ed..b15568d810 100644 --- a/examples/04-theming/03-theming-css/src/App.tsx +++ b/examples/04-theming/03-theming-css/src/App.tsx @@ -22,9 +22,6 @@ export default function App() { content: "Press the '/' key - the hovered Slash Menu items are also blue", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/04-theming/04-theming-css-variables/package.json b/examples/04-theming/04-theming-css-variables/package.json index 2d3fb8fd37..df834c89bf 100644 --- a/examples/04-theming/04-theming-css-variables/package.json +++ b/examples/04-theming/04-theming-css-variables/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/04-theming/04-theming-css-variables/src/App.tsx b/examples/04-theming/04-theming-css-variables/src/App.tsx index ba45053f04..7ef5ab3f6d 100644 --- a/examples/04-theming/04-theming-css-variables/src/App.tsx +++ b/examples/04-theming/04-theming-css-variables/src/App.tsx @@ -22,9 +22,6 @@ export default function App() { content: "Toggle light/dark mode in the page footer and see the theme change too", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/04-theming/05-theming-css-variables-code/package.json b/examples/04-theming/05-theming-css-variables-code/package.json index e391fc873d..bb64789b86 100644 --- a/examples/04-theming/05-theming-css-variables-code/package.json +++ b/examples/04-theming/05-theming-css-variables-code/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/04-theming/05-theming-css-variables-code/src/App.tsx b/examples/04-theming/05-theming-css-variables-code/src/App.tsx index 8a02496d7e..0eaa83cfa1 100644 --- a/examples/04-theming/05-theming-css-variables-code/src/App.tsx +++ b/examples/04-theming/05-theming-css-variables-code/src/App.tsx @@ -84,9 +84,6 @@ export default function App() { content: "Toggle light/dark mode in the page footer and see the theme change too", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/04-theming/06-code-block/package.json b/examples/04-theming/06-code-block/package.json index d1122bd3ac..b9fa69a80a 100644 --- a/examples/04-theming/06-code-block/package.json +++ b/examples/04-theming/06-code-block/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/code-block": "latest" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/04-theming/06-code-block/src/App.tsx b/examples/04-theming/06-code-block/src/App.tsx index 6111070e37..82d10bae9e 100644 --- a/examples/04-theming/06-code-block/src/App.tsx +++ b/examples/04-theming/06-code-block/src/App.tsx @@ -47,9 +47,6 @@ export default function App() { }, ], }, - { - type: "paragraph", - }, ], }); diff --git a/examples/04-theming/07-custom-code-block/.bnexample.json b/examples/04-theming/07-custom-code-block/.bnexample.json index 5776d2de67..84166710e3 100644 --- a/examples/04-theming/07-custom-code-block/.bnexample.json +++ b/examples/04-theming/07-custom-code-block/.bnexample.json @@ -5,10 +5,10 @@ "tags": ["Basic"], "dependencies": { "@blocknote/code-block": "latest", - "@shikijs/core": "^3.19.0", - "@shikijs/engine-javascript": "^3.19.0", - "@shikijs/langs-precompiled": "^3.19.0", - "@shikijs/themes": "^3.19.0", - "@shikijs/types": "^3.19.0" + "@shikijs/core": "^4", + "@shikijs/engine-javascript": "^4", + "@shikijs/langs-precompiled": "^4", + "@shikijs/themes": "^4", + "@shikijs/types": "^4" } } diff --git a/examples/04-theming/07-custom-code-block/package.json b/examples/04-theming/07-custom-code-block/package.json index 153b1c160c..aa72035cbc 100644 --- a/examples/04-theming/07-custom-code-block/package.json +++ b/examples/04-theming/07-custom-code-block/package.json @@ -16,22 +16,21 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/code-block": "latest", - "@shikijs/core": "^3.19.0", - "@shikijs/engine-javascript": "^3.19.0", - "@shikijs/langs-precompiled": "^3.19.0", - "@shikijs/themes": "^3.19.0", - "@shikijs/types": "^3.19.0" + "@shikijs/core": "^4", + "@shikijs/engine-javascript": "^4", + "@shikijs/langs-precompiled": "^4", + "@shikijs/themes": "^4", + "@shikijs/types": "^4" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/04-theming/07-custom-code-block/src/App.tsx b/examples/04-theming/07-custom-code-block/src/App.tsx index 32adb82472..8a9c74eac1 100644 --- a/examples/04-theming/07-custom-code-block/src/App.tsx +++ b/examples/04-theming/07-custom-code-block/src/App.tsx @@ -69,9 +69,6 @@ export default function App() { }, ], }, - { - type: "paragraph", - }, ], }); diff --git a/examples/05-interoperability/01-converting-blocks-to-html/package.json b/examples/05-interoperability/01-converting-blocks-to-html/package.json index 0f0316000f..7b0b7f6409 100644 --- a/examples/05-interoperability/01-converting-blocks-to-html/package.json +++ b/examples/05-interoperability/01-converting-blocks-to-html/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/05-interoperability/02-converting-blocks-from-html/package.json b/examples/05-interoperability/02-converting-blocks-from-html/package.json index d62b8e0f42..4c780e56d3 100644 --- a/examples/05-interoperability/02-converting-blocks-from-html/package.json +++ b/examples/05-interoperability/02-converting-blocks-from-html/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/05-interoperability/03-converting-blocks-to-md/package.json b/examples/05-interoperability/03-converting-blocks-to-md/package.json index f6b0651c0d..f4d8ae5ada 100644 --- a/examples/05-interoperability/03-converting-blocks-to-md/package.json +++ b/examples/05-interoperability/03-converting-blocks-to-md/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/05-interoperability/04-converting-blocks-from-md/package.json b/examples/05-interoperability/04-converting-blocks-from-md/package.json index 3ca496c50d..6a2256cb74 100644 --- a/examples/05-interoperability/04-converting-blocks-from-md/package.json +++ b/examples/05-interoperability/04-converting-blocks-from-md/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/05-interoperability/05-converting-blocks-to-pdf/package.json b/examples/05-interoperability/05-converting-blocks-to-pdf/package.json index 3af24b9f75..fec26aef21 100644 --- a/examples/05-interoperability/05-converting-blocks-to-pdf/package.json +++ b/examples/05-interoperability/05-converting-blocks-to-pdf/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-pdf-exporter": "latest", @@ -28,7 +27,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/05-interoperability/06-converting-blocks-to-docx/package.json b/examples/05-interoperability/06-converting-blocks-to-docx/package.json index af8c76e04b..16bb0dfa29 100644 --- a/examples/05-interoperability/06-converting-blocks-to-docx/package.json +++ b/examples/05-interoperability/06-converting-blocks-to-docx/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-docx-exporter": "latest", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/05-interoperability/07-converting-blocks-to-odt/package.json b/examples/05-interoperability/07-converting-blocks-to-odt/package.json index aae892d407..5f3efacc28 100644 --- a/examples/05-interoperability/07-converting-blocks-to-odt/package.json +++ b/examples/05-interoperability/07-converting-blocks-to-odt/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-odt-exporter": "latest", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json index 10e8836f84..a2f87503bf 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-email-exporter": "latest", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/05-interoperability/09-blocks-to-html-static-render/package.json b/examples/05-interoperability/09-blocks-to-html-static-render/package.json index 52926421b4..58649cab50 100644 --- a/examples/05-interoperability/09-blocks-to-html-static-render/package.json +++ b/examples/05-interoperability/09-blocks-to-html-static-render/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/05-interoperability/09-blocks-to-html-static-render/src/App.tsx b/examples/05-interoperability/09-blocks-to-html-static-render/src/App.tsx index 8764f9fcc9..7ef44fc498 100644 --- a/examples/05-interoperability/09-blocks-to-html-static-render/src/App.tsx +++ b/examples/05-interoperability/09-blocks-to-html-static-render/src/App.tsx @@ -148,9 +148,6 @@ export default function App() { }, ], }, - { - type: "paragraph", - }, ], }); diff --git a/examples/05-interoperability/10-static-html-render/package.json b/examples/05-interoperability/10-static-html-render/package.json index 6343ac6efc..7da684540f 100644 --- a/examples/05-interoperability/10-static-html-render/package.json +++ b/examples/05-interoperability/10-static-html-render/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/05-interoperability/10-static-html-render/src/App.tsx b/examples/05-interoperability/10-static-html-render/src/App.tsx index 0094c677cb..f1f4eb4d42 100644 --- a/examples/05-interoperability/10-static-html-render/src/App.tsx +++ b/examples/05-interoperability/10-static-html-render/src/App.tsx @@ -144,9 +144,6 @@ export default function App() { }, ], }, - { - type: "paragraph", - }, ], }); diff --git a/examples/06-custom-schema/01-alert-block/.bnexample.json b/examples/06-custom-schema/01-alert-block/.bnexample.json index 955f240be5..1354e61187 100644 --- a/examples/06-custom-schema/01-alert-block/.bnexample.json +++ b/examples/06-custom-schema/01-alert-block/.bnexample.json @@ -10,7 +10,7 @@ "Slash Menu" ], "dependencies": { - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "react-icons": "^5.5.0" } } diff --git a/examples/06-custom-schema/01-alert-block/package.json b/examples/06-custom-schema/01-alert-block/package.json index b02510a70f..2ef9cadd20 100644 --- a/examples/06-custom-schema/01-alert-block/package.json +++ b/examples/06-custom-schema/01-alert-block/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/06-custom-schema/01-alert-block/src/App.tsx b/examples/06-custom-schema/01-alert-block/src/App.tsx index 929f5b8459..34a3afba7b 100644 --- a/examples/06-custom-schema/01-alert-block/src/App.tsx +++ b/examples/06-custom-schema/01-alert-block/src/App.tsx @@ -32,9 +32,6 @@ export default function App() { type: "paragraph", content: "Click the '!' icon to change the alert type", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/06-custom-schema/02-suggestion-menus-mentions/package.json b/examples/06-custom-schema/02-suggestion-menus-mentions/package.json index ea836ebabe..21e338460a 100644 --- a/examples/06-custom-schema/02-suggestion-menus-mentions/package.json +++ b/examples/06-custom-schema/02-suggestion-menus-mentions/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/06-custom-schema/02-suggestion-menus-mentions/src/App.tsx b/examples/06-custom-schema/02-suggestion-menus-mentions/src/App.tsx index 4339153441..c1e1a5c412 100644 --- a/examples/06-custom-schema/02-suggestion-menus-mentions/src/App.tsx +++ b/examples/06-custom-schema/02-suggestion-menus-mentions/src/App.tsx @@ -75,9 +75,6 @@ export function App() { type: "paragraph", content: "Press the '@' key to open the mentions menu and add another", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/06-custom-schema/03-font-style/package.json b/examples/06-custom-schema/03-font-style/package.json index ab031bf380..b784dc08b2 100644 --- a/examples/06-custom-schema/03-font-style/package.json +++ b/examples/06-custom-schema/03-font-style/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/06-custom-schema/03-font-style/src/App.tsx b/examples/06-custom-schema/03-font-style/src/App.tsx index 4cae9935b1..c813aaf713 100644 --- a/examples/06-custom-schema/03-font-style/src/App.tsx +++ b/examples/06-custom-schema/03-font-style/src/App.tsx @@ -60,6 +60,36 @@ const SetFontStyleButton = () => { ); }; +const CustomFormattingToolbar = () => ( + + + + + + + + + + + {/* Adds SetFontStyleButton */} + + + + + + + + + + + + + +); + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -91,64 +121,13 @@ export default function App() { content: "Highlight some text to open the Formatting Toolbar and change the font elsewhere", }, - { - type: "paragraph", - }, ], }); return ( {/* Replaces the default Formatting Toolbar. */} - ( - - - - - - - - - - - {/* Adds SetFontStyleButton */} - - - - - - - - - - - - - - )} - /> + ); } diff --git a/examples/06-custom-schema/04-pdf-file-block/.bnexample.json b/examples/06-custom-schema/04-pdf-file-block/.bnexample.json index f3f9f4d51b..c9ed35fa87 100644 --- a/examples/06-custom-schema/04-pdf-file-block/.bnexample.json +++ b/examples/06-custom-schema/04-pdf-file-block/.bnexample.json @@ -10,7 +10,7 @@ "Slash Menu" ], "dependencies": { - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "react-icons": "^5.5.0" }, "pro": true diff --git a/examples/06-custom-schema/04-pdf-file-block/package.json b/examples/06-custom-schema/04-pdf-file-block/package.json index d5f5615cd0..cd1de2d12f 100644 --- a/examples/06-custom-schema/04-pdf-file-block/package.json +++ b/examples/06-custom-schema/04-pdf-file-block/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/06-custom-schema/04-pdf-file-block/src/App.tsx b/examples/06-custom-schema/04-pdf-file-block/src/App.tsx index 3c244a2840..c5a69d9b4b 100644 --- a/examples/06-custom-schema/04-pdf-file-block/src/App.tsx +++ b/examples/06-custom-schema/04-pdf-file-block/src/App.tsx @@ -67,9 +67,6 @@ export default function App() { type: "paragraph", content: "Press the '/' key to open the Slash Menu and add another PDF", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/06-custom-schema/05-alert-block-full-ux/.bnexample.json b/examples/06-custom-schema/05-alert-block-full-ux/.bnexample.json index 3326066fe0..7e53bcb415 100644 --- a/examples/06-custom-schema/05-alert-block-full-ux/.bnexample.json +++ b/examples/06-custom-schema/05-alert-block-full-ux/.bnexample.json @@ -11,7 +11,7 @@ "Slash Menu" ], "dependencies": { - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "react-icons": "^5.5.0" } } diff --git a/examples/06-custom-schema/05-alert-block-full-ux/package.json b/examples/06-custom-schema/05-alert-block-full-ux/package.json index 55dc8b0bf1..6c56f839f1 100644 --- a/examples/06-custom-schema/05-alert-block-full-ux/package.json +++ b/examples/06-custom-schema/05-alert-block-full-ux/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx b/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx index 625dcce896..4a816f872c 100644 --- a/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx +++ b/examples/06-custom-schema/05-alert-block-full-ux/src/App.tsx @@ -13,6 +13,7 @@ import { SuggestionMenuController, blockTypeSelectItems, getDefaultReactSlashMenuItems, + useBlockNoteEditor, useCreateBlockNote, } from "@blocknote/react"; @@ -28,6 +29,31 @@ const schema = BlockNoteSchema.create().extend({ }, }); +const CustomFormattingToolbar = () => { + const editor = useBlockNoteEditor< + typeof schema.blockSchema, + typeof schema.inlineContentSchema, + typeof schema.styleSchema + >(); + + return ( + // Uses the default Formatting Toolbar. + + ); +}; + // Slash menu item to insert an Alert block const insertAlert = (editor: typeof schema.BlockNoteEditor) => ({ title: "Alert", @@ -75,9 +101,6 @@ export default function App() { content: "Or select some text to see the alert in the Formatting Toolbar's Block Type Select", }, - { - type: "paragraph", - }, ], }); @@ -85,24 +108,7 @@ export default function App() { return ( {/* Replaces the default Formatting Toolbar */} - ( - // Uses the default Formatting Toolbar. - - )} - /> + {/* Replaces the default Slash Menu. */} ' icon to show/hide its children", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/06-custom-schema/07-configuring-blocks/package.json b/examples/06-custom-schema/07-configuring-blocks/package.json index edf107c487..fc2e9a52cb 100644 --- a/examples/06-custom-schema/07-configuring-blocks/package.json +++ b/examples/06-custom-schema/07-configuring-blocks/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/06-custom-schema/07-configuring-blocks/src/App.tsx b/examples/06-custom-schema/07-configuring-blocks/src/App.tsx index 491dc8a1c1..47db15f007 100644 --- a/examples/06-custom-schema/07-configuring-blocks/src/App.tsx +++ b/examples/06-custom-schema/07-configuring-blocks/src/App.tsx @@ -32,9 +32,6 @@ export default function App() { content: "Notice how only heading levels 1-3 are available, and toggle headings are not shown.", }, - { - type: "paragraph", - }, ], }); diff --git a/examples/06-custom-schema/08-non-editable-block/.bnexample.json b/examples/06-custom-schema/08-non-editable-block/.bnexample.json new file mode 100644 index 0000000000..c94d1e9154 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/.bnexample.json @@ -0,0 +1,6 @@ +{ + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": ["Intermediate", "Blocks", "Custom Schemas"] +} diff --git a/examples/06-custom-schema/08-non-editable-block/README.md b/examples/06-custom-schema/08-non-editable-block/README.md new file mode 100644 index 0000000000..9c7cce19d1 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/README.md @@ -0,0 +1,8 @@ +# Non-Editable Block + +In this example, we create a custom block which renders a simple HTML paragraph with placeholder text. The block has no editable content. + +**Relevant Docs:** + +- [Custom Blocks](/docs/features/custom-schemas/custom-blocks) +- [Editor Setup](/docs/getting-started/editor-setup) diff --git a/examples/06-custom-schema/08-non-editable-block/index.html b/examples/06-custom-schema/08-non-editable-block/index.html new file mode 100644 index 0000000000..9b55422066 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/index.html @@ -0,0 +1,14 @@ + + + + + Non-Editable Block + + + +
+ + + diff --git a/examples/06-custom-schema/08-non-editable-block/main.tsx b/examples/06-custom-schema/08-non-editable-block/main.tsx new file mode 100644 index 0000000000..677c7f7eed --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./src/App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/06-custom-schema/08-non-editable-block/package.json b/examples/06-custom-schema/08-non-editable-block/package.json new file mode 100644 index 0000000000..a988601ed7 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/package.json @@ -0,0 +1,30 @@ +{ + "name": "@blocknote/example-custom-schema-non-editable-block", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "type": "module", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build:prod": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", + "@blocknote/mantine": "latest", + "@blocknote/react": "latest", + "@blocknote/shadcn": "latest", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@types/react": "^19.2.3", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" + } +} \ No newline at end of file diff --git a/examples/06-custom-schema/08-non-editable-block/src/App.tsx b/examples/06-custom-schema/08-non-editable-block/src/App.tsx new file mode 100644 index 0000000000..ca7a4cd8d0 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/src/App.tsx @@ -0,0 +1,35 @@ +import { BlockNoteSchema } from "@blocknote/core"; +import "@blocknote/core/fonts/inter.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { useCreateBlockNote } from "@blocknote/react"; + +import { createNonEditableBlock } from "./NonEditableBlock"; + +// Our schema with block specs, which contain the configs and implementations for +// blocks that we want our editor to use. +const schema = BlockNoteSchema.create().extend({ + blockSpecs: { + // Creates an instance of the Non-Editable block and adds it to the schema. + nonEditable: createNonEditableBlock(), + }, +}); + +export default function App() { + // Creates a new editor instance. + const editor = useCreateBlockNote({ + schema, + initialContent: [ + { + type: "paragraph", + content: "Welcome to this demo!", + }, + { + type: "nonEditable", + }, + ], + }); + + // Renders the editor instance. + return ; +} diff --git a/examples/06-custom-schema/08-non-editable-block/src/NonEditableBlock.tsx b/examples/06-custom-schema/08-non-editable-block/src/NonEditableBlock.tsx new file mode 100644 index 0000000000..a930c21f74 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/src/NonEditableBlock.tsx @@ -0,0 +1,13 @@ +import { createReactBlockSpec } from "@blocknote/react"; + +// The Non-Editable block. +export const createNonEditableBlock = createReactBlockSpec( + { + type: "nonEditable", + propSchema: {}, + content: "none", + }, + { + render: () =>

This is a non-editable block.

, + }, +); diff --git a/examples/06-custom-schema/08-non-editable-block/tsconfig.json b/examples/06-custom-schema/08-non-editable-block/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/06-custom-schema/08-non-editable-block/vite.config.ts b/examples/06-custom-schema/08-non-editable-block/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/06-custom-schema/08-non-editable-block/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/examples/06-custom-schema/draggable-inline-content/package.json b/examples/06-custom-schema/draggable-inline-content/package.json index 98db481029..3689f5390c 100644 --- a/examples/06-custom-schema/draggable-inline-content/package.json +++ b/examples/06-custom-schema/draggable-inline-content/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/06-custom-schema/react-custom-blocks/package.json b/examples/06-custom-schema/react-custom-blocks/package.json index 0372b1c809..b31875e4da 100644 --- a/examples/06-custom-schema/react-custom-blocks/package.json +++ b/examples/06-custom-schema/react-custom-blocks/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/06-custom-schema/react-custom-inline-content/package.json b/examples/06-custom-schema/react-custom-inline-content/package.json index 98abca8461..5e2ed6d99b 100644 --- a/examples/06-custom-schema/react-custom-inline-content/package.json +++ b/examples/06-custom-schema/react-custom-inline-content/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/06-custom-schema/react-custom-styles/package.json b/examples/06-custom-schema/react-custom-styles/package.json index eb88b335ff..3815485703 100644 --- a/examples/06-custom-schema/react-custom-styles/package.json +++ b/examples/06-custom-schema/react-custom-styles/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/07-collaboration/01-partykit/package.json b/examples/07-collaboration/01-partykit/package.json index c29a4981a9..9f4a8d0870 100644 --- a/examples/07-collaboration/01-partykit/package.json +++ b/examples/07-collaboration/01-partykit/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "y-partykit": "^0.0.25", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/07-collaboration/02-liveblocks/package.json b/examples/07-collaboration/02-liveblocks/package.json index defc9277d1..79dd475fc8 100644 --- a/examples/07-collaboration/02-liveblocks/package.json +++ b/examples/07-collaboration/02-liveblocks/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@liveblocks/client": "^3.17.0", @@ -31,7 +30,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/07-collaboration/03-y-sweet/package.json b/examples/07-collaboration/03-y-sweet/package.json index ca2e4b0097..8ca1ec5d75 100644 --- a/examples/07-collaboration/03-y-sweet/package.json +++ b/examples/07-collaboration/03-y-sweet/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@y-sweet/react": "^0.6.3" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/07-collaboration/04-electric-sql/package.json b/examples/07-collaboration/04-electric-sql/package.json index dd9d538875..8f0c69c80b 100644 --- a/examples/07-collaboration/04-electric-sql/package.json +++ b/examples/07-collaboration/04-electric-sql/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/07-collaboration/05-comments/.bnexample.json b/examples/07-collaboration/05-comments/.bnexample.json index 09ecc7196c..f2e0c026a9 100644 --- a/examples/07-collaboration/05-comments/.bnexample.json +++ b/examples/07-collaboration/05-comments/.bnexample.json @@ -5,6 +5,6 @@ "tags": ["Advanced", "Comments", "Collaboration"], "dependencies": { "@y-sweet/react": "^0.6.3", - "@mantine/core": "^8.3.11" + "@mantine/core": "^9.0.2" } } diff --git a/examples/07-collaboration/05-comments/package.json b/examples/07-collaboration/05-comments/package.json index 27897406ce..7736c432b4 100644 --- a/examples/07-collaboration/05-comments/package.json +++ b/examples/07-collaboration/05-comments/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@y-sweet/react": "^0.6.3" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/07-collaboration/06-comments-with-sidebar/.bnexample.json b/examples/07-collaboration/06-comments-with-sidebar/.bnexample.json index a80bff5aba..ff82fe290f 100644 --- a/examples/07-collaboration/06-comments-with-sidebar/.bnexample.json +++ b/examples/07-collaboration/06-comments-with-sidebar/.bnexample.json @@ -6,6 +6,6 @@ "dependencies": { "y-partykit": "^0.0.25", "yjs": "^13.6.27", - "@mantine/core": "^8.3.11" + "@mantine/core": "^9.0.2" } } diff --git a/examples/07-collaboration/06-comments-with-sidebar/package.json b/examples/07-collaboration/06-comments-with-sidebar/package.json index c1a6c49f9a..67a5504590 100644 --- a/examples/07-collaboration/06-comments-with-sidebar/package.json +++ b/examples/07-collaboration/06-comments-with-sidebar/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "y-partykit": "^0.0.25", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/07-collaboration/07-ghost-writer/package.json b/examples/07-collaboration/07-ghost-writer/package.json index a45bc3a305..26e4956fab 100644 --- a/examples/07-collaboration/07-ghost-writer/package.json +++ b/examples/07-collaboration/07-ghost-writer/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "y-partykit": "^0.0.25", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/07-collaboration/08-forking/package.json b/examples/07-collaboration/08-forking/package.json index 08ca3f389d..3d82fc59ba 100644 --- a/examples/07-collaboration/08-forking/package.json +++ b/examples/07-collaboration/08-forking/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "y-partykit": "^0.0.25", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/07-collaboration/09-comments-testing/.bnexample.json b/examples/07-collaboration/09-comments-testing/.bnexample.json new file mode 100644 index 0000000000..5d7d986420 --- /dev/null +++ b/examples/07-collaboration/09-comments-testing/.bnexample.json @@ -0,0 +1,9 @@ +{ + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": ["Advanced", "Comments", "Testing"], + "dependencies": { + "yjs": "^13.6.27" + } +} diff --git a/examples/07-collaboration/09-comments-testing/README.md b/examples/07-collaboration/09-comments-testing/README.md new file mode 100644 index 0000000000..b59f2ecd1b --- /dev/null +++ b/examples/07-collaboration/09-comments-testing/README.md @@ -0,0 +1,3 @@ +# Comments Testing + +A minimal comments example used for end-to-end testing. Uses a local Y.Doc (no collaboration provider) with a single hardcoded editor user. diff --git a/examples/07-collaboration/09-comments-testing/index.html b/examples/07-collaboration/09-comments-testing/index.html new file mode 100644 index 0000000000..f50976be79 --- /dev/null +++ b/examples/07-collaboration/09-comments-testing/index.html @@ -0,0 +1,14 @@ + + + + + Comments Testing + + + +
+ + + diff --git a/examples/07-collaboration/09-comments-testing/main.tsx b/examples/07-collaboration/09-comments-testing/main.tsx new file mode 100644 index 0000000000..677c7f7eed --- /dev/null +++ b/examples/07-collaboration/09-comments-testing/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./src/App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/07-collaboration/09-comments-testing/package.json b/examples/07-collaboration/09-comments-testing/package.json new file mode 100644 index 0000000000..c31e6c15c3 --- /dev/null +++ b/examples/07-collaboration/09-comments-testing/package.json @@ -0,0 +1,31 @@ +{ + "name": "@blocknote/example-collaboration-comments-testing", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "type": "module", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build:prod": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", + "@blocknote/mantine": "latest", + "@blocknote/react": "latest", + "@blocknote/shadcn": "latest", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "yjs": "^13.6.27" + }, + "devDependencies": { + "@types/react": "^19.2.3", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" + } +} \ No newline at end of file diff --git a/examples/07-collaboration/09-comments-testing/src/App.tsx b/examples/07-collaboration/09-comments-testing/src/App.tsx new file mode 100644 index 0000000000..3bada358c1 --- /dev/null +++ b/examples/07-collaboration/09-comments-testing/src/App.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { + CommentsExtension, + DefaultThreadStoreAuth, + YjsThreadStore, +} from "@blocknote/core/comments"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { useCreateBlockNote } from "@blocknote/react"; +import { useMemo } from "react"; +import * as Y from "yjs"; + +const USER = { + id: "1", + username: "John Doe", + avatarUrl: "https://placehold.co/100x100?text=John", + role: "editor" as const, +}; + +async function resolveUsers(userIds: string[]) { + return [USER].filter((user) => userIds.includes(user.id)); +} + +export default function App() { + const doc = useMemo(() => new Y.Doc(), []); + + const threadStore = useMemo(() => { + return new YjsThreadStore( + USER.id, + doc.getMap("threads"), + new DefaultThreadStoreAuth(USER.id, USER.role), + ); + }, [doc]); + + const editor = useCreateBlockNote( + { + extensions: [CommentsExtension({ threadStore, resolveUsers })], + }, + [threadStore], + ); + + return ; +} diff --git a/examples/07-collaboration/09-comments-testing/tsconfig.json b/examples/07-collaboration/09-comments-testing/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/07-collaboration/09-comments-testing/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/07-collaboration/09-comments-testing/vite.config.ts b/examples/07-collaboration/09-comments-testing/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/07-collaboration/09-comments-testing/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/examples/08-extensions/01-tiptap-arrow-conversion/package.json b/examples/08-extensions/01-tiptap-arrow-conversion/package.json index 7eaeaf3eaa..c781441abe 100644 --- a/examples/08-extensions/01-tiptap-arrow-conversion/package.json +++ b/examples/08-extensions/01-tiptap-arrow-conversion/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@tiptap/core": "^3.13.0" @@ -26,7 +25,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/09-ai/01-minimal/.bnexample.json b/examples/09-ai/01-minimal/.bnexample.json index 30d6def791..9aede450f7 100644 --- a/examples/09-ai/01-minimal/.bnexample.json +++ b/examples/09-ai/01-minimal/.bnexample.json @@ -5,7 +5,7 @@ "tags": ["AI", "llm"], "dependencies": { "@blocknote/xl-ai": "latest", - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "ai": "^6.0.5" } } diff --git a/examples/09-ai/01-minimal/package.json b/examples/09-ai/01-minimal/package.json index b244123bd7..b4ef7599fc 100644 --- a/examples/09-ai/01-minimal/package.json +++ b/examples/09-ai/01-minimal/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-ai": "latest", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/09-ai/01-minimal/src/App.tsx b/examples/09-ai/01-minimal/src/App.tsx index 37417d292b..3ada0eea97 100644 --- a/examples/09-ai/01-minimal/src/App.tsx +++ b/examples/09-ai/01-minimal/src/App.tsx @@ -22,12 +22,27 @@ import { en as aiEn } from "@blocknote/xl-ai/locales"; import "@blocknote/xl-ai/style.css"; import { DefaultChatTransport } from "ai"; -import { useEffect } from "react"; import { getEnv } from "./getEnv"; const BASE_URL = getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai"; +// Formatting toolbar with the `AIToolbarButton` added +const FormattingToolbarWithAI = () => ( + + {...getFormattingToolbarItems()} + {/* Add the AI button */} + + +); + +// Slash menu items with the AI option added +const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [ + ...getDefaultReactSlashMenuItems(editor), + // add the default AI slash menu items, or define your own + ...getAISlashMenuItems(editor), +]; + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -84,54 +99,23 @@ export default function App() { {/* Add the AI Command menu to the editor */} - {/* We disabled the default formatting toolbar with `formattingToolbar=false` - and replace it for one with an "AI button" (defined below). + {/* We disabled the default formatting toolbar with `formattingToolbar=false` + and replace it for one with an "AI button" (defined below). (See "Formatting Toolbar" in docs) */} - + - {/* We disabled the default SlashMenu with `slashMenu=false` - and replace it for one with an AI option (defined below). + {/* We disabled the default SlashMenu with `slashMenu=false` + and replace it for one with an AI option (defined below). (See "Suggestion Menus" in docs) */} - + + filterSuggestionItems(getSlashMenuItemsWithAI(editor), query) + } + />
); } - -// Formatting toolbar with the `AIToolbarButton` added -function FormattingToolbarWithAI() { - return ( - ( - - {...getFormattingToolbarItems()} - {/* Add the AI button */} - - - )} - /> - ); -} - -// Slash menu with the AI option added -function SuggestionMenuWithAI(props: { - editor: BlockNoteEditor; -}) { - return ( - - filterSuggestionItems( - [ - ...getDefaultReactSlashMenuItems(props.editor), - // add the default AI slash menu items, or define your own - ...getAISlashMenuItems(props.editor), - ], - query, - ) - } - /> - ); -} diff --git a/examples/09-ai/02-playground/.bnexample.json b/examples/09-ai/02-playground/.bnexample.json index 30d6def791..9aede450f7 100644 --- a/examples/09-ai/02-playground/.bnexample.json +++ b/examples/09-ai/02-playground/.bnexample.json @@ -5,7 +5,7 @@ "tags": ["AI", "llm"], "dependencies": { "@blocknote/xl-ai": "latest", - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "ai": "^6.0.5" } } diff --git a/examples/09-ai/02-playground/package.json b/examples/09-ai/02-playground/package.json index 9bfc984a4a..98f9c40219 100644 --- a/examples/09-ai/02-playground/package.json +++ b/examples/09-ai/02-playground/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-ai": "latest", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/09-ai/02-playground/src/App.tsx b/examples/09-ai/02-playground/src/App.tsx index ace8b47d79..008b17b40a 100644 --- a/examples/09-ai/02-playground/src/App.tsx +++ b/examples/09-ai/02-playground/src/App.tsx @@ -33,6 +33,20 @@ import { getEnv } from "./getEnv"; const BASE_URL = getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai"; +// Formatting toolbar with the `AIToolbarButton` added +const FormattingToolbarWithAI = () => ( + + {...getFormattingToolbarItems()} + + +); + +// Slash menu items with the AI option added +const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [ + ...getDefaultReactSlashMenuItems(editor), + ...getAISlashMenuItems(editor), +]; + export default function App() { const [model, setModel] = useState( "groq.chat/llama-3.3-70b-versatile", @@ -129,52 +143,23 @@ export default function App() { {/* Add the AI Command menu to the editor */} - {/* We disabled the default formatting toolbar with `formattingToolbar=false` - and replace it for one with an "AI button" (defined below). + {/* We disabled the default formatting toolbar with `formattingToolbar=false` + and replace it for one with an "AI button" (defined below). (See "Formatting Toolbar" in docs) */} - + {/* We disabled the default SlashMenu with `slashMenu=false` and replace it for one with an AI option (defined below). (See "Suggestion Menus" in docs) */} - + + filterSuggestionItems(getSlashMenuItemsWithAI(editor), query) + } + />
); } - -// Formatting toolbar with the `AIToolbarButton` added -function FormattingToolbarWithAI() { - return ( - ( - - {...getFormattingToolbarItems()} - - - )} - /> - ); -} - -// Slash menu with the AI option added -function SuggestionMenuWithAI(props: { - editor: BlockNoteEditor; -}) { - return ( - - filterSuggestionItems( - [ - ...getDefaultReactSlashMenuItems(props.editor), - ...getAISlashMenuItems(props.editor), - ], - query, - ) - } - /> - ); -} diff --git a/examples/09-ai/03-custom-ai-menu-items/.bnexample.json b/examples/09-ai/03-custom-ai-menu-items/.bnexample.json index a026ef1ee5..9a91d82062 100644 --- a/examples/09-ai/03-custom-ai-menu-items/.bnexample.json +++ b/examples/09-ai/03-custom-ai-menu-items/.bnexample.json @@ -5,7 +5,7 @@ "tags": ["AI", "llm"], "dependencies": { "@blocknote/xl-ai": "latest", - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "ai": "^6.0.5", "react-icons": "^5.5.0" } diff --git a/examples/09-ai/03-custom-ai-menu-items/package.json b/examples/09-ai/03-custom-ai-menu-items/package.json index 1385ceab9b..ee4c5d2163 100644 --- a/examples/09-ai/03-custom-ai-menu-items/package.json +++ b/examples/09-ai/03-custom-ai-menu-items/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-ai": "latest", @@ -28,7 +27,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/09-ai/03-custom-ai-menu-items/src/App.tsx b/examples/09-ai/03-custom-ai-menu-items/src/App.tsx index 8eee964b9f..3646cc8d9e 100644 --- a/examples/09-ai/03-custom-ai-menu-items/src/App.tsx +++ b/examples/09-ai/03-custom-ai-menu-items/src/App.tsx @@ -30,6 +30,63 @@ import { addRelatedTopics, makeInformal } from "./customAIMenuItems"; const BASE_URL = getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai"; +function CustomAIMenu() { + return ( + , + aiResponseStatus: + | "user-input" + | "thinking" + | "ai-writing" + | "error" + | "user-reviewing" + | "closed", + ) => { + if (aiResponseStatus === "user-input") { + // Returns different items based on whether the AI Menu was + // opened via the Formatting Toolbar or the Slash Menu. + if (editor.getSelection()) { + return [ + // Gets the default AI Menu items + ...getDefaultAIMenuItems(editor, aiResponseStatus), + // Adds our custom item to make the text more casual. + // Only appears when the AI Menu is opened via the + // Formatting Toolbar. + makeInformal(editor), + ]; + } else { + return [ + // Gets the default AI Menu items + ...getDefaultAIMenuItems(editor, aiResponseStatus), + // Adds our custom item to find related topics. Only + // appears when the AI Menu is opened via the Slash + // Menu. + addRelatedTopics(editor), + ]; + } + } + // for other states, return the default items + return getDefaultAIMenuItems(editor, aiResponseStatus); + }} + /> + ); +} + +// Formatting toolbar with the `AIToolbarButton` added +const FormattingToolbarWithAI = () => ( + + {...getFormattingToolbarItems()} + + +); + +// Slash menu items with the AI option added +const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [ + ...getDefaultReactSlashMenuItems(editor), + ...getAISlashMenuItems(editor), +]; + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -85,95 +142,23 @@ export default function App() { as well as our custom ones. */} - {/* We disabled the default formatting toolbar with `formattingToolbar=false` - and replace it for one with an "AI button" (defined below). + {/* We disabled the default formatting toolbar with `formattingToolbar=false` + and replace it for one with an "AI button" (defined below). (See "Formatting Toolbar" in docs) */} - + {/* We disabled the default SlashMenu with `slashMenu=false` and replace it for one with an AI option (defined below). (See "Suggestion Menus" in docs) */} - + + filterSuggestionItems(getSlashMenuItemsWithAI(editor), query) + } + />
); } - -function CustomAIMenu() { - return ( - , - aiResponseStatus: - | "user-input" - | "thinking" - | "ai-writing" - | "error" - | "user-reviewing" - | "closed", - ) => { - if (aiResponseStatus === "user-input") { - // Returns different items based on whether the AI Menu was - // opened via the Formatting Toolbar or the Slash Menu. - if (editor.getSelection()) { - return [ - // Gets the default AI Menu items - ...getDefaultAIMenuItems(editor, aiResponseStatus), - // Adds our custom item to make the text more casual. - // Only appears when the AI Menu is opened via the - // Formatting Toolbar. - makeInformal(editor), - ]; - } else { - return [ - // Gets the default AI Menu items - ...getDefaultAIMenuItems(editor, aiResponseStatus), - // Adds our custom item to find related topics. Only - // appears when the AI Menu is opened via the Slash - // Menu. - addRelatedTopics(editor), - ]; - } - } - // for other states, return the default items - return getDefaultAIMenuItems(editor, aiResponseStatus); - }} - /> - ); -} - -// Formatting toolbar with the `AIToolbarButton` added -function FormattingToolbarWithAI() { - return ( - ( - - {...getFormattingToolbarItems()} - - - )} - /> - ); -} - -// Slash menu with the AI option added -function SuggestionMenuWithAI(props: { - editor: BlockNoteEditor; -}) { - return ( - - filterSuggestionItems( - [ - ...getDefaultReactSlashMenuItems(props.editor), - ...getAISlashMenuItems(props.editor), - ], - query, - ) - } - /> - ); -} diff --git a/examples/09-ai/04-with-collaboration/.bnexample.json b/examples/09-ai/04-with-collaboration/.bnexample.json index 922d7f719e..83bed82fe4 100644 --- a/examples/09-ai/04-with-collaboration/.bnexample.json +++ b/examples/09-ai/04-with-collaboration/.bnexample.json @@ -5,7 +5,7 @@ "tags": ["AI", "llm"], "dependencies": { "@blocknote/xl-ai": "latest", - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "ai": "^6.0.5", "y-partykit": "^0.0.25", "yjs": "^13.6.27" diff --git a/examples/09-ai/04-with-collaboration/package.json b/examples/09-ai/04-with-collaboration/package.json index fe17faaa59..e75864a84f 100644 --- a/examples/09-ai/04-with-collaboration/package.json +++ b/examples/09-ai/04-with-collaboration/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-ai": "latest", @@ -29,7 +28,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/09-ai/04-with-collaboration/src/App.tsx b/examples/09-ai/04-with-collaboration/src/App.tsx index 3fd8076ccd..9073141352 100644 --- a/examples/09-ai/04-with-collaboration/src/App.tsx +++ b/examples/09-ai/04-with-collaboration/src/App.tsx @@ -58,6 +58,22 @@ if (isGhostWriting) { const ghostContent = "This demo shows a two-way sync of documents. It allows you to test collaboration features, and see how stable the editor is. "; +// Formatting toolbar with the `AIToolbarButton` added +const FormattingToolbarWithAI = () => ( + + {...getFormattingToolbarItems()} + {/* Add the AI button */} + + +); + +// Slash menu items with the AI option added +const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [ + ...getDefaultReactSlashMenuItems(editor), + // add the default AI slash menu items, or define your own + ...getAISlashMenuItems(editor), +]; + export default function App() { const [numGhostWriters, setNumGhostWriters] = useState(1); const [isPaused, setIsPaused] = useState(false); @@ -176,17 +192,22 @@ export default function App() { {/* Add the AI Command menu to the editor */} - {/* We disabled the default formatting toolbar with `formattingToolbar=false` - and replace it for one with an "AI button" (defined below). + {/* We disabled the default formatting toolbar with `formattingToolbar=false` + and replace it for one with an "AI button" (defined below). (See "Formatting Toolbar" in docs) */} - + {/* We disabled the default SlashMenu with `slashMenu=false` and replace it for one with an AI option (defined below). (See "Suggestion Menus" in docs) */} - + + filterSuggestionItems(getSlashMenuItemsWithAI(editor), query) + } + /> {!isGhostWriting && ( @@ -205,39 +226,3 @@ export default function App() { ); } - -// Formatting toolbar with the `AIToolbarButton` added -function FormattingToolbarWithAI() { - return ( - ( - - {...getFormattingToolbarItems()} - {/* Add the AI button */} - - - )} - /> - ); -} - -// Slash menu with the AI option added -function SuggestionMenuWithAI(props: { - editor: BlockNoteEditor; -}) { - return ( - - filterSuggestionItems( - [ - ...getDefaultReactSlashMenuItems(props.editor), - // add the default AI slash menu items, or define your own - ...getAISlashMenuItems(props.editor), - ], - query, - ) - } - /> - ); -} diff --git a/examples/09-ai/05-manual-execution/.bnexample.json b/examples/09-ai/05-manual-execution/.bnexample.json index c5b86534d1..890b2909fe 100644 --- a/examples/09-ai/05-manual-execution/.bnexample.json +++ b/examples/09-ai/05-manual-execution/.bnexample.json @@ -5,7 +5,7 @@ "tags": ["AI", "llm"], "dependencies": { "@blocknote/xl-ai": "latest", - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "ai": "^6.0.5", "y-partykit": "^0.0.25", "yjs": "^13.6.27" diff --git a/examples/09-ai/05-manual-execution/package.json b/examples/09-ai/05-manual-execution/package.json index b23f7678a0..56ee0692fd 100644 --- a/examples/09-ai/05-manual-execution/package.json +++ b/examples/09-ai/05-manual-execution/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-ai": "latest", @@ -29,7 +28,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/09-ai/06-client-side-transport/.bnexample.json b/examples/09-ai/06-client-side-transport/.bnexample.json index 1c7e871335..0ac2b679fc 100644 --- a/examples/09-ai/06-client-side-transport/.bnexample.json +++ b/examples/09-ai/06-client-side-transport/.bnexample.json @@ -6,7 +6,7 @@ "dependencies": { "@ai-sdk/groq": "^3.0.2", "@blocknote/xl-ai": "latest", - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "ai": "^6.0.5" } } diff --git a/examples/09-ai/06-client-side-transport/package.json b/examples/09-ai/06-client-side-transport/package.json index 4a143fbef3..94250e9f3c 100644 --- a/examples/09-ai/06-client-side-transport/package.json +++ b/examples/09-ai/06-client-side-transport/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@ai-sdk/groq": "^3.0.2", @@ -28,7 +27,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/09-ai/06-client-side-transport/src/App.tsx b/examples/09-ai/06-client-side-transport/src/App.tsx index ca7b64518e..14ed72abdb 100644 --- a/examples/09-ai/06-client-side-transport/src/App.tsx +++ b/examples/09-ai/06-client-side-transport/src/App.tsx @@ -28,6 +28,22 @@ import { getEnv } from "./getEnv"; const BASE_URL = getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai"; +// Formatting toolbar with the `AIToolbarButton` added +const FormattingToolbarWithAI = () => ( + + {...getFormattingToolbarItems()} + {/* Add the AI button */} + + +); + +// Slash menu items with the AI option added +const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [ + ...getDefaultReactSlashMenuItems(editor), + // add the default AI slash menu items, or define your own + ...getAISlashMenuItems(editor), +]; + // We define the model directly in our app using the Vercel AI SDK const model = createGroq({ // We supply a custom fetch function so that requests are routed through our proxy server @@ -97,54 +113,23 @@ export default function App() { {/* Add the AI Command menu to the editor */} - {/* We disabled the default formatting toolbar with `formattingToolbar=false` - and replace it for one with an "AI button" (defined below). + {/* We disabled the default formatting toolbar with `formattingToolbar=false` + and replace it for one with an "AI button" (defined below). (See "Formatting Toolbar" in docs) */} - + {/* We disabled the default SlashMenu with `slashMenu=false` and replace it for one with an AI option (defined below). (See "Suggestion Menus" in docs) */} - + + filterSuggestionItems(getSlashMenuItemsWithAI(editor), query) + } + />
); } - -// Formatting toolbar with the `AIToolbarButton` added -function FormattingToolbarWithAI() { - return ( - ( - - {...getFormattingToolbarItems()} - {/* Add the AI button */} - - - )} - /> - ); -} - -// Slash menu with the AI option added -function SuggestionMenuWithAI(props: { - editor: BlockNoteEditor; -}) { - return ( - - filterSuggestionItems( - [ - ...getDefaultReactSlashMenuItems(props.editor), - // add the default AI slash menu items, or define your own - ...getAISlashMenuItems(props.editor), - ], - query, - ) - } - /> - ); -} diff --git a/examples/09-ai/07-server-persistence/.bnexample.json b/examples/09-ai/07-server-persistence/.bnexample.json index 35815d0d21..12b79358ef 100644 --- a/examples/09-ai/07-server-persistence/.bnexample.json +++ b/examples/09-ai/07-server-persistence/.bnexample.json @@ -5,7 +5,7 @@ "tags": ["AI", "llm"], "dependencies": { "@blocknote/xl-ai": "latest", - "@mantine/core": "^8.3.11", + "@mantine/core": "^9.0.2", "ai": "^6.0.5" } } diff --git a/examples/09-ai/07-server-persistence/package.json b/examples/09-ai/07-server-persistence/package.json index b379364817..bbcb69e15d 100644 --- a/examples/09-ai/07-server-persistence/package.json +++ b/examples/09-ai/07-server-persistence/package.json @@ -16,9 +16,8 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3", "@blocknote/xl-ai": "latest", @@ -27,7 +26,7 @@ "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/09-ai/07-server-persistence/src/App.tsx b/examples/09-ai/07-server-persistence/src/App.tsx index a22d9c9e1c..1d07beafac 100644 --- a/examples/09-ai/07-server-persistence/src/App.tsx +++ b/examples/09-ai/07-server-persistence/src/App.tsx @@ -26,6 +26,22 @@ import { getEnv } from "./getEnv"; const BASE_URL = getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai"; +// Formatting toolbar with the `AIToolbarButton` added +const FormattingToolbarWithAI = () => ( + + {...getFormattingToolbarItems()} + {/* Add the AI button */} + + +); + +// Slash menu items with the AI option added +const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [ + ...getDefaultReactSlashMenuItems(editor), + // add the default AI slash menu items, or define your own + ...getAISlashMenuItems(editor), +]; + export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ @@ -112,54 +128,23 @@ export default function App() { {/* Add the AI Command menu to the editor */} - {/* We disabled the default formatting toolbar with `formattingToolbar=false` - and replace it for one with an "AI button" (defined below). + {/* We disabled the default formatting toolbar with `formattingToolbar=false` + and replace it for one with an "AI button" (defined below). (See "Formatting Toolbar" in docs) */} - + {/* We disabled the default SlashMenu with `slashMenu=false` and replace it for one with an AI option (defined below). (See "Suggestion Menus" in docs) */} - + + filterSuggestionItems(getSlashMenuItemsWithAI(editor), query) + } + />
); } - -// Formatting toolbar with the `AIToolbarButton` added -function FormattingToolbarWithAI() { - return ( - ( - - {...getFormattingToolbarItems()} - {/* Add the AI button */} - - - )} - /> - ); -} - -// Slash menu with the AI option added -function SuggestionMenuWithAI(props: { - editor: BlockNoteEditor; -}) { - return ( - - filterSuggestionItems( - [ - ...getDefaultReactSlashMenuItems(props.editor), - // add the default AI slash menu items, or define your own - ...getAISlashMenuItems(props.editor), - ], - query, - ) - } - /> - ); -} diff --git a/examples/vanilla-js/react-vanilla-custom-blocks/package.json b/examples/vanilla-js/react-vanilla-custom-blocks/package.json index caf42e11e5..bb2e5beeee 100644 --- a/examples/vanilla-js/react-vanilla-custom-blocks/package.json +++ b/examples/vanilla-js/react-vanilla-custom-blocks/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/vanilla-js/react-vanilla-custom-inline-content/package.json b/examples/vanilla-js/react-vanilla-custom-inline-content/package.json index 2eb0b9a64f..e065088442 100644 --- a/examples/vanilla-js/react-vanilla-custom-inline-content/package.json +++ b/examples/vanilla-js/react-vanilla-custom-inline-content/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/examples/vanilla-js/react-vanilla-custom-styles/package.json b/examples/vanilla-js/react-vanilla-custom-styles/package.json index f192b05a60..1f2691e014 100644 --- a/examples/vanilla-js/react-vanilla-custom-styles/package.json +++ b/examples/vanilla-js/react-vanilla-custom-styles/package.json @@ -16,16 +16,15 @@ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", "react": "^19.2.3", "react-dom": "^19.2.3" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - "vite": "^5.4.20" + "@vitejs/plugin-react": "^6.0.1", + "vite": "^8.0.8" } } \ No newline at end of file diff --git a/package.json b/package.json index e0b97bd346..d0f2875eec 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "root", "type": "module", "devDependencies": { - "@nx/js": "22.6.4", + "@nx/js": "22.6.5", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "concurrently": "9.1.2", @@ -10,13 +10,13 @@ "eslint-config-react-app": "^7.0.1", "eslint-plugin-import": "^2.32.0", "glob": "^10.5.0", - "nx": "22.6.4", + "nx": "22.6.5", "prettier": "3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", "serve": "14.2.6", "typescript": "^5.9.3", "vitest": "^4.1.2", - "wait-on": "8.0.3" + "wait-on": "9.0.5" }, "pnpm": { "ignoredBuiltDependencies": [ @@ -35,17 +35,8 @@ "unrs-resolver" ], "overrides": { - "msw": "2.11.5", - "ai": "6.0.5", - "@ai-sdk/anthropic": "3.0.2", - "@ai-sdk/openai": "3.0.2", - "@ai-sdk/groq": "3.0.2", - "@ai-sdk/google": "3.0.2", - "@ai-sdk/mistral": "3.0.2", - "@ai-sdk/openai-compatible": "2.0.2", - "@ai-sdk/provider-utils": "4.0.2", - "@ai-sdk/react": "3.0.5", - "@ai-sdk/gateway": "3.0.4" + "vitest": "4.1.2", + "@vitest/runner": "4.1.2" } }, "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b", @@ -69,5 +60,30 @@ "start": "serve playground/dist -c ../serve.json", "test": "nx run-many --target=test", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,css,scss,md}\"" - } + }, + "overrides": { + "msw": "2.11.5", + "ai": "6.0.5", + "@ai-sdk/anthropic": "3.0.2", + "@ai-sdk/openai": "3.0.2", + "@ai-sdk/groq": "3.0.2", + "@ai-sdk/google": "3.0.2", + "@ai-sdk/mistral": "3.0.2", + "@ai-sdk/openai-compatible": "2.0.2", + "@ai-sdk/provider-utils": "4.0.2", + "@ai-sdk/react": "3.0.5", + "@ai-sdk/gateway": "3.0.4", + "@headlessui/react": "^2.2.4", + "@tiptap/core": "^3.0.0", + "@tiptap/pm": "^3.0.0" + }, + "workspaces": [ + "packages/*", + "examples/*/*", + "playground", + "fumadocs", + "docs", + "shared", + "tests" + ] } diff --git a/packages/ariakit/package.json b/packages/ariakit/package.json index 8745119df6..b7291bf6ea 100644 --- a/packages/ariakit/package.json +++ b/packages/ariakit/package.json @@ -11,7 +11,7 @@ "directory": "packages/ariakit" }, "license": "MPL-2.0", - "version": "0.48.0", + "version": "0.51.0", "files": [ "dist", "types", @@ -57,20 +57,20 @@ }, "dependencies": { "@ariakit/react": "^0.4.19", - "@blocknote/core": "0.48.0", - "@blocknote/react": "0.48.0" + "@blocknote/core": "0.51.0", + "@blocknote/react": "0.51.0" }, "devDependencies": { "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^8.57.1", - "react": "^19.2.3", - "react-dom": "^19.2.3", + "react": "^19.2.5", + "react-dom": "^19.2.5", "rimraf": "^5.0.10", "rollup-plugin-webpack-stats": "^0.2.6", "typescript": "^5.9.3", - "vite": "^8.0.3", + "vite": "^8.0.8", "vite-plugin-eslint": "^1.8.1", "vite-plugin-externalize-deps": "^0.10.0" }, diff --git a/packages/code-block/package.json b/packages/code-block/package.json index f71bbef191..f69342f942 100644 --- a/packages/code-block/package.json +++ b/packages/code-block/package.json @@ -9,7 +9,7 @@ "directory": "packages/code-block" }, "license": "MPL-2.0", - "version": "0.48.0", + "version": "0.51.0", "files": [ "dist", "types", @@ -49,7 +49,7 @@ "test-watch": "vitest watch" }, "dependencies": { - "@blocknote/core": "0.48.0", + "@blocknote/core": "0.51.0", "@shikijs/core": "^4", "@shikijs/engine-javascript": "^4", "@shikijs/langs-precompiled": "^4", @@ -60,12 +60,12 @@ "eslint": "^8.57.1", "rollup-plugin-webpack-stats": "^0.2.6", "typescript": "^5.9.3", - "vite": "^8.0.3", + "vite": "^8.0.8", "vite-plugin-eslint": "^1.8.1", "vitest": "^4.1.2" }, "peerDependencies": { - "@blocknote/core": "0.48.0" + "@blocknote/core": "0.51.0" }, "eslintConfig": { "extends": [ diff --git a/packages/core/package.json b/packages/core/package.json index 7e8879f54e..c37562b259 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -11,7 +11,7 @@ "directory": "packages/core" }, "license": "MPL-2.0", - "version": "0.48.0", + "version": "0.51.0", "files": [ "dist", "types", @@ -86,7 +86,8 @@ "lint": "eslint src --max-warnings 0", "test": "vitest --run", "test-watch": "vitest watch", - "clean": "rimraf dist && rimraf types" + "clean": "rimraf dist && rimraf types", + "update-tlds": "node scripts/update-tlds.mjs" }, "dependencies": { "@emoji-mart/data": "^1.2.1", @@ -98,7 +99,6 @@ "@tiptap/extension-code": "^3.13.0", "@tiptap/extension-horizontal-rule": "^3.13.0", "@tiptap/extension-italic": "^3.13.0", - "@tiptap/extension-link": "^3.22.1", "@tiptap/extension-paragraph": "^3.13.0", "@tiptap/extension-strike": "^3.13.0", "@tiptap/extension-text": "^3.13.0", @@ -107,38 +107,24 @@ "@tiptap/pm": "^3.13.0", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", - "hast-util-from-dom": "^5.0.1", + "lib0": "^0.2.99", "prosemirror-highlight": "^0.15.1", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-tables": "^1.8.3", "prosemirror-transform": "^1.11.0", "prosemirror-view": "^1.41.4", - "rehype-format": "^5.0.1", - "rehype-parse": "^9.0.1", - "rehype-remark": "^10.0.1", - "rehype-stringify": "^10.0.1", - "remark-gfm": "^4.0.1", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "remark-stringify": "^11.0.0", - "unified": "^11.0.5", - "unist-util-visit": "^5.0.0", - "uuid": "^8.3.2", "y-prosemirror": "^1.3.7", "y-protocols": "^1.0.6", "yjs": "^13.6.27" }, "devDependencies": { - "@types/emoji-mart": "^3.0.14", - "@types/hast": "^3.0.4", - "@types/uuid": "^8.3.4", "eslint": "^8.57.1", - "jsdom": "^25.0.1", + "jsdom": "^29.0.2", "rimraf": "^5.0.10", "rollup-plugin-webpack-stats": "^0.2.6", "typescript": "^5.9.3", - "vite": "^8.0.3", + "vite": "^8.0.8", "vite-plugin-eslint": "^1.8.1", "vitest": "^4.1.2" }, diff --git a/packages/core/scripts/update-tlds.mjs b/packages/core/scripts/update-tlds.mjs new file mode 100644 index 0000000000..43f4d02e15 --- /dev/null +++ b/packages/core/scripts/update-tlds.mjs @@ -0,0 +1,135 @@ +#!/usr/bin/env node +/** + * Regenerate src/extensions/tiptap-extensions/Link/helpers/tlds.ts from IANA's + * authoritative TLD list. + * + * Run with: pnpm --filter @blocknote/core update-tlds + * + * Encoding format ported from linkifyjs (MIT, https://github.com/nfrasser/linkifyjs): + * a sorted TLD list is built into a trie, then serialized as an ASCII string + * where letters descend the trie and digit runs mean "emit a word and pop N + * levels back up." Shared TLD prefixes (e.g. construction/consulting/ + * contractors) collapse, producing a payload smaller than a flat list. + * + * IDN punycode entries (XN--...) are skipped: the schemeless URL regex in + * linkDetector.ts requires ASCII-only TLDs, so unicode TLDs would never reach + * the validation step. + */ + +import { writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +const TLDS_URL = "https://data.iana.org/TLD/tlds-alpha-by-domain.txt"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const OUT_PATH = resolve( + __dirname, + "../src/extensions/tiptap-extensions/Link/helpers/tlds.ts", +); + +function createTrie(words) { + const root = {}; + for (const word of words) { + let current = root; + for (const letter of word) { + if (!(letter in current)) { + current[letter] = {}; + } + current = current[letter]; + } + current.isWord = true; + } + return root; +} + +function encodeTrieHelper(trie) { + const output = []; + for (const k in trie) { + if (k === "isWord") { + output.push(0); + continue; + } + output.push(k); + output.push(...encodeTrieHelper(trie[k])); + if (typeof output[output.length - 1] === "number") { + output[output.length - 1] += 1; + } else { + output.push(1); + } + } + return output; +} + +function encodeTlds(tlds) { + return encodeTrieHelper(createTrie(tlds)).join(""); +} + +function decodeTlds(encoded) { + const words = []; + const stack = []; + let i = 0; + const digits = "0123456789"; + while (i < encoded.length) { + let popDigitCount = 0; + while (digits.indexOf(encoded[i + popDigitCount]) >= 0) { + popDigitCount++; + } + if (popDigitCount > 0) { + words.push(stack.join("")); + let popCount = parseInt(encoded.substring(i, i + popDigitCount), 10); + while (popCount-- > 0) { + stack.pop(); + } + i += popDigitCount; + } else { + stack.push(encoded[i]); + i++; + } + } + return words; +} + +async function main() { + console.log(`Fetching ${TLDS_URL}...`); + const response = await fetch(TLDS_URL); + if (!response.ok) { + throw new Error(`Failed to fetch IANA TLDs: ${response.status}`); + } + const body = await response.text(); + + const tlds = body + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#") && !/^XN--/i.test(line)) + .map((line) => line.toLowerCase()) + .sort(); + + console.log(`Encoding ${tlds.length} TLDs...`); + const encoded = encodeTlds(tlds); + + console.log("Round-trip asserting..."); + const decoded = decodeTlds(encoded); + if (JSON.stringify(decoded) !== JSON.stringify(tlds)) { + throw new Error("Encode/decode round-trip mismatch"); + } + + const fileContents = `// THIS FILE IS AUTO-GENERATED. DO NOT EDIT DIRECTLY. +// Source: ${TLDS_URL} +// Regenerate with: pnpm --filter @blocknote/core update-tlds +// Encoding format ported from linkifyjs (MIT) — trie collapsed into ASCII. + +export const ENCODED_TLDS = + "${encoded}"; +`; + + writeFileSync(OUT_PATH, fileContents); + console.log( + `Wrote ${OUT_PATH} (${encoded.length} chars, ${tlds.length} TLDs)`, + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap index dd575d1041..e854849d11 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap @@ -664,7 +664,7 @@ exports[`Test insertBlocks > Insert multiple blocks after 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1339,7 +1339,7 @@ exports[`Test insertBlocks > Insert multiple blocks before 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1934,7 +1934,7 @@ exports[`Test insertBlocks > Insert single basic block after 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -2541,7 +2541,7 @@ exports[`Test insertBlocks > Insert single basic block before (without type) 2`] { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3136,7 +3136,7 @@ exports[`Test insertBlocks > Insert single basic block before 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3847,7 +3847,7 @@ exports[`Test insertBlocks > Insert single complex block after 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -4558,7 +4558,7 @@ exports[`Test insertBlocks > Insert single complex block before 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts index 81390947bf..25debee60c 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts @@ -26,9 +26,11 @@ export function insertBlocks< const id = typeof referenceBlock === "string" ? referenceBlock : referenceBlock.id; const pmSchema = getPmSchema(tr); - const nodesToInsert = blocksToInsert.map((block) => - blockToNode(block, pmSchema), - ); + const nodesToInsert = blocksToInsert.map((block) => { + const node = blockToNode(block, pmSchema); + node.check(); // `blockToNode` is lenient; validate before mutating the doc + return node; + }); const posInfo = getNodeById(id, tr.doc); if (!posInfo) { diff --git a/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap index 690c00017e..20c94c5ab8 100644 --- a/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap @@ -540,7 +540,7 @@ exports[`Test mergeBlocks > Basic 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1091,7 +1091,7 @@ exports[`Test mergeBlocks > Blocks have different types 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1642,7 +1642,7 @@ exports[`Test mergeBlocks > First block has children 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -2192,7 +2192,7 @@ exports[`Test mergeBlocks > Second block has children 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -2749,7 +2749,7 @@ exports[`Test mergeBlocks > Second block is empty 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap index 902463bbc1..e59da045ed 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap @@ -557,7 +557,7 @@ exports[`Test moveBlocksDown > Basic 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -568,8 +568,25 @@ exports[`Test moveBlocksDown > Basic 1`] = ` ] `; -exports[`Test moveBlocksDown > Into children 1`] = ` +exports[`Test moveBlocksDown > Explicit block argument moves the given block 1`] = ` [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, { "children": [], "content": [ @@ -589,23 +606,6 @@ exports[`Test moveBlocksDown > Into children 1`] = ` }, { "children": [ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 1", - "type": "text", - }, - ], - "id": "paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, { "children": [ { @@ -1125,7 +1125,7 @@ exports[`Test moveBlocksDown > Into children 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1136,7 +1136,7 @@ exports[`Test moveBlocksDown > Into children 1`] = ` ] `; -exports[`Test moveBlocksDown > Last block 1`] = ` +exports[`Test moveBlocksDown > Explicit block argument with nested block 1`] = ` [ { "children": [], @@ -1172,36 +1172,35 @@ exports[`Test moveBlocksDown > Last block 1`] = ` }, "type": "paragraph", }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph with children", + "type": "text", + }, + ], + "id": "paragraph-with-children", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, { "children": [ { - "children": [ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Double Nested Paragraph 0", - "type": "text", - }, - ], - "id": "double-nested-paragraph-0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], + "children": [], "content": [ { "styles": {}, - "text": "Nested Paragraph 0", + "text": "Double Nested Paragraph 0", "type": "text", }, ], - "id": "nested-paragraph-0", + "id": "double-nested-paragraph-0", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1213,11 +1212,11 @@ exports[`Test moveBlocksDown > Last block 1`] = ` "content": [ { "styles": {}, - "text": "Paragraph with children", + "text": "Nested Paragraph 0", "type": "text", }, ], - "id": "paragraph-with-children", + "id": "nested-paragraph-0", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1693,7 +1692,7 @@ exports[`Test moveBlocksDown > Last block 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1704,7 +1703,7 @@ exports[`Test moveBlocksDown > Last block 1`] = ` ] `; -exports[`Test moveBlocksDown > Multiple blocks 1`] = ` +exports[`Test moveBlocksDown > Into children 1`] = ` [ { "children": [], @@ -1724,41 +1723,24 @@ exports[`Test moveBlocksDown > Multiple blocks 1`] = ` "type": "paragraph", }, { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph with props", - "type": "text", - }, - ], - "id": "paragraph-with-props", - "props": { - "backgroundColor": "default", - "textAlignment": "center", - "textColor": "red", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ + "children": [ { - "styles": {}, - "text": "Paragraph 1", - "type": "text", + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", }, - ], - "id": "paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [ { "children": [ { @@ -1827,6 +1809,23 @@ exports[`Test moveBlocksDown > Multiple blocks 1`] = ` }, "type": "paragraph", }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph with props", + "type": "text", + }, + ], + "id": "paragraph-with-props", + "props": { + "backgroundColor": "default", + "textAlignment": "center", + "textColor": "red", + }, + "type": "paragraph", + }, { "children": [], "content": [ @@ -2261,7 +2260,7 @@ exports[`Test moveBlocksDown > Multiple blocks 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -2272,7 +2271,7 @@ exports[`Test moveBlocksDown > Multiple blocks 1`] = ` ] `; -exports[`Test moveBlocksDown > Multiple blocks ending in block with children 1`] = ` +exports[`Test moveBlocksDown > Last block 1`] = ` [ { "children": [], @@ -2291,23 +2290,6 @@ exports[`Test moveBlocksDown > Multiple blocks ending in block with children 1`] }, "type": "paragraph", }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 2", - "type": "text", - }, - ], - "id": "paragraph-2", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, { "children": [], "content": [ @@ -2378,6 +2360,23 @@ exports[`Test moveBlocksDown > Multiple blocks ending in block with children 1`] }, "type": "paragraph", }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, { "children": [], "content": [ @@ -2829,7 +2828,7 @@ exports[`Test moveBlocksDown > Multiple blocks ending in block with children 1`] { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -2840,7 +2839,7 @@ exports[`Test moveBlocksDown > Multiple blocks ending in block with children 1`] ] `; -exports[`Test moveBlocksDown > Multiple blocks ending in nested block 1`] = ` +exports[`Test moveBlocksDown > Multiple blocks 1`] = ` [ { "children": [], @@ -2864,15 +2863,15 @@ exports[`Test moveBlocksDown > Multiple blocks ending in nested block 1`] = ` "content": [ { "styles": {}, - "text": "Paragraph 2", + "text": "Paragraph with props", "type": "text", }, ], - "id": "paragraph-2", + "id": "paragraph-with-props", "props": { "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", + "textAlignment": "center", + "textColor": "red", }, "type": "paragraph", }, @@ -2951,15 +2950,15 @@ exports[`Test moveBlocksDown > Multiple blocks ending in nested block 1`] = ` "content": [ { "styles": {}, - "text": "Paragraph with props", + "text": "Paragraph 2", "type": "text", }, ], - "id": "paragraph-with-props", + "id": "paragraph-2", "props": { "backgroundColor": "default", - "textAlignment": "center", - "textColor": "red", + "textAlignment": "left", + "textColor": "default", }, "type": "paragraph", }, @@ -3397,7 +3396,7 @@ exports[`Test moveBlocksDown > Multiple blocks ending in nested block 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3408,7 +3407,7 @@ exports[`Test moveBlocksDown > Multiple blocks ending in nested block 1`] = ` ] `; -exports[`Test moveBlocksDown > Multiple blocks starting and ending in nested block 1`] = ` +exports[`Test moveBlocksDown > Multiple blocks ending in block with children 1`] = ` [ { "children": [], @@ -3432,11 +3431,11 @@ exports[`Test moveBlocksDown > Multiple blocks starting and ending in nested blo "content": [ { "styles": {}, - "text": "Paragraph 1", + "text": "Paragraph 2", "type": "text", }, ], - "id": "paragraph-1", + "id": "paragraph-2", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3449,22 +3448,11 @@ exports[`Test moveBlocksDown > Multiple blocks starting and ending in nested blo "content": [ { "styles": {}, - "text": "Paragraph with children", + "text": "Paragraph 1", "type": "text", }, ], - "id": "paragraph-with-children", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [], - "id": "trailing-paragraph", + "id": "paragraph-1", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3475,15 +3463,33 @@ exports[`Test moveBlocksDown > Multiple blocks starting and ending in nested blo { "children": [ { - "children": [], + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 0", + "type": "text", + }, + ], + "id": "double-nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], "content": [ { "styles": {}, - "text": "Double Nested Paragraph 0", + "text": "Nested Paragraph 0", "type": "text", }, ], - "id": "double-nested-paragraph-0", + "id": "nested-paragraph-0", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3495,11 +3501,11 @@ exports[`Test moveBlocksDown > Multiple blocks starting and ending in nested blo "content": [ { "styles": {}, - "text": "Nested Paragraph 0", + "text": "Paragraph with children", "type": "text", }, ], - "id": "nested-paragraph-0", + "id": "paragraph-with-children", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3512,28 +3518,11 @@ exports[`Test moveBlocksDown > Multiple blocks starting and ending in nested blo "content": [ { "styles": {}, - "text": "Paragraph 2", + "text": "Paragraph with props", "type": "text", }, ], - "id": "paragraph-2", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph with props", - "type": "text", - }, - ], - "id": "paragraph-with-props", + "id": "paragraph-with-props", "props": { "backgroundColor": "default", "textAlignment": "center", @@ -3975,7 +3964,7 @@ exports[`Test moveBlocksDown > Multiple blocks starting and ending in nested blo { "children": [], "content": [], - "id": "0", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3986,7 +3975,7 @@ exports[`Test moveBlocksDown > Multiple blocks starting and ending in nested blo ] `; -exports[`Test moveBlocksDown > Multiple blocks starting in block with children 1`] = ` +exports[`Test moveBlocksDown > Multiple blocks ending in nested block 1`] = ` [ { "children": [], @@ -4010,11 +3999,11 @@ exports[`Test moveBlocksDown > Multiple blocks starting in block with children 1 "content": [ { "styles": {}, - "text": "Paragraph 1", + "text": "Paragraph 2", "type": "text", }, ], - "id": "paragraph-1", + "id": "paragraph-2", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -4027,15 +4016,15 @@ exports[`Test moveBlocksDown > Multiple blocks starting in block with children 1 "content": [ { "styles": {}, - "text": "Paragraph with props", + "text": "Paragraph 1", "type": "text", }, ], - "id": "paragraph-with-props", + "id": "paragraph-1", "props": { "backgroundColor": "default", - "textAlignment": "center", - "textColor": "red", + "textAlignment": "left", + "textColor": "default", }, "type": "paragraph", }, @@ -4097,15 +4086,15 @@ exports[`Test moveBlocksDown > Multiple blocks starting in block with children 1 "content": [ { "styles": {}, - "text": "Paragraph 2", + "text": "Paragraph with props", "type": "text", }, ], - "id": "paragraph-2", + "id": "paragraph-with-props", "props": { "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", + "textAlignment": "center", + "textColor": "red", }, "type": "paragraph", }, @@ -4543,7 +4532,7 @@ exports[`Test moveBlocksDown > Multiple blocks starting in block with children 1 { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -4554,7 +4543,7 @@ exports[`Test moveBlocksDown > Multiple blocks starting in block with children 1 ] `; -exports[`Test moveBlocksDown > Multiple blocks starting in nested block 1`] = ` +exports[`Test moveBlocksDown > Multiple blocks starting and ending in nested block 1`] = ` [ { "children": [], @@ -4609,18 +4598,12 @@ exports[`Test moveBlocksDown > Multiple blocks starting in nested block 1`] = ` }, { "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph with props", - "type": "text", - }, - ], - "id": "paragraph-with-props", + "content": [], + "id": "paragraph-9", "props": { "backgroundColor": "default", - "textAlignment": "center", - "textColor": "red", + "textAlignment": "left", + "textColor": "default", }, "type": "paragraph", }, @@ -4676,6 +4659,23 @@ exports[`Test moveBlocksDown > Multiple blocks starting in nested block 1`] = ` }, "type": "paragraph", }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph with props", + "type": "text", + }, + ], + "id": "paragraph-with-props", + "props": { + "backgroundColor": "default", + "textAlignment": "center", + "textColor": "red", + }, + "type": "paragraph", + }, { "children": [], "content": [ @@ -5107,21 +5107,10 @@ exports[`Test moveBlocksDown > Multiple blocks starting in nested block 1`] = ` }, "type": "heading", }, - { - "children": [], - "content": [], - "id": "trailing-paragraph", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; -exports[`Test moveBlocksDown > Out of children 1`] = ` +exports[`Test moveBlocksDown > Multiple blocks starting in block with children 1`] = ` [ { "children": [], @@ -5157,6 +5146,23 @@ exports[`Test moveBlocksDown > Out of children 1`] = ` }, "type": "paragraph", }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph with props", + "type": "text", + }, + ], + "id": "paragraph-with-props", + "props": { + "backgroundColor": "default", + "textAlignment": "center", + "textColor": "red", + }, + "type": "paragraph", + }, { "children": [ { @@ -5227,23 +5233,6 @@ exports[`Test moveBlocksDown > Out of children 1`] = ` }, "type": "paragraph", }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph with props", - "type": "text", - }, - ], - "id": "paragraph-with-props", - "props": { - "backgroundColor": "default", - "textAlignment": "center", - "textColor": "red", - }, - "type": "paragraph", - }, { "children": [], "content": [ @@ -5607,7 +5596,43 @@ exports[`Test moveBlocksDown > Out of children 1`] = ` "type": "paragraph", }, { - "children": [], + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], "content": [ { "styles": { @@ -5640,33 +5665,31 @@ exports[`Test moveBlocksDown > Out of children 1`] = ` "type": "heading", }, { - "children": [ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Double Nested Paragraph 1", - "type": "text", - }, - ], - "id": "double-nested-paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], + "children": [], + "content": [], + "id": "paragraph-9", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test moveBlocksDown > Multiple blocks starting in nested block 1`] = ` +[ + { + "children": [], "content": [ { "styles": {}, - "text": "Nested Paragraph 1", + "text": "Paragraph 0", "type": "text", }, ], - "id": "nested-paragraph-1", + "id": "paragraph-0", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -5676,8 +5699,14 @@ exports[`Test moveBlocksDown > Out of children 1`] = ` }, { "children": [], - "content": [], - "id": "trailing-paragraph", + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -5685,21 +5714,16 @@ exports[`Test moveBlocksDown > Out of children 1`] = ` }, "type": "paragraph", }, -] -`; - -exports[`Test moveBlocksUp > Basic 1`] = ` -[ { "children": [], "content": [ { "styles": {}, - "text": "Paragraph 1", + "text": "Paragraph with children", "type": "text", }, ], - "id": "paragraph-1", + "id": "paragraph-with-children", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -5712,48 +5736,30 @@ exports[`Test moveBlocksUp > Basic 1`] = ` "content": [ { "styles": {}, - "text": "Paragraph 0", + "text": "Paragraph with props", "type": "text", }, ], - "id": "paragraph-0", + "id": "paragraph-with-props", "props": { "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", + "textAlignment": "center", + "textColor": "red", }, "type": "paragraph", }, { "children": [ { - "children": [ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Double Nested Paragraph 0", - "type": "text", - }, - ], - "id": "double-nested-paragraph-0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], + "children": [], "content": [ { "styles": {}, - "text": "Nested Paragraph 0", + "text": "Double Nested Paragraph 0", "type": "text", }, ], - "id": "nested-paragraph-0", + "id": "double-nested-paragraph-0", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -5765,11 +5771,11 @@ exports[`Test moveBlocksUp > Basic 1`] = ` "content": [ { "styles": {}, - "text": "Paragraph with children", + "text": "Nested Paragraph 0", "type": "text", }, ], - "id": "paragraph-with-children", + "id": "nested-paragraph-0", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -5799,32 +5805,15 @@ exports[`Test moveBlocksUp > Basic 1`] = ` "content": [ { "styles": {}, - "text": "Paragraph with props", + "text": "Paragraph 3", "type": "text", }, ], - "id": "paragraph-with-props", + "id": "paragraph-3", "props": { "backgroundColor": "default", - "textAlignment": "center", - "textColor": "red", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 3", - "type": "text", - }, - ], - "id": "paragraph-3", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", + "textAlignment": "left", + "textColor": "default", }, "type": "paragraph", }, @@ -6245,7 +6234,2277 @@ exports[`Test moveBlocksUp > Basic 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test moveBlocksDown > Out of children 1`] = ` +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 0", + "type": "text", + }, + ], + "id": "double-nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph with children", + "type": "text", + }, + ], + "id": "paragraph-with-children", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph with props", + "type": "text", + }, + ], + "id": "paragraph-with-props", + "props": { + "backgroundColor": "default", + "textAlignment": "center", + "textColor": "red", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 3", + "type": "text", + }, + ], + "id": "paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Paragraph", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "paragraph-with-styled-content", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 4", + "type": "text", + }, + ], + "id": "paragraph-4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 1", + "type": "text", + }, + ], + "id": "heading-0", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 5", + "type": "text", + }, + ], + "id": "paragraph-5", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": undefined, + "id": "image-0", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "", + "showPreview": true, + "textAlignment": "left", + "url": "https://via.placeholder.com/150", + }, + "type": "image", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 6", + "type": "text", + }, + ], + "id": "paragraph-6", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 3", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 4", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 5", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 6", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 7", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 8", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 9", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "table-0", + "props": { + "textColor": "default", + }, + "type": "table", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 7", + "type": "text", + }, + ], + "id": "paragraph-7", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "empty-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 8", + "type": "text", + }, + ], + "id": "paragraph-8", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "isToggleable": false, + "level": 2, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "paragraph-9", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test moveBlocksUp > Basic 1`] = ` +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 0", + "type": "text", + }, + ], + "id": "double-nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph with children", + "type": "text", + }, + ], + "id": "paragraph-with-children", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph with props", + "type": "text", + }, + ], + "id": "paragraph-with-props", + "props": { + "backgroundColor": "default", + "textAlignment": "center", + "textColor": "red", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 3", + "type": "text", + }, + ], + "id": "paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Paragraph", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "paragraph-with-styled-content", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 4", + "type": "text", + }, + ], + "id": "paragraph-4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 1", + "type": "text", + }, + ], + "id": "heading-0", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 5", + "type": "text", + }, + ], + "id": "paragraph-5", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": undefined, + "id": "image-0", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "", + "showPreview": true, + "textAlignment": "left", + "url": "https://via.placeholder.com/150", + }, + "type": "image", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 6", + "type": "text", + }, + ], + "id": "paragraph-6", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 3", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 4", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 5", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 6", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 7", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 8", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 9", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "table-0", + "props": { + "textColor": "default", + }, + "type": "table", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 7", + "type": "text", + }, + ], + "id": "paragraph-7", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "empty-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 8", + "type": "text", + }, + ], + "id": "paragraph-8", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "isToggleable": false, + "level": 2, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", + }, + { + "children": [], + "content": [], + "id": "paragraph-9", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test moveBlocksUp > Explicit block argument moves the given block 1`] = ` +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 0", + "type": "text", + }, + ], + "id": "double-nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph with children", + "type": "text", + }, + ], + "id": "paragraph-with-children", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph with props", + "type": "text", + }, + ], + "id": "paragraph-with-props", + "props": { + "backgroundColor": "default", + "textAlignment": "center", + "textColor": "red", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 3", + "type": "text", + }, + ], + "id": "paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Paragraph", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "paragraph-with-styled-content", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 4", + "type": "text", + }, + ], + "id": "paragraph-4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 1", + "type": "text", + }, + ], + "id": "heading-0", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 5", + "type": "text", + }, + ], + "id": "paragraph-5", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": undefined, + "id": "image-0", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "", + "showPreview": true, + "textAlignment": "left", + "url": "https://via.placeholder.com/150", + }, + "type": "image", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 6", + "type": "text", + }, + ], + "id": "paragraph-6", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 3", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 4", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 5", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 6", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 7", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 8", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 9", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "table-0", + "props": { + "textColor": "default", + }, + "type": "table", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 7", + "type": "text", + }, + ], + "id": "paragraph-7", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "empty-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 8", + "type": "text", + }, + ], + "id": "paragraph-8", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "isToggleable": false, + "level": 2, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", + }, + { + "children": [], + "content": [], + "id": "paragraph-9", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test moveBlocksUp > Explicit block argument with nested block 1`] = ` +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 0", + "type": "text", + }, + ], + "id": "double-nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph with children", + "type": "text", + }, + ], + "id": "paragraph-with-children", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph with props", + "type": "text", + }, + ], + "id": "paragraph-with-props", + "props": { + "backgroundColor": "default", + "textAlignment": "center", + "textColor": "red", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 3", + "type": "text", + }, + ], + "id": "paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Paragraph", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "paragraph-with-styled-content", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 4", + "type": "text", + }, + ], + "id": "paragraph-4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 1", + "type": "text", + }, + ], + "id": "heading-0", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 5", + "type": "text", + }, + ], + "id": "paragraph-5", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": undefined, + "id": "image-0", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "", + "showPreview": true, + "textAlignment": "left", + "url": "https://via.placeholder.com/150", + }, + "type": "image", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 6", + "type": "text", + }, + ], + "id": "paragraph-6", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 3", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 4", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 5", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 6", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 7", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 8", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 9", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "table-0", + "props": { + "textColor": "default", + }, + "type": "table", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 7", + "type": "text", + }, + ], + "id": "paragraph-7", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "empty-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 8", + "type": "text", + }, + ], + "id": "paragraph-8", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "isToggleable": false, + "level": 2, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", + }, + { + "children": [], + "content": [], + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -6813,7 +9072,7 @@ exports[`Test moveBlocksUp > First block 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -7381,7 +9640,7 @@ exports[`Test moveBlocksUp > Into children 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -7949,7 +10208,7 @@ exports[`Test moveBlocksUp > Multiple blocks 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -8517,7 +10776,7 @@ exports[`Test moveBlocksUp > Multiple blocks ending in block with children 1`] = { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -9085,7 +11344,7 @@ exports[`Test moveBlocksUp > Multiple blocks ending in nested block 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -9652,7 +11911,7 @@ exports[`Test moveBlocksUp > Multiple blocks starting and ending in nested block { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -10220,7 +12479,7 @@ exports[`Test moveBlocksUp > Multiple blocks starting in block with children 1`] { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -10787,7 +13046,7 @@ exports[`Test moveBlocksUp > Multiple blocks starting in nested block 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -11354,7 +13613,7 @@ exports[`Test moveBlocksUp > Out of children 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts index 8c637d5985..763de289c5 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts @@ -2,7 +2,10 @@ import { NodeSelection, TextSelection } from "prosemirror-state"; import { CellSelection } from "prosemirror-tables"; import { describe, expect, it } from "vitest"; -import { getBlockInfoFromTransaction } from "../../../getBlockInfoFromPos.js"; +import { + getBlockInfoFromTransaction, + getNearestBlockPos, +} from "../../../getBlockInfoFromPos.js"; import { setupTestEnv } from "../../setupTestEnv.js"; import { moveBlocksDown, @@ -204,6 +207,45 @@ describe("Test moveBlocksUp", () => { expect(getEditor().document).toMatchSnapshot(); }); + + it("Explicit block argument moves the given block", () => { + getEditor().setTextCursorPosition("paragraph-0"); + + moveBlocksUp(getEditor(), "paragraph-2"); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("Explicit block argument does not change the selection", () => { + getEditor().setTextCursorPosition("paragraph-1"); + makeSelectionSpanContent("text"); + + moveBlocksUp(getEditor(), "paragraph-2"); + + const { anchor, head } = getEditor().transact((tr) => tr.selection); + const anchorBlockId = getEditor().transact( + (tr) => getNearestBlockPos(tr.doc, anchor).node.attrs.id, + ); + const headBlockId = getEditor().transact( + (tr) => getNearestBlockPos(tr.doc, head).node.attrs.id, + ); + expect(anchorBlockId).toBe("paragraph-1"); + expect(headBlockId).toBe("paragraph-1"); + }); + + it("Explicit block argument with first block is a no-op", () => { + const documentBefore = getEditor().document; + + moveBlocksUp(getEditor(), "paragraph-0"); + + expect(getEditor().document).toEqual(documentBefore); + }); + + it("Explicit block argument with nested block", () => { + moveBlocksUp(getEditor(), "nested-paragraph-1"); + + expect(getEditor().document).toMatchSnapshot(); + }); }); describe("Test moveBlocksDown", () => { @@ -232,7 +274,7 @@ describe("Test moveBlocksDown", () => { }); it("Last block", () => { - getEditor().setTextCursorPosition("trailing-paragraph"); + getEditor().setTextCursorPosition("paragraph-9"); moveBlocksDown(getEditor()); @@ -286,4 +328,43 @@ describe("Test moveBlocksDown", () => { expect(getEditor().document).toMatchSnapshot(); }); + + it("Explicit block argument moves the given block", () => { + getEditor().setTextCursorPosition("paragraph-9"); + + moveBlocksDown(getEditor(), "paragraph-0"); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("Explicit block argument does not change the selection", () => { + getEditor().setTextCursorPosition("paragraph-1"); + makeSelectionSpanContent("text"); + + moveBlocksDown(getEditor(), "paragraph-0"); + + const { anchor, head } = getEditor().transact((tr) => tr.selection); + const anchorBlockId = getEditor().transact( + (tr) => getNearestBlockPos(tr.doc, anchor).node.attrs.id, + ); + const headBlockId = getEditor().transact( + (tr) => getNearestBlockPos(tr.doc, head).node.attrs.id, + ); + expect(anchorBlockId).toBe("paragraph-1"); + expect(headBlockId).toBe("paragraph-1"); + }); + + it("Explicit block argument with last block is a no-op", () => { + const documentBefore = getEditor().document; + + moveBlocksDown(getEditor(), "trailing-paragraph"); + + expect(getEditor().document).toEqual(documentBefore); + }); + + it("Explicit block argument with nested block", () => { + moveBlocksDown(getEditor(), "nested-paragraph-0"); + + expect(getEditor().document).toMatchSnapshot(); + }); }); diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts index 8d4591123e..bb2f08dfca 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts @@ -123,29 +123,50 @@ function updateBlockSelectionFromData( tr.setSelection(selection); } -/** - * Replaces any `columnList` blocks with the children of their columns. This is - * done here instead of in `getSelection` as we still need to remove the entire - * `columnList` node but only insert the `blockContainer` nodes inside it. - * @param blocks The blocks to flatten. - */ +// Replaces top-level `column` blocks with their children, as a `column` is not +// a valid block outside a `columnList`. Other blocks are returned as-is. function flattenColumns( blocks: Block[], ): Block[] { - return blocks - .map((block) => { - if (block.type === "columnList") { - return block.children - .map((column) => flattenColumns(column.children)) - .flat(); + return blocks.flatMap((block) => + block.type === "column" ? block.children : [block], + ); +} + +/** + * Removes the given blocks from the editor, then inserts them before/after a + * reference block. + * @param editor The BlockNote editor instance to move the blocks in. + * @param blocks The blocks to move. + * @param referenceBlock The reference block to insert the blocks before/after. + * @param placement Whether to insert the blocks before or after the reference + * block. + */ +export function moveBlocks( + editor: BlockNoteEditor, + blocks: Block[], + referenceBlock: BlockIdentifier, + placement: "before" | "after", +) { + editor.transact(() => { + // A `columnList` reference can be dissolved by `fixColumnList` when its + // `column`s are removed, leaving its ID invalid for re-insertion. Anchor + // to an adjacent block instead, which is unaffected by the removal. + const refBlock = editor.getBlock(referenceBlock); + if (refBlock?.type === "columnList") { + const adjacent = + placement === "after" + ? editor.getNextBlock(refBlock) + : editor.getPrevBlock(refBlock); + if (adjacent) { + referenceBlock = adjacent; + placement = placement === "after" ? "before" : "after"; } + } - return { - ...block, - children: flattenColumns(block.children), - }; - }) - .flat(); + editor.removeBlocks(blocks); + editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement); + }); } /** @@ -170,8 +191,7 @@ export function moveSelectedBlocksAndSelection( ]; const selectionData = getBlockSelectionData(editor); - editor.removeBlocks(blocks); - editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement); + moveBlocks(editor, blocks, referenceBlock, placement); updateBlockSelectionFromData(tr, selectionData); }); @@ -289,50 +309,91 @@ function getMoveDownPlacement( return { referenceBlock, placement }; } -export function moveBlocksUp(editor: BlockNoteEditor) { +export function moveBlocksUp( + editor: BlockNoteEditor, + blockIdentifier?: BlockIdentifier, +) { editor.transact(() => { - const selection = editor.getSelection(); - const block = selection?.blocks[0] || editor.getTextCursorPosition().block; + let sourceBlock: Block | undefined; + if (blockIdentifier) { + sourceBlock = editor.getBlock(blockIdentifier); + if (!sourceBlock) { + return; + } + } else { + const selection = editor.getSelection(); + sourceBlock = + selection?.blocks[0] || editor.getTextCursorPosition().block; + } const moveUpPlacement = getMoveUpPlacement( editor, - editor.getPrevBlock(block), - editor.getParentBlock(block), + editor.getPrevBlock(sourceBlock), + editor.getParentBlock(sourceBlock), ); if (!moveUpPlacement) { return; } - moveSelectedBlocksAndSelection( - editor, - moveUpPlacement.referenceBlock, - moveUpPlacement.placement, - ); + if (blockIdentifier) { + moveBlocks( + editor, + [sourceBlock], + moveUpPlacement.referenceBlock, + moveUpPlacement.placement, + ); + } else { + moveSelectedBlocksAndSelection( + editor, + moveUpPlacement.referenceBlock, + moveUpPlacement.placement, + ); + } }); } -export function moveBlocksDown(editor: BlockNoteEditor) { +export function moveBlocksDown( + editor: BlockNoteEditor, + blockIdentifier?: BlockIdentifier, +) { editor.transact(() => { - const selection = editor.getSelection(); - const block = - selection?.blocks[selection?.blocks.length - 1] || - editor.getTextCursorPosition().block; + let sourceBlock: Block | undefined; + if (blockIdentifier) { + sourceBlock = editor.getBlock(blockIdentifier); + if (!sourceBlock) { + return; + } + } else { + const selection = editor.getSelection(); + sourceBlock = + selection?.blocks[selection?.blocks.length - 1] || + editor.getTextCursorPosition().block; + } const moveDownPlacement = getMoveDownPlacement( editor, - editor.getNextBlock(block), - editor.getParentBlock(block), + editor.getNextBlock(sourceBlock), + editor.getParentBlock(sourceBlock), ); if (!moveDownPlacement) { return; } - moveSelectedBlocksAndSelection( - editor, - moveDownPlacement.referenceBlock, - moveDownPlacement.placement, - ); + if (blockIdentifier) { + moveBlocks( + editor, + [sourceBlock], + moveDownPlacement.referenceBlock, + moveDownPlacement.placement, + ); + } else { + moveSelectedBlocksAndSelection( + editor, + moveDownPlacement.referenceBlock, + moveDownPlacement.placement, + ); + } }); } diff --git a/packages/core/src/api/blockManipulation/commands/nestBlock/__snapshots__/nestBlock.test.ts.snap b/packages/core/src/api/blockManipulation/commands/nestBlock/__snapshots__/nestBlock.test.ts.snap index f2e45772c1..95a5419dab 100644 --- a/packages/core/src/api/blockManipulation/commands/nestBlock/__snapshots__/nestBlock.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/nestBlock/__snapshots__/nestBlock.test.ts.snap @@ -71,17 +71,6 @@ exports[`unnestBlock / liftListItem > BLO-835: unnest block with siblings after }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -139,17 +128,6 @@ exports[`unnestBlock / liftListItem > BLO-835: unnest block with siblings after }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -241,17 +219,6 @@ exports[`unnestBlock / liftListItem > BLO-835: unnest block with siblings after }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -326,17 +293,6 @@ exports[`unnestBlock / liftListItem > BLO-844/847: unnest with complex nesting a }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -376,17 +332,6 @@ exports[`unnestBlock / liftListItem > BLO-844/847: unnest with complex nesting a }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -514,17 +459,6 @@ exports[`unnestBlock / liftListItem > BLO-899: Shift-Tab on second-level nested }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -600,17 +534,6 @@ exports[`unnestBlock / liftListItem > BLO-899: Shift-Tab on second-level nested }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -703,17 +626,6 @@ exports[`unnestBlock / liftListItem > BLO-953: unnest block with multi-level nes }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -789,17 +701,6 @@ exports[`unnestBlock / liftListItem > BLO-953: unnest block with multi-level nes }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -891,17 +792,6 @@ exports[`unnestBlock / liftListItem > Edge cases > should handle unnesting block }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -961,17 +851,6 @@ exports[`unnestBlock / liftListItem > Edge cases > should handle unnesting with }, "type": "heading", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -1012,17 +891,6 @@ exports[`unnestBlock / liftListItem > nestBlock > should nest a block under its }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; @@ -1080,16 +948,5 @@ exports[`unnestBlock / liftListItem > nestBlock > should nest into a sibling tha }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] `; diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap index d255acf235..d876b31175 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap @@ -470,7 +470,7 @@ exports[`Test replaceBlocks > Remove multiple consecutive blocks 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -767,7 +767,7 @@ exports[`Test replaceBlocks > Remove multiple non-consecutive blocks 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1318,7 +1318,7 @@ exports[`Test replaceBlocks > Remove single block 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1850,7 +1850,7 @@ exports[`Test replaceBlocks > Replace multiple consecutive blocks with multiple { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -2342,7 +2342,7 @@ exports[`Test replaceBlocks > Replace multiple consecutive blocks with single ba { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -2892,7 +2892,7 @@ exports[`Test replaceBlocks > Replace multiple consecutive blocks with single co { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3240,7 +3240,7 @@ exports[`Test replaceBlocks > Replace multiple non-consecutive blocks with multi { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3548,7 +3548,7 @@ exports[`Test replaceBlocks > Replace multiple non-consecutive blocks with singl { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3914,7 +3914,7 @@ exports[`Test replaceBlocks > Replace multiple non-consecutive blocks with singl { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -4516,7 +4516,7 @@ exports[`Test replaceBlocks > Replace single block with multiple 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -5078,7 +5078,7 @@ exports[`Test replaceBlocks > Replace single block with single basic 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -5698,7 +5698,7 @@ exports[`Test replaceBlocks > Replace single block with single complex 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts index 04a2425a33..f1e946f909 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -27,9 +27,11 @@ export function removeAndInsertBlocks< const pmSchema = getPmSchema(tr); // Converts the `PartialBlock`s to ProseMirror nodes to insert them into the // document. - const nodesToInsert: Node[] = blocksToInsert.map((block) => - blockToNode(block, pmSchema), - ); + const nodesToInsert: Node[] = blocksToInsert.map((block) => { + const node = blockToNode(block, pmSchema); + node.check(); // `blockToNode` is lenient; validate before mutating the doc + return node; + }); const idsOfBlocksToRemove = new Set( blocksToRemove.map((block) => diff --git a/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap b/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap index 60c3d1c1ed..8cd297eaee 100644 --- a/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap @@ -574,7 +574,7 @@ exports[`Test splitBlocks > Basic 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1159,7 +1159,7 @@ exports[`Test splitBlocks > Block has children 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1744,7 +1744,7 @@ exports[`Test splitBlocks > Don't keep props 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -2329,7 +2329,7 @@ exports[`Test splitBlocks > Don't keep type 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -2908,7 +2908,7 @@ exports[`Test splitBlocks > End of content 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3495,7 +3495,7 @@ exports[`Test splitBlocks > Keep type 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap b/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap index 3246168815..e4559884da 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap @@ -629,7 +629,7 @@ exports[`Test updateBlock > Revert all props 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1269,7 +1269,7 @@ exports[`Test updateBlock > Revert single prop 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -1909,7 +1909,7 @@ exports[`Test updateBlock > Update all props 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -2549,7 +2549,7 @@ exports[`Test updateBlock > Update children 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -3131,7 +3131,7 @@ exports[`Test updateBlock > Update inline content to no content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -4055,7 +4055,7 @@ exports[`Test updateBlock > Update inline content to table content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -4634,7 +4634,7 @@ exports[`Test updateBlock > Update no content to empty inline content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -5217,7 +5217,7 @@ exports[`Test updateBlock > Update no content to empty table content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -5808,7 +5808,7 @@ exports[`Test updateBlock > Update no content to inline content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -6735,7 +6735,7 @@ exports[`Test updateBlock > Update no content to table content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -7303,7 +7303,7 @@ exports[`Test updateBlock > Update partial (offset start + end) 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -7864,7 +7864,7 @@ exports[`Test updateBlock > Update partial (offset start) 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -8425,7 +8425,7 @@ exports[`Test updateBlock > Update partial (props + offset end) 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -8993,7 +8993,7 @@ exports[`Test updateBlock > Update partial (table cell) 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -9561,7 +9561,7 @@ exports[`Test updateBlock > Update partial (table row) 1`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -10201,7 +10201,7 @@ exports[`Test updateBlock > Update single prop 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -10609,7 +10609,7 @@ exports[`Test updateBlock > Update table content to empty inline content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -11029,7 +11029,7 @@ exports[`Test updateBlock > Update table content to inline content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -11443,7 +11443,7 @@ exports[`Test updateBlock > Update table content to no content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -12079,7 +12079,7 @@ exports[`Test updateBlock > Update type 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -12691,7 +12691,7 @@ exports[`Test updateBlock > Update with plain content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -13331,7 +13331,7 @@ exports[`Test updateBlock > Update with styled content 2`] = ` { "children": [], "content": [], - "id": "trailing-paragraph", + "id": "paragraph-9", "props": { "backgroundColor": "default", "textAlignment": "left", diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index 4318b19ca7..a3e2b3b0db 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -128,16 +128,18 @@ export function updateBlockTr< // for this, we do a nodeToBlock on the existing block to get the children. // it would be cleaner to use a ReplaceAroundStep, but this is a bit simpler and it's quite an edge case const existingBlock = nodeToBlock(blockInfo.bnBlock.node, pmSchema); + const replacementNode = blockToNode( + { + children: existingBlock.children, // if no children are passed in, use existing children + ...block, + }, + pmSchema, + ); + replacementNode.check(); // `blockToNode` is lenient; validate before mutating the doc tr.replaceWith( blockInfo.bnBlock.beforePos, blockInfo.bnBlock.afterPos, - blockToNode( - { - children: existingBlock.children, // if no children are passed in, use existing children - ...block, - }, - pmSchema, - ), + replacementNode, ); return; @@ -278,7 +280,9 @@ function updateChildren< const pmSchema = getPmSchema(tr); if (block.children !== undefined && block.children.length > 0) { const childNodes = block.children.map((child) => { - return blockToNode(child, pmSchema); + const node = blockToNode(child, pmSchema); + node.check(); // `blockToNode` is lenient; validate before mutating the doc + return node; }); // Checks if a blockGroup node already exists. diff --git a/packages/core/src/api/blockManipulation/setupTestEnv.ts b/packages/core/src/api/blockManipulation/setupTestEnv.ts index 37b4807063..bd1caf6300 100644 --- a/packages/core/src/api/blockManipulation/setupTestEnv.ts +++ b/packages/core/src/api/blockManipulation/setupTestEnv.ts @@ -185,7 +185,7 @@ const testDocument: PartialBlock[] = [ ], }, { - id: "trailing-paragraph", + id: "paragraph-9", type: "paragraph", }, ]; diff --git a/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts b/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts index b566dfdbe2..ffb298544f 100644 --- a/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts @@ -1,9 +1,6 @@ import { EditorView } from "prosemirror-view"; -export async function handleVSCodePaste( - event: ClipboardEvent, - view: EditorView, -) { +export function handleVSCodePaste(event: ClipboardEvent, view: EditorView) { const { schema } = view.state; if (!event.clipboardData) { @@ -17,8 +14,7 @@ export async function handleVSCodePaste( } if (!schema.nodes.codeBlock) { - view.pasteText(text); - return true; + return false; } const vscode = event.clipboardData!.getData("vscode-editor-data"); diff --git a/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts b/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts index 415b3cb7be..9fa4ed3c55 100644 --- a/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts +++ b/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts @@ -57,8 +57,13 @@ function defaultPasteHandler({ } if (format === "vscode-editor-data") { - handleVSCodePaste(event, editor.prosemirrorView); - return true; + // If VSCode clipboard data cannot be parsed as a code block, try parsing + // `text/plain` as a fallback. + if (handleVSCodePaste(event, editor.prosemirrorView)) { + return true; + } + + format = "text/plain"; } if (format === "Files") { diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts index 3a6aeaffd5..e150af1309 100644 --- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts +++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts @@ -140,16 +140,29 @@ export function selectedFragmentToHTML< editor, ); - const markdown = cleanHTMLToMarkdown(externalHTML); + // Code blocks are treated differently for copying: text/plain is the raw + // selected text instead of markdown. + const { $from, $to } = view.state.selection; + const parentBlockType = $from.parent.type.name; + const parentBlockSpec = editor.blockImplementations[parentBlockType as any]; + const isPurelyInsideCodeBlock = + $from.sameParent($to) && + parentBlockSpec?.implementation.meta?.code === true; + + const markdown = isPurelyInsideCodeBlock + ? view.state.doc.textBetween($from.pos, $to.pos) + : cleanHTMLToMarkdown(externalHTML); return { clipboardHTML, externalHTML, markdown }; } -const checkIfSelectionInNonEditableBlock = () => { - // Let browser handle event if selection is empty (nothing - // happens). - const selection = window.getSelection(); - if (!selection || selection.isCollapsed) { +const checkIfSelectionInNonEditableBlock = (view: EditorView) => { + // Use ProseMirror's internal selection state to check for empty selection. + // window.getSelection() returns null or a collapsed selection inside Shadow + // DOM (Firefox, Safari, and Chromium edge cases), causing this guard to + // misfire and silently skip clipboard writes. view.state.selection is always + // accurate regardless of DOM mode. + if (view.state.selection.empty) { return true; } @@ -158,16 +171,19 @@ const checkIfSelectionInNonEditableBlock = () => { // non-editable block. We only need to check one node as it's // not possible for the browser selection to start in an // editable block and end in a non-editable one. - let node = selection.focusNode; - while (node) { - if ( - node instanceof HTMLElement && - node.getAttribute("contenteditable") === "false" - ) { - return true; + const selection = window.getSelection(); + if (selection && !selection.isCollapsed) { + let node = selection.focusNode; + while (node) { + if ( + node instanceof HTMLElement && + node.getAttribute("contenteditable") === "false" + ) { + return true; + } + + node = node.parentElement; } - - node = node.parentElement; } return false; @@ -213,7 +229,7 @@ export const createCopyToClipboardExtension = < props: { handleDOMEvents: { copy(view, event) { - if (checkIfSelectionInNonEditableBlock()) { + if (checkIfSelectionInNonEditableBlock(view)) { return true; } @@ -222,7 +238,7 @@ export const createCopyToClipboardExtension = < return true; }, cut(view, event) { - if (checkIfSelectionInNonEditableBlock()) { + if (checkIfSelectionInNonEditableBlock(view)) { return true; } diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts index 894c0f1c1b..72569ffced 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts @@ -37,7 +37,7 @@ export function serializeInlineContentExternalHTML< editor: BlockNoteEditor, blockContent: PartialBlock["content"], serializer: DOMSerializer, - options?: { document?: Document }, + options?: { document?: Document; blockType?: string }, ) { let nodes: Node[]; @@ -45,9 +45,17 @@ export function serializeInlineContentExternalHTML< if (!blockContent) { throw new Error("blockContent is required"); } else if (typeof blockContent === "string") { - nodes = inlineContentToNodes([blockContent], editor.pmSchema); + nodes = inlineContentToNodes( + [blockContent], + editor.pmSchema, + options?.blockType, + ); } else if (Array.isArray(blockContent)) { - nodes = inlineContentToNodes(blockContent, editor.pmSchema); + nodes = inlineContentToNodes( + blockContent, + editor.pmSchema, + options?.blockType, + ); } else if (blockContent.type === "tableContent") { nodes = tableContentToNodes(blockContent, editor.pmSchema); } else { @@ -262,7 +270,7 @@ function serializeBlock< editor, block.content as any, // TODO serializer, - options, + { ...options, blockType: block.type }, ); ret.contentDOM.appendChild(ic); diff --git a/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts b/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts new file mode 100644 index 0000000000..7faa154dc6 --- /dev/null +++ b/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts @@ -0,0 +1,813 @@ +/** + * Custom HTML-to-Markdown serializer for BlockNote. + * Replaces the unified/rehype-remark pipeline with a direct DOM-based implementation. + * + * Input: HTML string from createExternalHTMLExporter + * Output: GFM-compatible markdown string + */ + +/** + * Convert an HTML string (from BlockNote's external HTML exporter) to markdown. + */ +export function htmlToMarkdown(html: string): string { + // Use a temporary element to parse HTML. This works in both browser and + // server (JSDOM) environments, unlike `new DOMParser()` which may not be + // globally available in Node.js. + const container = document.createElement("div"); + container.innerHTML = html; + const result = serializeChildren(container, { + indent: "", + inListItem: false, + }); + return result.trim() + "\n"; +} + +interface SerializeContext { + indent: string; // current indentation prefix for list nesting + // True when the current node is being serialized as continuation content + // of a parent list item. Used to suppress trailing blank lines that would + // otherwise turn the parent list into a "loose" list. + inListItem: boolean; +} + +// ─── Main Serializer ───────────────────────────────────────────────────────── + +function serializeChildren(node: Node, ctx: SerializeContext): string { + let result = ""; + const children = Array.from(node.childNodes); + + for (let i = 0; i < children.length; i++) { + const child = children[i]; + result += serializeNode(child, ctx); + } + + return result; +} + +function serializeNode(node: Node, ctx: SerializeContext): string { + if (node.nodeType === 3 /* Node.TEXT_NODE */) { + return node.textContent || ""; + } + + if (node.nodeType !== 1 /* Node.ELEMENT_NODE */) { + return ""; + } + + const el = node as HTMLElement; + const tag = el.tagName.toLowerCase(); + + switch (tag) { + case "p": + return serializeParagraph(el, ctx); + case "h1": + case "h2": + case "h3": + case "h4": + case "h5": + case "h6": + return serializeHeading(el, ctx); + case "blockquote": + return serializeBlockquote(el, ctx); + case "pre": + return serializeCodeBlock(el, ctx); + case "ul": + return serializeUnorderedList(el, ctx); + case "ol": + return serializeOrderedList(el, ctx); + case "table": + return serializeTable(el, ctx); + case "hr": + return ctx.indent + "***\n\n"; + case "img": + return serializeImage(el, ctx); + case "video": + return serializeVideo(el, ctx); + case "audio": + return serializeAudio(el, ctx); + case "embed": + return serializeEmbed(el, ctx); + case "figure": + return serializeFigure(el, ctx); + case "a": + // Block-level link (file block) + return serializeBlockLink(el, ctx); + case "details": + return serializeDetails(el, ctx); + case "div": + // Page break or generic container — serialize children + return serializeChildren(el, ctx); + case "br": + return ""; + default: + return serializeChildren(el, ctx); + } +} + +// ─── Block Serializers ─────────────────────────────────────────────────────── + +function serializeParagraph(el: HTMLElement, ctx: SerializeContext): string { + const content = serializeInlineContent(el); + // Trim leading/trailing hard breaks (matching remark behavior) + const trimmed = trimHardBreaks(content); + if (ctx.inListItem) { + return trimmed; + } + return ctx.indent + trimmed + "\n\n"; +} + +function serializeHeading(el: HTMLElement, ctx: SerializeContext): string { + const level = parseInt(el.tagName[1], 10); + const prefix = "#".repeat(level) + " "; + const content = serializeInlineContent(el); + return ctx.indent + prefix + content + "\n\n"; +} + +function serializeBlockquote(el: HTMLElement, ctx: SerializeContext): string { + // Check if blockquote contains block-level elements (like

) + const blockChildren = Array.from(el.children).filter((child) => { + const tag = child.tagName.toLowerCase(); + return ["p", "ul", "ol", "pre", "blockquote", "table", "hr"].includes(tag); + }); + + let content: string; + if (blockChildren.length > 0) { + // Has block-level children — serialize each + const parts: string[] = []; + for (const child of blockChildren) { + const tag = child.tagName.toLowerCase(); + if (tag === "p") { + parts.push(serializeInlineContent(child as HTMLElement)); + } else { + const innerCtx: SerializeContext = { indent: "", inListItem: false }; + parts.push(serializeNode(child, innerCtx).trim()); + } + } + content = parts.join("\n\n"); + } else { + // No block-level children — treat entire content as inline + content = serializeInlineContent(el); + } + + const lines = content.split("\n"); + return lines.map((line) => ctx.indent + "> " + line).join("\n") + "\n\n"; +} + +function serializeCodeBlock(el: HTMLElement, ctx: SerializeContext): string { + const codeEl = el.querySelector("code"); + if (!codeEl) {return "";} + + const language = + codeEl.getAttribute("data-language") || + extractLanguageFromClass(codeEl.className) || + ""; + + // Extract code content, handling
elements as newlines + const code = extractCodeContent(codeEl); + + // Use a fence longer than the longest backtick run in the code + const longestRun = Math.max( + 0, + ...((code.match(/`+/g) ?? []).map((run) => run.length)) + ); + const fence = "`".repeat(Math.max(3, longestRun + 1)); + + // For empty code blocks, don't add a newline between the fences + if (!code) { + return ctx.indent + fence + language + "\n" + fence + "\n\n"; + } + + return ( + ctx.indent + + fence + + language + + "\n" + + code + + (code.endsWith("\n") ? "" : "\n") + + fence + + "\n\n" + ); +} + +function extractCodeContent(el: Element): string { + let result = ""; + for (const child of Array.from(el.childNodes)) { + if (child.nodeType === 3 /* Node.TEXT_NODE */) { + result += child.textContent || ""; + } else if (child.nodeType === 1 /* Node.ELEMENT_NODE */) { + const tag = (child as HTMLElement).tagName.toLowerCase(); + if (tag === "br") { + result += "\n"; + } else { + result += extractCodeContent(child as Element); + } + } + } + return result; +} + +function extractLanguageFromClass(className: string): string { + const match = className.match(/language-(\S+)/); + return match ? match[1] : ""; +} + +function serializeUnorderedList( + el: HTMLElement, + ctx: SerializeContext +): string { + let result = ""; + const items = Array.from(el.children).filter( + (child) => child.tagName.toLowerCase() === "li" + ); + + for (const item of items) { + result += serializeListItem(item as HTMLElement, "bullet", ctx); + } + + // Trailing blank line separates the list from the next block. Skip when + // this list is nested inside another list item — adding it would convert + // the parent list into a "loose" list (or break tightness). + if (!ctx.inListItem) { + result += "\n"; + } + return result; +} + +function serializeOrderedList(el: HTMLElement, ctx: SerializeContext): string { + let result = ""; + const items = Array.from(el.children).filter( + (child) => child.tagName.toLowerCase() === "li" + ); + const startNum = parseInt(el.getAttribute("start") || "1", 10); + + for (let i = 0; i < items.length; i++) { + const num = startNum + i; + result += serializeListItem(items[i] as HTMLElement, "ordered", ctx, num); + } + + if (!ctx.inListItem) { + result += "\n"; + } + return result; +} + +function serializeListItem( + el: HTMLElement, + listType: "bullet" | "ordered", + ctx: SerializeContext, + num?: number +): string { + // Check for checkbox (task list) - direct children only + let checkbox: HTMLInputElement | null = null; + let details: HTMLElement | null = null; + + for (const child of Array.from(el.children)) { + const tag = child.tagName.toLowerCase(); + if (tag === "input" && (child as HTMLInputElement).type === "checkbox") { + checkbox = child as HTMLInputElement; + } + if (tag === "details") { + details = child as HTMLElement; + } + } + + let marker: string; + let markerWidth: number; + + if (checkbox) { + const state = checkbox.checked ? "[x]" : "[ ]"; + marker = `* ${state} `; + // For child indentation, use bullet width (2), not full checkbox marker width + markerWidth = 2; + } else if (listType === "ordered") { + marker = `${num}. `; + markerWidth = marker.length; + } else { + marker = "* "; + markerWidth = 2; + } + + // Collect the item's inline content + let inlineContent: string; + let firstContentEl: Element | null; + + if (details) { + // Toggle item: get content from summary + const summary = details.querySelector("summary"); + const summaryP = summary?.querySelector("p"); + firstContentEl = details; + inlineContent = summaryP ? serializeInlineContent(summaryP) : ""; + } else { + firstContentEl = getFirstContentElement(el, checkbox); + inlineContent = firstContentEl ? serializeInlineContent(firstContentEl) : ""; + } + + // The marker line ends with a single `\n` so that consecutive list items + // produce a "tight" list (no blank line between markers). Continuation + // content within the item (nested lists, continuation paragraphs, other + // blocks) injects its own spacing as needed. + let result = ctx.indent + marker + inlineContent + "\n"; + + // Serialize child content (nested lists, continuation paragraphs, etc.) + const childIndent = ctx.indent + " ".repeat(markerWidth); + const childCtx: SerializeContext = { indent: childIndent, inListItem: true }; + + // For toggle items, also serialize children inside the details element + if (details) { + const summary = details.querySelector("summary"); + for (const child of Array.from(details.children)) { + if (child === summary) {continue;} + const childTag = child.tagName.toLowerCase(); + if (childTag === "p") { + const content = serializeInlineContent(child as HTMLElement); + // Continuation paragraph needs a blank line to separate it from the + // previous content; CommonMark would otherwise treat it as a soft + // wrap of that content. + result += "\n" + childIndent + content + "\n"; + } else { + result += serializeNode(child, childCtx); + } + } + } + + const children = Array.from(el.children); + for (const child of children) { + const childTag = child.tagName.toLowerCase(); + + // Skip the first content element and checkbox + if (child === firstContentEl || (child as HTMLElement) === checkbox) {continue;} + if (childTag === "input") {continue;} + + // Nested lists and other block content + if (childTag === "ul" || childTag === "ol") { + // Nested list flows directly under the parent marker — no blank line. + result += serializeNode(child, childCtx); + } else if (childTag === "p") { + // Continuation paragraph within list item — requires blank line before + // so it isn't read as part of the marker line's text. + const content = serializeInlineContent(child as HTMLElement); + result += "\n" + childIndent + content + "\n"; + } else { + // Other block-level children (code blocks, blockquotes, etc.) already + // emit their own separating newlines; prefix with a blank line so they + // are recognized as separate blocks. + result += "\n" + serializeNode(child, childCtx); + } + } + + return result; +} + +function getFirstContentElement( + li: HTMLElement, + checkbox: HTMLInputElement | null +): HTMLElement | null { + for (const child of Array.from(li.children)) { + if (child === checkbox) {continue;} + if (child.tagName.toLowerCase() === "input") {continue;} + const tag = child.tagName.toLowerCase(); + if (tag === "p" || tag === "span") {return child as HTMLElement;} + } + return null; +} + +// ─── Table Serializer ──────────────────────────────────────────────────────── + +function serializeTable(el: HTMLElement, ctx: SerializeContext): string { + // First, determine column count from colgroup or first row + const colgroup = el.querySelector("colgroup"); + let colCount = 0; + + if (colgroup) { + colCount = colgroup.querySelectorAll("col").length; + } + + const rows: string[][] = []; + let hasHeader = false; + + // Collect all rows, handling colspan/rowspan + const trElements = el.querySelectorAll("tr"); + // Build a grid to handle colspan/rowspan + const grid: (string | null)[][] = []; + + trElements.forEach((tr, rowIdx) => { + if (!grid[rowIdx]) {grid[rowIdx] = [];} + const cellElements = tr.querySelectorAll("th, td"); + let gridCol = 0; + + cellElements.forEach((cell) => { + // Find next empty column in this row + while (grid[rowIdx][gridCol] !== undefined) {gridCol++;} + + if (rowIdx === 0 && cell.tagName.toLowerCase() === "th") { + hasHeader = true; + } + + const content = escapeTableCell( + serializeInlineContent(cell as HTMLElement).trim() + ); + const colspan = parseInt(cell.getAttribute("colspan") || "1", 10); + const rowspan = parseInt(cell.getAttribute("rowspan") || "1", 10); + + // Fill the grid + for (let r = 0; r < rowspan; r++) { + for (let c = 0; c < colspan; c++) { + const ri = rowIdx + r; + if (!grid[ri]) {grid[ri] = [];} + grid[ri][gridCol + c] = r === 0 && c === 0 ? content : ""; + } + } + + gridCol += colspan; + }); + + // Update colCount + if (grid[rowIdx]) { + colCount = Math.max(colCount, grid[rowIdx].length); + } + }); + + // Convert grid to rows + for (const gridRow of grid) { + const row: string[] = []; + for (let c = 0; c < colCount; c++) { + row.push(gridRow && gridRow[c] !== undefined ? (gridRow[c] ?? "") : ""); + } + rows.push(row); + } + + if (rows.length === 0) {return "";} + + // Determine column widths + const colWidths: number[] = []; + for (let c = 0; c < colCount; c++) { + let maxWidth = 3; // minimum width for separator "---" + for (const row of rows) { + const cellWidth = c < row.length ? row[c].length : 0; + maxWidth = Math.max(maxWidth, cellWidth); + } + // Use minimum of 10 to match remark output + colWidths.push(Math.max(maxWidth, 10)); + } + + let result = ""; + + if (hasHeader) { + result += ctx.indent + formatTableRow(rows[0], colWidths, colCount) + "\n"; + result += ctx.indent + formatSeparatorRow(colWidths, colCount) + "\n"; + for (let r = 1; r < rows.length; r++) { + result += + ctx.indent + formatTableRow(rows[r], colWidths, colCount) + "\n"; + } + } else { + // No header — emit empty header + separator + const emptyRow = new Array(colCount).fill(""); + result += ctx.indent + formatTableRow(emptyRow, colWidths, colCount) + "\n"; + result += ctx.indent + formatSeparatorRow(colWidths, colCount) + "\n"; + for (const row of rows) { + result += + ctx.indent + formatTableRow(row, colWidths, colCount) + "\n"; + } + } + + result += "\n"; + return result; +} + +function escapeTableCell(text: string): string { + return text.replace(/\|/g, "\\|"); +} + +function formatTableRow( + cells: string[], + colWidths: number[], + colCount: number +): string { + const parts: string[] = []; + for (let c = 0; c < colCount; c++) { + const cell = c < cells.length ? cells[c] : ""; + parts.push(" " + cell.padEnd(colWidths[c]) + " "); + } + return "|" + parts.join("|") + "|"; +} + +function formatSeparatorRow(colWidths: number[], colCount: number): string { + const parts: string[] = []; + for (let c = 0; c < colCount; c++) { + parts.push(" " + "-".repeat(colWidths[c]) + " "); + } + return "|" + parts.join("|") + "|"; +} + +// ─── Media Serializers ─────────────────────────────────────────────────────── + +function serializeImage(el: HTMLElement, ctx: SerializeContext): string { + const src = el.getAttribute("src") || ""; + const alt = el.getAttribute("alt") || ""; + // Empty placeholder — preserve the block-level break, matching how + // serializeParagraph/serializeHeading emit `\n\n` for empty content. + if (!src) {return "\n\n";} + return ctx.indent + `![${alt}](${src})\n\n`; +} + +function serializeVideo(el: HTMLElement, ctx: SerializeContext): string { + const src = + el.getAttribute("src") || el.getAttribute("data-url") || ""; + const name = el.getAttribute("data-name") || el.getAttribute("title") || ""; + if (!src) {return "\n\n";} + return ctx.indent + `![${name}](${src})\n\n`; +} + +function serializeAudio(el: HTMLElement, ctx: SerializeContext): string { + const src = el.getAttribute("src") || ""; + if (!src) {return "\n\n";} + // Audio has no markdown syntax, so emit raw HTML. The markdown parser + // passes

${escapeHtmlText(captionText)}
` + : ""; + return ctx.indent + `
${tag}${captionPart}
\n\n`; +} + +function escapeHtmlAttr(value: string): string { + return value + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(//g, ">"); +} + +function escapeHtmlText(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">"); +} + +function serializeBlockLink(el: HTMLElement, ctx: SerializeContext): string { + const href = el.getAttribute("href") || ""; + const text = el.textContent?.trim() || ""; + if (!href) {return ctx.indent + text + "\n\n";} + return ctx.indent + formatLink(text, href) + "\n\n"; +} + +/** + * Render a link, mirroring the remark-stringify behavior from + * TypeCellOS/BlockNote#2661: when the link label equals the URL (or is + * empty), emit the bare URL so that pasting the link into another input + * produces a valid href instead of ``-autolink brackets or redundant + * `[url](url)` markup. Otherwise emit `[text](url)` with the URL escaped so + * a `)` inside the URL does not prematurely close the destination. + */ +function formatLink(text: string, href: string): string { + if (!text || text === href) { + return href; + } + return `[${text}](${escapeLinkDestination(href)})`; +} + +function escapeLinkDestination(url: string): string { + return url.replace(/[\\()]/g, "\\$&"); +} + +function serializeDetails(el: HTMLElement, ctx: SerializeContext): string { + // Toggle heading or toggle list item + const summary = el.querySelector("summary"); + if (!summary) {return serializeChildren(el, ctx);} + + // Check if summary contains a heading + const heading = summary.querySelector("h1, h2, h3, h4, h5, h6"); + if (heading) { + let result = serializeHeading(heading as HTMLElement, ctx); + // Also serialize non-summary children of details + for (const child of Array.from(el.children)) { + if (child !== summary) { + result += serializeNode(child, ctx); + } + } + return result; + } + + // Otherwise serialize the summary content + return serializeChildren(summary, ctx); +} + +// ─── Inline Content Serializer ─────────────────────────────────────────────── + +function serializeInlineContent(el: Element): string { + let result = ""; + + for (const child of Array.from(el.childNodes)) { + if (child.nodeType === 3 /* Node.TEXT_NODE */) { + result += child.textContent || ""; + } else if (child.nodeType === 1 /* Node.ELEMENT_NODE */) { + const childEl = child as HTMLElement; + const tag = childEl.tagName.toLowerCase(); + + switch (tag) { + case "strong": + case "b": { + const inner = serializeInlineContent(childEl); + const { content, trailing } = extractTrailingWhitespace(inner); + if (content) { + result += `**${content}**${trailing}`; + } else { + // All whitespace — just output it without emphasis + result += trailing; + } + break; + } + case "em": + case "i": { + const inner = serializeInlineContent(childEl); + const { content, trailing } = extractTrailingWhitespace(inner); + if (content) { + result += `*${content}*${trailing}`; + } else { + result += trailing; + } + break; + } + case "s": + case "del": + result += `~~${serializeInlineContent(childEl)}~~`; + break; + case "code": { + const text = childEl.textContent || ""; + const longestRun = Math.max( + 0, + ...((text.match(/`+/g) ?? []).map((run) => run.length)) + ); + const fence = "`".repeat(longestRun + 1); + const needsPadding = + text.startsWith("`") || text.endsWith("`"); + result += fence + (needsPadding ? ` ${text} ` : text) + fence; + break; + } + case "u": + // No markdown equivalent — strip the tag, keep content + result += serializeInlineContent(childEl); + break; + case "a": { + const href = childEl.getAttribute("href") || ""; + const text = serializeInlineContent(childEl); + result += formatLink(text, href); + break; + } + case "br": + result += "\\\n"; + break; + case "span": + // Color spans, etc. — strip the tag, keep content + result += serializeInlineContent(childEl); + break; + case "img": { + const src = childEl.getAttribute("src") || ""; + const alt = childEl.getAttribute("alt") || ""; + result += `![${alt}](${src})`; + break; + } + case "video": { + const src = + childEl.getAttribute("src") || + childEl.getAttribute("data-url") || + ""; + const name = + childEl.getAttribute("data-name") || + childEl.getAttribute("title") || + ""; + result += `![${name}](${src})`; + break; + } + case "p": + // Paragraph inside inline context (e.g., table cell) + result += serializeInlineContent(childEl); + break; + case "input": + // Checkbox in task list — handled at block level + break; + default: + result += serializeInlineContent(childEl); + break; + } + } + } + + return result; +} + +/** + * Extract trailing whitespace from emphasis content. + * Moves trailing spaces outside the emphasis delimiters to produce valid markdown. + * E.g., `Bold ` → `**Bold** ` instead of `**Bold **`. + */ +function extractTrailingWhitespace(text: string): { + content: string; + trailing: string; +} { + const match = text.match(/^(.*?)(\s*)$/); + if (match) { + return { content: match[1], trailing: match[2] }; + } + return { content: text, trailing: "" }; +} + +/** + * Escape leading character after emphasis if it could break parsing. + * For example, "Heading" after "**Bold **" — the 'H' should be escaped + * if the trailing space was escaped. + */ + +/** + * Trim leading/trailing hard breaks from inline content. + * Matches remark behavior where
at start/end of paragraph is dropped. + */ +function trimHardBreaks(content: string): string { + // Remove leading hard breaks + let result = content.replace(/^(\\\n)+/, ""); + // Remove trailing hard breaks produced by `
` + result = result.replace(/(\\\n)+$/, ""); + return result; +} diff --git a/packages/core/src/api/exporters/markdown/markdownExporter.ts b/packages/core/src/api/exporters/markdown/markdownExporter.ts index 23aad8db7c..2f73616dc0 100644 --- a/packages/core/src/api/exporters/markdown/markdownExporter.ts +++ b/packages/core/src/api/exporters/markdown/markdownExporter.ts @@ -1,9 +1,4 @@ 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.js"; import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; @@ -13,25 +8,11 @@ import { StyleSchema, } from "../../../schema/index.js"; import { createExternalHTMLExporter } from "../html/externalHTMLExporter.js"; -import { removeUnderlines } from "./util/removeUnderlinesRehypePlugin.js"; -import { addSpacesToCheckboxes } from "./util/addSpacesToCheckboxesRehypePlugin.js"; -import { convertVideoToMarkdown } from "./util/convertVideoToMarkdownRehypePlugin.js"; +import { htmlToMarkdown } from "./htmlToMarkdown.js"; // Needs to be sync because it's used in drag handler event (SideMenuPlugin) export function cleanHTMLToMarkdown(cleanHTMLString: string) { - const markdownString = unified() - .use(rehypeParse, { fragment: true }) - .use(convertVideoToMarkdown) - .use(removeUnderlines) - .use(addSpacesToCheckboxes) - .use(rehypeRemark) - .use(remarkGfm) - .use(remarkStringify, { - handlers: { text: (node) => node.value }, - }) - .processSync(cleanHTMLString); - - return markdownString.value as string; + return htmlToMarkdown(cleanHTMLString); } export function blocksToMarkdown< diff --git a/packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts b/packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts deleted file mode 100644 index 7c03eb9a64..0000000000 --- a/packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Element as HASTElement, Parent as HASTParent } from "hast"; -import { fromDom } from "hast-util-from-dom"; - -/** - * 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 helper = (tree: HASTParent) => { - if (tree.children && "length" in tree.children && tree.children.length) { - for (let i = tree.children.length - 1; i >= 0; i--) { - const child = tree.children[i]; - const nextChild = - i + 1 < tree.children.length ? tree.children[i + 1] : undefined; - - // Checks for paragraph element after checkbox input element. - if ( - child.type === "element" && - child.tagName === "input" && - child.properties?.type === "checkbox" && - nextChild?.type === "element" && - nextChild.tagName === "p" - ) { - // Converts paragraph to span, otherwise remark will think it needs to - // be on a new line. - nextChild.tagName = "span"; - // Adds a space after the checkbox input element. - nextChild.children.splice( - 0, - 0, - fromDom(document.createTextNode(" ")) as HASTElement, - ); - } else { - helper(child as HASTParent); - } - } - } - }; - - return helper; -} diff --git a/packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts b/packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts deleted file mode 100644 index a7de2e3442..0000000000 --- a/packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Parent as HASTParent } from "hast"; -import { visit } from "unist-util-visit"; - -// Originally, rehypeParse parses videos as links, which is incorrect. -export function convertVideoToMarkdown() { - return (tree: HASTParent) => { - visit(tree, "element", (node, index, parent) => { - if (parent && node.tagName === "video") { - const src = node.properties?.src || node.properties?.["data-url"] || ""; - const name = - node.properties?.title || node.properties?.["data-name"] || ""; - parent.children[index!] = { - type: "text", - value: `![${name}](${src})`, - }; - } - }); - }; -} diff --git a/packages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts b/packages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts deleted file mode 100644 index 5b455d1b53..0000000000 --- a/packages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Element as HASTElement, Parent as HASTParent } from "hast"; - -/** - * Rehype plugin which removes tags. Used to remove underlines before converting HTML to markdown, as Markdown - * doesn't support underlines. - */ -export function removeUnderlines() { - const removeUnderlinesHelper = (tree: HASTParent) => { - let numChildElements = tree.children.length; - - for (let i = 0; i < numChildElements; i++) { - const node = tree.children[i]; - - if (node.type === "element") { - // Recursively removes underlines from child elements. - removeUnderlinesHelper(node); - - if ((node as HASTElement).tagName === "u") { - // Lifts child nodes outside underline element, deletes the underline element, and updates current index & - // the number of child elements. - if (node.children.length > 0) { - tree.children.splice(i, 1, ...node.children); - - const numElementsAdded = node.children.length - 1; - numChildElements += numElementsAdded; - i += numElementsAdded; - } else { - tree.children.splice(i, 1); - - numChildElements--; - i--; - } - } - } - } - }; - - return removeUnderlinesHelper; -} diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts index 206ff8d9fd..d970227a49 100644 --- a/packages/core/src/api/nodeConversions/blockToNode.ts +++ b/packages/core/src/api/nodeConversions/blockToNode.ts @@ -366,8 +366,9 @@ export function blockToNode( groupNode ? [contentNode, groupNode] : contentNode, ); } else if (schema.nodes[block.type].isInGroup("bnBlock")) { - // this is a bnBlock node like Column or ColumnList that directly translates to a prosemirror node - return schema.nodes[block.type].createChecked( + // `create` (not `createChecked`) so partial container blocks pass through; + // callers that mutate the doc validate via `node.check()` before inserting. + return schema.nodes[block.type].create( { id: id, ...block.props, diff --git a/packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap b/packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap index 68c0a1c817..1db488255b 100644 --- a/packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap +++ b/packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap @@ -1,129 +1,144 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Lift nested lists > Lifts multiple bullet lists 1`] = ` -" -
    -
    -
  • Bullet List Item 1
  • -
    -
      -
    • Nested Bullet List Item 1
    • -
    • Nested Bullet List Item 2
    • -
    -
      -
    • Nested Bullet List Item 3
    • -
    • Nested Bullet List Item 4
    • -
    -
    -
    -
  • Bullet List Item 2
  • -
-" +"
    +
  • + Bullet List Item 1 +
    • +
    • + Nested Bullet List Item 1 +
    • +
    • + Nested Bullet List Item 2 +
    • +
      +
    • + Nested Bullet List Item 3 +
    • +
    • + Nested Bullet List Item 4 +
    • +
    +
  • + Bullet List Item 2 +
  • +
" `; exports[`Lift nested lists > Lifts multiple bullet lists with content in between 1`] = ` -" -
    -
    -
  • Bullet List Item 1
  • -
    -
      -
    • Nested Bullet List Item 1
    • -
    • Nested Bullet List Item 2
    • -
    -
    -
    -
    -
  • In between content
  • -
    -
      -
    • Nested Bullet List Item 3
    • -
    • Nested Bullet List Item 4
    • -
    -
    -
    -
  • Bullet List Item 2
  • -
-" +"
    +
  • + Bullet List Item 1 +
    • +
    • + Nested Bullet List Item 1 +
    • +
    • + Nested Bullet List Item 2 +
    • +
  • + In between content +
    • +
    • + Nested Bullet List Item 3 +
    • +
    • + Nested Bullet List Item 4 +
    • +
    +
  • + Bullet List Item 2 +
  • +
" `; exports[`Lift nested lists > Lifts nested bullet lists 1`] = ` -" -
    -
    -
  • Bullet List Item 1
  • -
    -
      -
    • Nested Bullet List Item 1
    • -
    • Nested Bullet List Item 2
    • -
    -
    -
    -
  • Bullet List Item 2
  • -
-" +"
    +
  • + Bullet List Item 1 +
    • +
    • + Nested Bullet List Item 1 +
    • +
    • + Nested Bullet List Item 2 +
    • +
    +
  • + Bullet List Item 2 +
  • +
" `; exports[`Lift nested lists > Lifts nested bullet lists with content after nested list 1`] = ` -" -
    -
    -
  • Bullet List Item 1
  • -
    -
      -
    • Nested Bullet List Item 1
    • -
    • Nested Bullet List Item 2
    • -
    -
    -
    -
  • More content in list item 1
  • -
  • Bullet List Item 2
  • -
-" +"
    +
  • + Bullet List Item 1 +
    • +
    • + Nested Bullet List Item 1 +
    • +
    • + Nested Bullet List Item 2 +
    • +
  • + More content in list item 1 +
  • +
  • + Bullet List Item 2 +
  • +
" `; exports[`Lift nested lists > Lifts nested bullet lists without li 1`] = ` -" -
    Bullet List Item 1 -
      -
    • Nested Bullet List Item 1
    • -
    • Nested Bullet List Item 2
    • -
    -
  • Bullet List Item 2
  • -
-" +"
    + Bullet List Item 1 +
      +
    • + Nested Bullet List Item 1 +
    • +
    • + Nested Bullet List Item 2 +
    • +
    +
  • + Bullet List Item 2 +
  • +
" `; exports[`Lift nested lists > Lifts nested mixed lists 1`] = ` -" -
    -
    -
  1. Numbered List Item 1
  2. -
    -
      -
    • Bullet List Item 1
    • -
    • Bullet List Item 2
    • -
    -
    -
    -
  3. Numbered List Item 2
  4. -
-" +"
    +
  1. + Numbered List Item 1 +
    • +
    • + Bullet List Item 1 +
    • +
    • + Bullet List Item 2 +
    • +
    +
  2. + Numbered List Item 2 +
  3. +
" `; exports[`Lift nested lists > Lifts nested numbered lists 1`] = ` -" -
    -
    -
  1. Numbered List Item 1
  2. -
    -
      -
    1. Nested Numbered List Item 1
    2. -
    3. Nested Numbered List Item 2
    4. -
    -
    -
    -
  3. Numbered List Item 2
  4. -
-" +"
    +
  1. + Numbered List Item 1 +
    1. +
    2. + Nested Numbered List Item 1 +
    3. +
    4. + Nested Numbered List Item 2 +
    5. +
    +
  2. + Numbered List Item 2 +
  3. +
" `; diff --git a/packages/core/src/api/parsers/html/util/nestedLists.test.ts b/packages/core/src/api/parsers/html/util/nestedLists.test.ts index 03fadebefe..e695efa9c4 100644 --- a/packages/core/src/api/parsers/html/util/nestedLists.test.ts +++ b/packages/core/src/api/parsers/html/util/nestedLists.test.ts @@ -1,20 +1,9 @@ import { describe, expect, it } from "vitest"; import { nestedListsToBlockNoteStructure } from "./nestedLists.js"; -import { unified } from "unified"; -import rehypeParse from "rehype-parse"; -import rehypeFormat from "rehype-format"; -import rehypeStringify from "rehype-stringify"; async function testHTML(html: string) { const htmlNode = nestedListsToBlockNoteStructure(html); - - const pretty = await unified() - .use(rehypeParse, { fragment: true }) - .use(rehypeFormat) - .use(rehypeStringify) - .process(htmlNode.innerHTML); - - expect(pretty.value).toMatchSnapshot(); + expect(htmlNode.innerHTML).toMatchSnapshot(); } describe("Lift nested lists", () => { diff --git a/packages/core/src/api/parsers/markdown/markdownToHtml.ts b/packages/core/src/api/parsers/markdown/markdownToHtml.ts new file mode 100644 index 0000000000..6cf94bf4b8 --- /dev/null +++ b/packages/core/src/api/parsers/markdown/markdownToHtml.ts @@ -0,0 +1,1194 @@ +import { isVideoUrl } from "../../../util/string.js"; + +/** + * Custom markdown-to-HTML converter for BlockNote. + * Replaces the unified/remark/rehype pipeline with a direct, minimal implementation + * that handles exactly the markdown features BlockNote needs. + */ + +// ─── HTML Escaping ─────────────────────────────────────────────────────────── + +function escapeHtml(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function isAlphanumeric(char: string | undefined): boolean { + if (!char) { + return false; + } + return /\w/.test(char); +} + +/** + * Returns true when an underscore delimiter at position `i` is "intraword", + * meaning the characters on both sides are alphanumeric (e.g. `snake_case`). + * In that case the underscore should NOT be treated as emphasis per CommonMark. + */ +function isIntraword(text: string, i: number, delimLen: number): boolean { + const before = i > 0 ? text[i - 1] : undefined; + const after = + i + delimLen < text.length ? text[i + delimLen] : undefined; + return isAlphanumeric(before) && isAlphanumeric(after); +} + +// ─── Inline Parser ─────────────────────────────────────────────────────────── + +type InlineTokenizer = ( + text: string, + i: number +) => { html: string; end: number } | null; + +function tryBackslashEscape( + text: string, + i: number +): { html: string; end: number } | null { + if (text[i] !== "\\" || i + 1 >= text.length) {return null;} + const next = text[i + 1]; + // Hard line break: backslash at end of line + if (next === "\n") { + return { html: "
\n", end: i + 2 }; + } + // Escapable characters + if ("\\`*_{}[]()#+-.!~|>".includes(next)) { + return { html: escapeHtml(next), end: i + 2 }; + } + return null; +} + +function tryInlineCode( + text: string, + i: number +): { html: string; end: number } | null { + if (text[i] !== "`") {return null;} + return parseInlineCode(text, i); +} + +function tryImage( + text: string, + i: number +): { html: string; end: number } | null { + if (text[i] !== "!" || text[i + 1] !== "[") {return null;} + return parseImage(text, i); +} + +function tryLink( + text: string, + i: number +): { html: string; end: number } | null { + if (text[i] !== "[") {return null;} + return parseLink(text, i); +} + +function tryStrikethrough( + text: string, + i: number +): { html: string; end: number } | null { + if (text[i] !== "~" || text[i + 1] !== "~") {return null;} + return parseDelimited(text, i, "~~", "", ""); +} + +function tryBoldItalic( + text: string, + i: number +): { html: string; end: number } | null { + if ( + (text[i] === "*" && text[i + 1] === "*" && text[i + 2] === "*") || + (text[i] === "_" && + text[i + 1] === "_" && + text[i + 2] === "_" && + !isIntraword(text, i, 3)) + ) { + const delimiter = text.substring(i, i + 3); + return parseDelimited(text, i, delimiter, "", ""); + } + return null; +} + +function tryBold( + text: string, + i: number +): { html: string; end: number } | null { + if ( + (text[i] === "*" && text[i + 1] === "*") || + (text[i] === "_" && text[i + 1] === "_" && !isIntraword(text, i, 2)) + ) { + const delimiter = text.substring(i, i + 2); + return parseDelimited(text, i, delimiter, "", ""); + } + return null; +} + +function tryItalic( + text: string, + i: number +): { html: string; end: number } | null { + if (text[i] === "*" || (text[i] === "_" && !isIntraword(text, i, 1))) { + return parseDelimited(text, i, text[i], "", ""); + } + return null; +} + +function trySoftBreak( + text: string, + i: number +): { html: string; end: number } | null { + if (text[i] === "\n") { + return { html: "
\n", end: i + 1 }; + } + return null; +} + +// Inline raw HTML: pass through tags, comments, CDATA, processing +// instructions, and declarations verbatim so authors can mix HTML into +// markdown (e.g. `text foo more`). Anything that doesn't match +// these shapes falls through and gets HTML-escaped as plain text. +const INLINE_HTML_TAG_RE = + /^<\/?[a-zA-Z][a-zA-Z0-9-]*(?:\s+[a-zA-Z_:][a-zA-Z0-9_.:-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*\/?>/; +const HTML_COMMENT_RE = /^/; +const HTML_CDATA_RE = /^/; +const HTML_PI_RE = /^<\?[\s\S]*?\?>/; +const HTML_DECL_RE = /^/; + +function tryInlineHtml( + text: string, + i: number +): { html: string; end: number } | null { + if (text[i] !== "<") {return null;} + const rest = text.substring(i); + for (const re of [ + HTML_COMMENT_RE, + HTML_CDATA_RE, + HTML_PI_RE, + HTML_DECL_RE, + INLINE_HTML_TAG_RE, + ]) { + const m = rest.match(re); + if (m) { + return { html: m[0], end: i + m[0].length }; + } + } + return null; +} + +/** Characters that can start an inline syntax token. */ +const SPECIAL_CHARS = new Set("\\`![~*_\n<"); + +/** + * Ordered array of inline tokenizers, tried in priority order. + * The first match wins. + */ +const inlineTokenizers: InlineTokenizer[] = [ + tryBackslashEscape, + tryInlineCode, + tryImage, + tryLink, + tryStrikethrough, + tryBoldItalic, // *** / ___ + tryBold, // ** / __ + tryItalic, // * / _ + tryInlineHtml, + trySoftBreak, +]; + +/** + * Parse inline markdown syntax and return HTML. + * Handles: bold, italic, bold+italic, strikethrough, inline code, + * links, images (with video detection), hard line breaks, backslash escapes. + */ +function parseInline(text: string): string { + let result = ""; + let i = 0; + + while (i < text.length) { + // Hard line break: 2+ trailing spaces immediately before a newline. + // (The other hard-break form, backslash + newline, is handled by + // tryBackslashEscape.) Strip the trailing spaces from the accumulated + // result before emitting the
. + if ( + text[i] === "\n" && + i >= 2 && + text[i - 1] === " " && + text[i - 2] === " " + ) { + result = result.replace(/ +$/, ""); + result += "
\n"; + i++; + continue; + } + + // Try each tokenizer in priority order + let matched = false; + if (SPECIAL_CHARS.has(text[i])) { + for (const tokenizer of inlineTokenizers) { + const r = tokenizer(text, i); + if (r) { + result += r.html; + i = r.end; + matched = true; + break; + } + } + } + + if (!matched) { + // Batch consecutive plain-text characters and escape once + const runStart = i; + i++; + while (i < text.length && !SPECIAL_CHARS.has(text[i])) { + i++; + } + result += escapeHtml(text.substring(runStart, i)); + } + } + + return result; +} + +function parseInlineCode( + text: string, + start: number +): { html: string; end: number } | null { + // Count opening backticks + let openCount = 0; + let i = start; + while (i < text.length && text[i] === "`") { + openCount++; + i++; + } + + // Find matching closing backticks + let j = i; + while (j < text.length) { + if (text[j] === "`") { + let closeCount = 0; + const closeStart = j; + while (j < text.length && text[j] === "`") { + closeCount++; + j++; + } + if (closeCount === openCount) { + let code = text.substring(i, closeStart); + // Per CommonMark: line endings inside a code span are converted to + // single spaces, then if the result starts AND ends with a space and + // is not all-spaces, one leading + trailing space is stripped (so + // `` ` `foo` ` `` is ``foo``). + code = code.replace(/\n/g, " "); + if ( + code.length >= 2 && + code[0] === " " && + code[code.length - 1] === " " && + /[^ ]/.test(code) + ) { + code = code.substring(1, code.length - 1); + } + return { + html: `${escapeHtml(code)}`, + end: j, + }; + } + } else { + j++; + } + } + return null; +} + +function parseImage( + text: string, + start: number +): { html: string; end: number } | null { + // ![alt](url) or ![alt](url "title") + // Use balanced bracket matching to handle nested/escaped brackets in alt text + const altEnd = findClosingBracket(text, start + 1); + if (altEnd === -1) {return null;} + const altStart = start + 2; // after ![ + + if (text[altEnd + 1] !== "(") {return null;} + + const urlStart = altEnd + 2; + const parenEnd = findClosingParen(text, urlStart - 1); + if (parenEnd === -1) {return null;} + + const alt = text.substring(altStart, altEnd); + const { url, title } = parseDestinationAndTitle( + text.substring(urlStart, parenEnd), + ); + + if (isVideoUrl(url)) { + // Use the alt text as the video's display name (falling back to the + // title) so a video link written with the standard `![name](url)` form + // round-trips into BlockNote's video block. Captioned videos go through + // raw `
` HTML instead, see htmlToMarkdown.serializeMediaFigure. + const name = alt || title; + return { + html: ``, + end: parenEnd + 1, + }; + } + + const titleAttr = + title !== undefined ? ` title="${escapeHtml(title)}"` : ""; + return { + html: `${escapeHtml(alt)}`, + end: parenEnd + 1, + }; +} + +function parseLink( + text: string, + start: number +): { html: string; end: number } | null { + // [text](url) + const textStart = start + 1; + const textEnd = findClosingBracket(text, start); + if (textEnd === -1) {return null;} + + if (text[textEnd + 1] !== "(") {return null;} + + const urlStart = textEnd + 2; + const parenEnd = findClosingParen(text, textEnd + 1); + if (parenEnd === -1) {return null;} + + const linkText = text.substring(textStart, textEnd); + const { url, title } = parseDestinationAndTitle( + text.substring(urlStart, parenEnd), + ); + + const titleAttr = + title !== undefined ? ` title="${escapeHtml(title)}"` : ""; + return { + html: `${parseInline(linkText)}`, + end: parenEnd + 1, + }; +} + +function findClosingBracket(text: string, openPos: number): number { + let depth = 0; + for (let i = openPos; i < text.length; i++) { + if (text[i] === "\\" && i + 1 < text.length) { + i++; // skip escaped + continue; + } + if (text[i] === "[") {depth++;} + if (text[i] === "]") { + depth--; + if (depth === 0) {return i;} + } + } + return -1; +} + +function findClosingParen(text: string, openPos: number): number { + let depth = 0; + for (let i = openPos; i < text.length; i++) { + if (text[i] === "\\" && i + 1 < text.length) { + i++; + continue; + } + if (text[i] === "(") {depth++;} + if (text[i] === ")") { + depth--; + if (depth === 0) {return i;} + } + } + return -1; +} + +/** + * Parse the inside of `(...)` from a link/image (the URL and optional title). + * Handles three URL forms: + * - bare: `/uri` or `/uri "title"` + * - angle-bracket: `` or ` "title"` (brackets are stripped) + * And three title-quote forms: `"..."`, `'...'`, `(...)`. + */ +function parseDestinationAndTitle(raw: string): { + url: string; + title?: string; +} { + raw = raw.trim(); + let url: string; + let rest: string; + + if (raw.startsWith("<")) { + const close = raw.indexOf(">"); + if (close === -1) { + // Unmatched `<` — treat the whole thing as the URL minus the `<`. + url = raw.substring(1); + rest = ""; + } else { + url = raw.substring(1, close); + rest = raw.substring(close + 1).trim(); + } + } else { + // Split at first unescaped whitespace. + let split = raw.length; + for (let i = 0; i < raw.length; i++) { + if (raw[i] === "\\" && i + 1 < raw.length) { + i++; + continue; + } + if (raw[i] === " " || raw[i] === "\t" || raw[i] === "\n") { + split = i; + break; + } + } + url = raw.substring(0, split); + rest = raw.substring(split).trim(); + } + + let title: string | undefined; + if (rest.length > 0) { + const titleMatch = rest.match(/^"([^"]*)"$|^'([^']*)'$|^\(([^)]*)\)$/); + if (titleMatch) { + title = titleMatch[1] ?? titleMatch[2] ?? titleMatch[3]; + } + } + + return { url, title }; +} + +function parseDelimited( + text: string, + start: number, + delimiter: string, + openTag: string, + closeTag: string +): { html: string; end: number } | null { + const len = delimiter.length; + const afterOpen = start + len; + + if (afterOpen >= text.length) {return null;} + + // Opening delimiter must not be followed by whitespace + if (text[afterOpen] === " " || text[afterOpen] === "\t") {return null;} + + // Find closing delimiter + let j = afterOpen; + while (j < text.length) { + // Skip escaped characters + if (text[j] === "\\" && j + 1 < text.length) { + j += 2; + continue; + } + + if (text.substring(j, j + len) === delimiter) { + // Closing delimiter must not be preceded by whitespace + if (text[j - 1] === " " || text[j - 1] === "\t") { + j++; + continue; + } + + // For single-char delimiters, don't accept closer if it's part of a + // multi-char run (e.g., don't treat the * in ** as italic closer) + if ( + len === 1 && + ((j > 0 && text[j - 1] === delimiter[0] && !(j >= 2 && text[j - 2] === "\\")) || + (j + len < text.length && text[j + len] === delimiter[0])) + ) { + j++; + continue; + } + + const inner = text.substring(afterOpen, j); + if (inner.length === 0) { + j++; + continue; + } + + return { + html: openTag + parseInline(inner) + closeTag, + end: j + len, + }; + } + j++; + } + + return null; +} + +// ─── Block-Level Types ─────────────────────────────────────────────────────── + +interface BlockToken { + type: string; +} + +interface HeadingToken extends BlockToken { + type: "heading"; + level: number; + content: string; +} + +interface ParagraphToken extends BlockToken { + type: "paragraph"; + content: string; +} + +interface CodeBlockToken extends BlockToken { + type: "codeBlock"; + language: string; + code: string; +} + +interface BlockquoteToken extends BlockToken { + type: "blockquote"; + content: string; +} + +interface HorizontalRuleToken extends BlockToken { + type: "hr"; +} + +interface ListItemToken extends BlockToken { + type: "listItem"; + listType: "bullet" | "ordered" | "task"; + indent: number; + content: string; + start?: number; // for ordered lists + checked?: boolean; // for task lists + childContent?: string; // recursively parsed content within this item +} + +interface TableToken extends BlockToken { + type: "table"; + headers: string[]; + rows: string[][]; + alignments: ("left" | "center" | "right" | null)[]; +} + +interface RawHtmlToken extends BlockToken { + type: "rawHtml"; + content: string; +} + +type Token = + | HeadingToken + | ParagraphToken + | CodeBlockToken + | BlockquoteToken + | HorizontalRuleToken + | ListItemToken + | TableToken + | RawHtmlToken; + +/** + * HTML block-level tag names (from the CommonMark type-6 list, plus `audio` + * which BlockNote serializes as raw HTML since markdown has no shorthand + * for it). When a line starts with `<` followed by one of these tag names, + * the run of non-blank lines is emitted verbatim as raw HTML rather than + * wrapped in a paragraph. + */ +const HTML_BLOCK_TAGS = new Set([ + "address", "article", "aside", "audio", "base", "basefont", "blockquote", + "body", "caption", "center", "col", "colgroup", "dd", "details", "dialog", + "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", + "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", + "header", "hr", "html", "iframe", "legend", "li", "link", "main", "menu", + "menuitem", "nav", "noframes", "ol", "optgroup", "option", "p", "param", + "section", "source", "summary", "table", "tbody", "td", "tfoot", "th", + "thead", "title", "tr", "track", "ul", +]); + +function isHtmlBlockStart(line: string): boolean { + // `, ``, ``, or ``. + // Lines are emitted verbatim until the next blank line. + if (isHtmlBlockStart(line)) { + const htmlLines: string[] = []; + while (i < lines.length && lines[i].trim() !== "") { + htmlLines.push(lines[i]); + i++; + } + tokens.push({ + type: "rawHtml", + content: htmlLines.join("\n"), + }); + prevLineWasBlank = false; + continue; + } + + // Paragraph (default) + const paraLines: string[] = [line]; + i++; + while (i < lines.length) { + const nextLine = lines[i]; + // Stop paragraph on blank line + if (nextLine.trim() === "") {break;} + // Stop on block-level element + if (/^(#{1,6})\s/.test(nextLine)) {break;} + if (/^(`{3,}|~{3,})/.test(nextLine)) {break;} + if (/^\s{0,3}>/.test(nextLine)) {break;} + if (/^(\s{0,3})([-*_])\s*(\2\s*){2,}$/.test(nextLine)) {break;} + if (/^\s*([-*+]|\d+[.)])\s+/.test(nextLine)) {break;} + if (/^\s*\|(.+\|)+\s*$/.test(nextLine)) {break;} + if (isHtmlBlockStart(nextLine)) {break;} + // Check if next-next line is setext marker + if ( + i + 1 < lines.length && + /^[=-]+\s*$/.test(lines[i + 1]) && + nextLine.trim().length > 0 + ) { + break; + } + paraLines.push(nextLine); + i++; + } + // CommonMark allows up to 3 leading spaces of indent on paragraph lines. + // Also strip trailing whitespace from the final line so a trailing + // hard-break sequence (` \n` at end of paragraph) doesn't leak as + // literal trailing spaces in the rendered output. + tokens.push({ + type: "paragraph", + content: paraLines + .map((l) => l.replace(/^ {1,3}/, "")) + .join("\n") + .replace(/[ \t]+$/, ""), + }); + prevLineWasBlank = false; + } + + return tokens; +} + +function tryParseTable( + lines: string[], + start: number +): { token: TableToken; nextLine: number } | null { + // A table needs at least a header row and a separator row + if (start + 1 >= lines.length) {return null;} + + const headerLine = lines[start]; + const separatorLine = lines[start + 1]; + + // Check separator line format: | --- | --- | or --- | --- (outer pipes optional) + // Must contain at least one pipe and only dashes, colons, pipes, and whitespace + if ( + !separatorLine.includes("|") || + !/^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(separatorLine) + ) {return null;} + + // Check header line has at least one pipe (required to distinguish from plain text) + if (!headerLine.includes("|")) {return null;} + + const headers = parsePipeCells(headerLine); + const alignments = parseAlignments(separatorLine); + + const rows: string[][] = []; + let i = start + 2; + while (i < lines.length) { + const line = lines[i]; + if (!line.includes("|")) {break;} + rows.push(parsePipeCells(line)); + i++; + } + + return { + token: { + type: "table", + headers, + rows, + alignments, + }, + nextLine: i, + }; +} + +function parsePipeCells(line: string): string[] { + // Trim leading/trailing pipes and split + const trimmed = line.trim(); + const withoutOuterPipes = trimmed.startsWith("|") + ? trimmed.substring(1) + : trimmed; + const content = withoutOuterPipes.endsWith("|") + ? withoutOuterPipes.substring(0, withoutOuterPipes.length - 1) + : withoutOuterPipes; + + // Split by pipes, handling escaped pipes + const cells: string[] = []; + let current = ""; + for (let i = 0; i < content.length; i++) { + if (content[i] === "\\" && i + 1 < content.length && content[i + 1] === "|") { + current += "|"; + i++; + } else if (content[i] === "|") { + cells.push(current.trim()); + current = ""; + } else { + current += content[i]; + } + } + cells.push(current.trim()); + + return cells; +} + +function parseAlignments( + separatorLine: string +): ("left" | "center" | "right" | null)[] { + const cells = parsePipeCells(separatorLine); + return cells.map((cell) => { + const trimmed = cell.trim(); + const left = trimmed.startsWith(":"); + const right = trimmed.endsWith(":"); + if (left && right) {return "center";} + if (right) {return "right";} + if (left) {return "left";} + return null; + }); +} + +// ─── HTML Emitter ──────────────────────────────────────────────────────────── + +function tokensToHtml(tokens: Token[]): string { + let html = ""; + let i = 0; + + while (i < tokens.length) { + const token = tokens[i]; + + switch (token.type) { + case "heading": { + const t = token as HeadingToken; + html += `${parseInline(t.content)}`; + i++; + break; + } + + case "paragraph": { + const t = token as ParagraphToken; + html += `

${parseInline(t.content)}

`; + i++; + break; + } + + case "codeBlock": { + const t = token as CodeBlockToken; + const langAttr = t.language + ? ` data-language="${escapeHtml(t.language)}"` + : ""; + html += `
${escapeHtml(t.code)}
`; + i++; + break; + } + + case "blockquote": { + const t = token as BlockquoteToken; + // Recursively parse blockquote content as markdown + const innerTokens = tokenize(t.content); + const innerHtml = tokensToHtml(innerTokens); + html += `
${innerHtml}
`; + i++; + break; + } + + case "hr": + html += `
`; + i++; + break; + + case "listItem": { + // Collect consecutive list items and build nested list structure + const listHtml = emitListItems(tokens, i); + html += listHtml.html; + i = listHtml.nextIndex; + break; + } + + case "table": { + const t = token as TableToken; + html += emitTable(t); + i++; + break; + } + + case "rawHtml": { + const t = token as RawHtmlToken; + html += t.content; + i++; + break; + } + + default: + i++; + } + } + + return html; +} + +function emitListItems( + tokens: Token[], + startIdx: number +): { html: string; nextIndex: number } { + let html = ""; + let i = startIdx; + let currentListType: "bullet" | "ordered" | null = null; + + while (i < tokens.length && tokens[i].type === "listItem") { + const item = tokens[i] as ListItemToken; + const effectiveType = getEffectiveListType(item.listType); + + // Check if we need to switch list type + if (currentListType !== null && currentListType !== effectiveType) { + // Close current list, open new one + html += ``; + currentListType = null; + } + + // Open list if needed + if (currentListType === null) { + if (effectiveType === "ordered") { + const startAttr = + item.start !== undefined && item.start !== 1 + ? ` start="${item.start}"` + : ""; + html += ``; + } else { + html += `
    `; + } + currentListType = effectiveType; + } + + // Emit list item + if (item.listType === "task") { + const checkedAttr = item.checked ? " checked" : ""; + html += `
  • ${parseInline(item.content)}

    `; + } else { + html += `
  • ${parseInline(item.content)}

    `; + } + + // Render child content (nested items, continuation paragraphs, etc.) + if (item.childContent) { + const childTokens = tokenize(item.childContent); + html += tokensToHtml(childTokens); + } + + html += `
  • `; + i++; + } + + // Close the list + if (currentListType !== null) { + html += ``; + } + + return { html, nextIndex: i }; +} + +function getEffectiveListType( + listType: "bullet" | "ordered" | "task" +): "bullet" | "ordered" { + return listType === "ordered" ? "ordered" : "bullet"; +} + +function emitTable(table: TableToken): string { + let html = ""; + + // BlockNote tables have no required header row, but the markdown table + // syntax does. When we serialize a headerless BlockNote table to markdown + // we emit an empty header row; on re-parse, treat that empty header as + // "no header" so the round-trip is stable (issue #739). + const headerIsEmpty = table.headers.every((h) => h.trim() === ""); + const colCount = table.headers.length; + + if (!headerIsEmpty) { + html += ""; + for (let c = 0; c < colCount; c++) { + const align = table.alignments[c]; + const alignAttr = align ? ` align="${align}"` : ""; + html += `${parseInline(table.headers[c])}`; + } + html += ""; + } + + if (table.rows.length > 0) { + html += ""; + for (const row of table.rows) { + html += ""; + for (let c = 0; c < colCount; c++) { + const cell = c < row.length ? row[c] : ""; + const align = table.alignments[c]; + const alignAttr = align ? ` align="${align}"` : ""; + html += `${parseInline(cell)}`; + } + html += ""; + } + html += ""; + } + + html += "
    "; + return html; +} + +// ─── Public API ────────────────────────────────────────────────────────────── + +/** + * Convert a markdown string to an HTML string. + * This is a direct replacement for the unified/remark/rehype pipeline. + */ +export function markdownToHtml(markdown: string): string { + const tokens = tokenize(markdown); + return tokensToHtml(tokens); +} diff --git a/packages/core/src/api/parsers/markdown/parseMarkdown.ts b/packages/core/src/api/parsers/markdown/parseMarkdown.ts index e98ef00baa..e1741e214e 100644 --- a/packages/core/src/api/parsers/markdown/parseMarkdown.ts +++ b/packages/core/src/api/parsers/markdown/parseMarkdown.ts @@ -1,11 +1,4 @@ import { Schema } from "prosemirror-model"; -import remarkGfm from "remark-gfm"; -import remarkParse from "remark-parse"; -import remarkRehype, { - defaultHandlers as remarkRehypeDefaultHandlers, -} from "remark-rehype"; -import rehypeStringify from "rehype-stringify"; -import { unified } from "unified"; import { Block } from "../../../blocks/defaultBlocks.js"; import { @@ -14,102 +7,10 @@ import { StyleSchema, } from "../../../schema/index.js"; import { HTMLToBlocks } from "../html/parseHTML.js"; -import { isVideoUrl } from "../../../util/string.js"; - -// modified version of https://github.com/syntax-tree/mdast-util-to-hast/blob/main/lib/handlers/code.js -// that outputs a data-language attribute instead of a CSS class (e.g.: language-typescript) -function code(state: any, node: any) { - const value = node.value ? node.value : ""; - /** @type {Properties} */ - const properties: any = {}; - - if (node.lang) { - // changed line - properties["data-language"] = node.lang; - } - - // Create ``. - /** @type {Element} */ - let result: any = { - type: "element", - tagName: "code", - properties, - children: [{ type: "text", value }], - }; - - if (node.meta) { - result.data = { meta: node.meta }; - } - - state.patch(node, result); - result = state.applyData(node, result); - - // Create `
    `.
    -  result = {
    -    type: "element",
    -    tagName: "pre",
    -    properties: {},
    -    children: [result],
    -  };
    -  state.patch(node, result);
    -  return result;
    -}
    -
    -function video(state: any, node: any) {
    -  const url = String(node?.url || "");
    -  const title = node?.title ? String(node.title) : undefined;
    -
    -  let result: any = {
    -    type: "element",
    -    tagName: "video",
    -    properties: {
    -      src: url,
    -      "data-name": title,
    -      "data-url": url,
    -      controls: true,
    -    },
    -    children: [],
    -  };
    -  state.patch?.(node, result);
    -  result = state.applyData ? state.applyData(node, result) : result;
    -
    -  return result;
    -}
    +import { markdownToHtml } from "./markdownToHtml.js";
     
     export function markdownToHTML(markdown: string): string {
    -  const htmlString = unified()
    -    .use(remarkParse)
    -    .use(remarkGfm)
    -    .use(remarkRehype, {
    -      handlers: {
    -        ...(remarkRehypeDefaultHandlers as any),
    -        image: (state: any, node: any) => {
    -          const url = String(node?.url || "");
    -
    -          if (isVideoUrl(url)) {
    -            return video(state, node);
    -          } else {
    -            return remarkRehypeDefaultHandlers.image(state, node);
    -          }
    -        },
    -        code,
    -        blockquote: (state: any, node: any) => {
    -          const result = {
    -            type: "element",
    -            tagName: "blockquote",
    -            properties: {},
    -            // The only difference from the original is that we don't wrap the children with line endings
    -            children: state.wrap(state.all(node), false),
    -          };
    -          state.patch(node, result);
    -          return state.applyData(node, result);
    -        },
    -      },
    -    })
    -    .use(rehypeStringify)
    -    .processSync(markdown);
    -
    -  return htmlString.value as string;
    +  return markdownToHtml(markdown);
     }
     
     export function markdownToBlocks<
    diff --git a/packages/core/src/blocks/Audio/block.ts b/packages/core/src/blocks/Audio/block.ts
    index f271fcb16a..78722cf988 100644
    --- a/packages/core/src/blocks/Audio/block.ts
    +++ b/packages/core/src/blocks/Audio/block.ts
    @@ -129,11 +129,8 @@ export const audioToExternalHTML =
         >,
       ) => {
         if (!block.props.url) {
    -      const div = document.createElement("p");
    -      div.textContent = "Add audio";
    -
           return {
    -        dom: div,
    +        dom: document.createElement("audio"),
           };
         }
     
    diff --git a/packages/core/src/blocks/Code/block.test.ts b/packages/core/src/blocks/Code/block.test.ts
    new file mode 100644
    index 0000000000..b687c03b22
    --- /dev/null
    +++ b/packages/core/src/blocks/Code/block.test.ts
    @@ -0,0 +1,258 @@
    +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
    +import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
    +import type { PartialBlock } from "../defaultBlocks.js";
    +import { getLanguageId, type CodeBlockOptions } from "./block.js";
    +
    +/**
    + * @vitest-environment jsdom
    + */
    +
    +/**
    + * Simulate typing text into the editor at the current cursor position.
    + * This triggers input rules by calling the view's handleTextInput prop,
    + * which is how ProseMirror processes keyboard text input.
    + */
    +function simulateTextInput(editor: BlockNoteEditor, text: string) {
    +  const view = editor.prosemirrorView;
    +  const { from, to } = view.state.selection;
    +  const deflt = () => view.state.tr.insertText(text, from, to);
    +  const handled = view.someProp("handleTextInput", (f) =>
    +    f(view, from, to, text, deflt),
    +  );
    +  if (!handled) {
    +    view.dispatch(deflt());
    +  }
    +}
    +
    +function typeString(editor: BlockNoteEditor, str: string) {
    +  for (const char of str) {
    +    simulateTextInput(editor, char);
    +  }
    +}
    +
    +/**
    + * Simulate a keyboard shortcut by invoking the view's handleKeyDown prop,
    + * which is how ProseMirror routes keymap-based handlers like Enter.
    + */
    +function pressKey(editor: BlockNoteEditor, key: string) {
    +  const view = editor.prosemirrorView;
    +  const event = new KeyboardEvent("keydown", { key });
    +  view.someProp("handleKeyDown", (f) => f(view, event));
    +}
    +
    +describe("Code block input rule", () => {
    +  let editor: BlockNoteEditor;
    +  const div = document.createElement("div");
    +
    +  beforeAll(() => {
    +    editor = BlockNoteEditor.create();
    +    editor.mount(div);
    +  });
    +
    +  afterAll(() => {
    +    editor._tiptapEditor.destroy();
    +    editor = undefined as any;
    +  });
    +
    +  beforeEach(() => {
    +    const testDoc: PartialBlock[] = [
    +      {
    +        id: "test-paragraph",
    +        type: "paragraph",
    +        content: "",
    +      },
    +    ];
    +    editor.replaceBlocks(editor.document, testDoc);
    +    editor.setTextCursorPosition("test-paragraph", "start");
    +  });
    +
    +  it("converts ```ts + space into a codeBlock", () => {
    +    typeString(editor, "```ts ");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("codeBlock");
    +    // Without supportedLanguages configured, the raw alias is used
    +    expect((block.props as any).language).toBe("ts");
    +  });
    +
    +  it("converts ``` + space into a codeBlock with empty language", () => {
    +    typeString(editor, "``` ");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("codeBlock");
    +    expect((block.props as any).language).toBe("");
    +  });
    +
    +  it("converts ```javascript + space into a codeBlock", () => {
    +    typeString(editor, "```javascript ");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("codeBlock");
    +    expect((block.props as any).language).toBe("javascript");
    +  });
    +
    +  it("does not trigger input rule without trailing space", () => {
    +    typeString(editor, "```ts");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("paragraph");
    +  });
    +
    +  it("does not trigger with only two backticks", () => {
    +    typeString(editor, "``ts ");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("paragraph");
    +  });
    +
    +  it("does not trigger in non-empty paragraph with preceding text", () => {
    +    typeString(editor, "some text ```ts ");
    +
    +    const block = editor.document[0];
    +    // The ^ anchor in the regex means it only triggers at the start of a block
    +    expect(block.type).toBe("paragraph");
    +  });
    +
    +  it("code block content is empty after conversion", () => {
    +    typeString(editor, "```ts ");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("codeBlock");
    +    expect(block.content).toEqual([]);
    +  });
    +
    +  it("converts ```ts + Enter into a codeBlock", () => {
    +    typeString(editor, "```ts");
    +    pressKey(editor, "Enter");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("codeBlock");
    +    expect((block.props as any).language).toBe("ts");
    +    expect(block.content).toEqual([]);
    +  });
    +
    +  it("converts ``` + Enter into a codeBlock with empty language", () => {
    +    typeString(editor, "```");
    +    pressKey(editor, "Enter");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("codeBlock");
    +    expect((block.props as any).language).toBe("");
    +  });
    +
    +  it("converts ```javascript + Enter into a codeBlock", () => {
    +    typeString(editor, "```javascript");
    +    pressKey(editor, "Enter");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("codeBlock");
    +    expect((block.props as any).language).toBe("javascript");
    +  });
    +
    +  it("does not trigger Enter conversion in non-empty paragraph with preceding text", () => {
    +    typeString(editor, "some text ```ts");
    +    pressKey(editor, "Enter");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("paragraph");
    +  });
    +
    +  it("does not trigger Enter conversion with only two backticks", () => {
    +    typeString(editor, "``ts");
    +    pressKey(editor, "Enter");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("paragraph");
    +  });
    +
    +  it("places cursor inside the new code block after space conversion", () => {
    +    typeString(editor, "```ts ");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("codeBlock");
    +
    +    const { block: cursorBlock } = editor.getTextCursorPosition();
    +    expect(cursorBlock.id).toBe(block.id);
    +
    +    // Typing should now go into the code block, not after it.
    +    typeString(editor, "hello");
    +    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",
    +    );
    +  });
    +
    +  it("places cursor inside the new code block after Enter conversion", () => {
    +    typeString(editor, "```ts");
    +    pressKey(editor, "Enter");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("codeBlock");
    +
    +    const { block: cursorBlock } = editor.getTextCursorPosition();
    +    expect(cursorBlock.id).toBe(block.id);
    +
    +    typeString(editor, "world");
    +    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",
    +    );
    +  });
    +
    +  it("Enter inside an existing code block does not retrigger conversion", () => {
    +    typeString(editor, "```ts ");
    +
    +    const block = editor.document[0];
    +    expect(block.type).toBe("codeBlock");
    +
    +    typeString(editor, "```js");
    +    pressKey(editor, "Enter");
    +
    +    // Enter inside a code block should insert a newline, not convert again.
    +    const after = editor.document[0];
    +    expect(after.type).toBe("codeBlock");
    +    expect((after.props as any).language).toBe("ts");
    +  });
    +});
    +
    +describe("getLanguageId", () => {
    +  const options: CodeBlockOptions = {
    +    supportedLanguages: {
    +      typescript: {
    +        name: "TypeScript",
    +        aliases: ["ts", "typescript"],
    +      },
    +      javascript: {
    +        name: "JavaScript",
    +        aliases: ["js", "javascript"],
    +      },
    +      python: {
    +        name: "Python",
    +        aliases: ["py", "python"],
    +      },
    +    },
    +  };
    +
    +  it("resolves alias to language id", () => {
    +    expect(getLanguageId(options, "ts")).toBe("typescript");
    +    expect(getLanguageId(options, "js")).toBe("javascript");
    +    expect(getLanguageId(options, "py")).toBe("python");
    +  });
    +
    +  it("resolves language id directly", () => {
    +    expect(getLanguageId(options, "typescript")).toBe("typescript");
    +    expect(getLanguageId(options, "javascript")).toBe("javascript");
    +  });
    +
    +  it("returns undefined for unknown language", () => {
    +    expect(getLanguageId(options, "unknown")).toBeUndefined();
    +  });
    +
    +  it("returns undefined with no supportedLanguages", () => {
    +    expect(getLanguageId({}, "ts")).toBeUndefined();
    +  });
    +});
    diff --git a/packages/core/src/blocks/File/block.ts b/packages/core/src/blocks/File/block.ts
    index a506cc45a3..8e1ddf622f 100644
    --- a/packages/core/src/blocks/File/block.ts
    +++ b/packages/core/src/blocks/File/block.ts
    @@ -75,11 +75,8 @@ export const createFileBlockSpec = createBlockSpec(createFileBlockConfig, {
       },
       toExternalHTML(block) {
         if (!block.props.url) {
    -      const div = document.createElement("p");
    -      div.textContent = "Add file";
    -
           return {
    -        dom: div,
    +        dom: document.createElement("embed"),
           };
         }
     
    diff --git a/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts b/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts
    index cbb347acff..51de658d2c 100644
    --- a/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts
    +++ b/packages/core/src/blocks/File/helpers/render/createFileBlockWrapper.ts
    @@ -26,7 +26,12 @@ export const createFileBlockWrapper = (
       element?: { dom: HTMLElement; destroy?: () => void },
       buttonIcon?: HTMLElement,
     ) => {
    -  const wrapper = document.createElement("div");
    +  // Use a 
    /
    when the block has a caption, so the caption + // is semantically associated with its content for assistive tech. Falls back + // to a plain
    when there is no caption (or the file has not been + // uploaded yet, since the upload UI never shows the caption). + const useFigure = block.props.url !== "" && !!block.props.caption; + const wrapper = document.createElement(useFigure ? "figure" : "div"); wrapper.className = "bn-file-block-content-wrapper"; // Show the add file button if the file has not been uploaded yet. Change to @@ -73,7 +78,7 @@ export const createFileBlockWrapper = ( // Show the caption if there is one. if (block.props.caption) { - const caption = document.createElement("p"); + const caption = document.createElement("figcaption"); caption.className = "bn-file-caption"; caption.textContent = block.props.caption; wrapper.appendChild(caption); diff --git a/packages/core/src/blocks/Image/block.ts b/packages/core/src/blocks/Image/block.ts index 83138c8842..e3ba986c6e 100644 --- a/packages/core/src/blocks/Image/block.ts +++ b/packages/core/src/blocks/Image/block.ts @@ -116,9 +116,15 @@ export const imageRender = image.src = block.props.url; } - image.alt = block.props.name || block.props.caption || "BlockNote image"; + // alt describes image content (per WCAG H86); figcaption (when present) + // is the contextual caption. Fall back to "" so unlabelled images are + // marked decorative rather than getting a noisy generic fallback. + image.alt = block.props.name || ""; image.contentEditable = "false"; image.draggable = false; + if (block.props.previewWidth) { + image.width = block.props.previewWidth; + } imageWrapper.appendChild(image); return createResizableFileBlockWrapper( @@ -141,11 +147,8 @@ export const imageToExternalHTML = >, ) => { if (!block.props.url) { - const div = document.createElement("p"); - div.textContent = "Add image"; - return { - dom: div, + dom: document.createElement("img"), }; } @@ -153,7 +156,7 @@ export const imageToExternalHTML = if (block.props.showPreview) { image = document.createElement("img"); image.src = block.props.url; - image.alt = block.props.name || block.props.caption || "BlockNote image"; + image.alt = block.props.name || ""; if (block.props.previewWidth) { image.width = block.props.previewWidth; } diff --git a/packages/core/src/blocks/ListItem/BulletListItem/block.ts b/packages/core/src/blocks/ListItem/BulletListItem/block.ts index 4cd64f5058..0a40bdc1ce 100644 --- a/packages/core/src/blocks/ListItem/BulletListItem/block.ts +++ b/packages/core/src/blocks/ListItem/BulletListItem/block.ts @@ -37,17 +37,20 @@ export const createBulletListItemBlockSpec = createBlockSpec( const parent = element.parentElement; - if (parent === null) { - return undefined; - } - if ( + parent === null || parent.tagName === "UL" || (parent.tagName === "DIV" && parent.parentElement?.tagName === "UL") ) { return parseDefaultProps(element); } + // Orphan `
  • ` (no
      /
        ancestor) — match as bulletListItem so + // pasting bare `
      1. ` HTML doesn't fall back to a paragraph. + if (!element.closest("ul, ol")) { + return parseDefaultProps(element); + } + return undefined; }, // As `li` elements can contain multiple paragraphs, we need to merge their contents diff --git a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts index d10d4b08f1..433fa47806 100644 --- a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts +++ b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.test.ts @@ -1,5 +1,5 @@ import { Selection } from "prosemirror-state"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; @@ -9,9 +9,22 @@ import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; const PLUGIN_KEY = "numbered-list-indexing-decorations$"; +// Track editors created in each test so we can unmount them in afterEach — +// otherwise prosemirror-view's DOMObserver leaves a setTimeout alive that +// fires after vitest tears down jsdom, throwing +// `ReferenceError: document is not defined` and failing the run. +const activeEditors: BlockNoteEditor[] = []; + +afterEach(() => { + while (activeEditors.length) { + activeEditors.pop()!.unmount(); + } +}); + function createEditor() { const editor = BlockNoteEditor.create(); editor.mount(document.createElement("div")); + activeEditors.push(editor); return editor; } diff --git a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts index 1011f58ca0..eb6b06dc7d 100644 --- a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts +++ b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts @@ -135,10 +135,9 @@ function getDecorations( // Find the start of the first change to limit traversal scope. // We only need to check from the change point forward, since earlier // blocks are unaffected and their mapped decorations remain correct. - const range = tr.changedRange(); - if (!range) { - return { decorations: nextDecorationSet }; - } + // On init (no steps), changedRange() returns null — fall back to a + // full scan so initial content gets indexed. + const range = tr.changedRange() ?? { from: 0, to: tr.doc.nodeSize - 2 }; const decorationsToAdd = [] as Deco[]; // Track blockGroups where we've verified a decoration match past the diff --git a/packages/core/src/blocks/Quote/block.ts b/packages/core/src/blocks/Quote/block.ts index bb14afbd77..b6b6a7710f 100644 --- a/packages/core/src/blocks/Quote/block.ts +++ b/packages/core/src/blocks/Quote/block.ts @@ -84,6 +84,15 @@ export const createQuoteBlockSpec = createBlockSpec( }; }, }, + { + find: new RegExp(`^\\p{Quotation_Mark}\\s$`, "u"), + replace() { + return { + type: "quote", + props: {}, + }; + }, + }, ], }), ], diff --git a/packages/core/src/blocks/Table/TableExtension.ts b/packages/core/src/blocks/Table/TableExtension.ts index 3660d8c620..1d2cf9d47f 100644 --- a/packages/core/src/blocks/Table/TableExtension.ts +++ b/packages/core/src/blocks/Table/TableExtension.ts @@ -1,5 +1,14 @@ import { callOrReturn, Extension, getExtensionField } from "@tiptap/core"; -import { columnResizing, goToNextCell, tableEditing } from "prosemirror-tables"; +import { TextSelection } from "prosemirror-state"; +import { + columnResizing, + goToNextCell, + isInTable, + moveCellForward, + nextCell, + selectionCell, + tableEditing, +} from "prosemirror-tables"; export const RESIZE_MIN_WIDTH = 35; export const EMPTY_CELL_WIDTH = 120; @@ -24,19 +33,39 @@ export const TableExtension = Extension.create({ addKeyboardShortcuts() { return { - // Makes enter create a new line within the cell. + // Moves the selection to the cell below. Enter: () => { if ( - this.editor.state.selection.empty && - this.editor.state.selection.$head.parent.type.name === - "tableParagraph" + this.editor.state.selection.$head.parent.type.name !== + "tableParagraph" ) { - this.editor.commands.insertContent({ type: "hardBreak" }); - - return true; + return false; } - return false; + return this.editor.commands.command(({ state, dispatch }) => { + if (!isInTable(state)) { + return false; + } + + const $cell = selectionCell(state); + const $nextCell = nextCell($cell, "vert", 1); + + if (!$nextCell) { + return false; + } + + if (dispatch) { + dispatch( + state.tr + .setSelection( + TextSelection.between($nextCell, moveCellForward($nextCell)), + ) + .scrollIntoView(), + ); + } + + return true; + }); }, // Ensures that backspace won't delete the table if the text cursor is at // the start of a cell and the selection is empty. diff --git a/packages/core/src/blocks/Table/block.ts b/packages/core/src/blocks/Table/block.ts index 9a23d227aa..c71d9ffb7d 100644 --- a/packages/core/src/blocks/Table/block.ts +++ b/packages/core/src/blocks/Table/block.ts @@ -10,6 +10,7 @@ import { TableContent, } from "../../schema/index.js"; import { mergeCSSClasses } from "../../util/browser.js"; +import { camelToDataKebab } from "../../util/string.js"; import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js"; import { defaultProps } from "../defaultProps.js"; import { EMPTY_CELL_WIDTH, TableExtension } from "./TableExtension.js"; @@ -252,6 +253,31 @@ const TiptapTableNode = Node.create({ super.ignoreMutation(record) ); } + + // `TableView` implements its own `update` method, as the view needs to + // be persisted across updates for column resizing to work properly. + // However, it doesn't do anything else, so we have to re-apply the + // HTML attributes from props manually. This isn't an issue for node + // views created e.g. by custom blocks, as those aren't persisted + // across updates (they are reinstantiated each time), and so + // `HTMLAttributes` is always up-to-date for those. + update(updatedNode: PMNode): boolean { + if (!super.update(updatedNode)) { + return false; + } + + for (const [propName, propSpec] of Object.entries(tablePropSchema)) { + const attrName = camelToDataKebab(propName); + const value = updatedNode.attrs[propName]; + if (value !== propSpec.default) { + this.dom.setAttribute(attrName, String(value)); + } else { + this.dom.removeAttribute(attrName); + } + } + + return true; + } } return new BlockNoteTableView(node, EMPTY_CELL_WIDTH, { diff --git a/packages/core/src/blocks/Video/block.ts b/packages/core/src/blocks/Video/block.ts index 026b333ba5..73193e6ddb 100644 --- a/packages/core/src/blocks/Video/block.ts +++ b/packages/core/src/blocks/Video/block.ts @@ -92,7 +92,9 @@ export const createVideoBlockSpec = createBlockSpec( video.controls = true; video.contentEditable = "false"; video.draggable = false; - video.width = block.props.previewWidth; + if (block.props.previewWidth) { + video.width = block.props.previewWidth; + } videoWrapper.appendChild(video); return createResizableFileBlockWrapper( @@ -105,11 +107,8 @@ export const createVideoBlockSpec = createBlockSpec( }, toExternalHTML(block) { if (!block.props.url) { - const div = document.createElement("p"); - div.textContent = "Add video"; - return { - dom: div, + dom: document.createElement("video"), }; } diff --git a/packages/core/src/blocks/Video/parseVideoElement.ts b/packages/core/src/blocks/Video/parseVideoElement.ts index 4b11481d48..7852866d4b 100644 --- a/packages/core/src/blocks/Video/parseVideoElement.ts +++ b/packages/core/src/blocks/Video/parseVideoElement.ts @@ -1,6 +1,7 @@ export const parseVideoElement = (videoElement: HTMLVideoElement) => { const url = videoElement.src || undefined; const previewWidth = videoElement.width || undefined; + const name = videoElement.getAttribute("data-name") || undefined; - return { url, previewWidth }; + return { url, previewWidth, name }; }; diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index 459b987f00..d2eeaa0aa3 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -1,3 +1,4 @@ +import { InputRule, markInputRule } from "@tiptap/core"; import Bold from "@tiptap/extension-bold"; import Code from "@tiptap/extension-code"; import Italic from "@tiptap/extension-italic"; @@ -136,7 +137,39 @@ export const defaultStyleSpecs = { italic: createStyleSpecFromTipTapMark(Italic, "boolean"), underline: createStyleSpecFromTipTapMark(Underline, "boolean"), strike: createStyleSpecFromTipTapMark(Strike, "boolean"), - code: createStyleSpecFromTipTapMark(Code, "boolean"), + code: createStyleSpecFromTipTapMark( + Code.extend({ + addInputRules() { + return [ + // Matches any string that starts with a backtick, ends with a + // backtick, and has any non-backtick characters in between. Copied + // from original input rule: + // https://github.com/ueberdosis/tiptap/blob/c27661c148cdbea9e1c80107e10d0a9d1775c4ec/packages/extension-code/src/code.ts#L116 + markInputRule({ + find: /(^|[^`])`([^`]+)`(?!`)$/, + type: this.type, + }), + // Extends the Code mark with an extra input rule that fires when a space is + // typed after the closing backtick. The default rule only fires when typing + // the closing backtick itself, so it misses the case where the user adds + // both backticks first, then writes content between them. + new InputRule({ + find: /(^|[^`])`([^`]+)`(?!`) $/, + handler: ({ state, range, match }) => { + const { tr, schema } = state; + const leadingChar = match[1]; + const content = match[2]; + tr.replaceWith(range.from + leadingChar.length, range.to, [ + schema.text(content, [this.type.create()]), + schema.text(" "), + ]); + }, + }), + ]; + }, + }), + "boolean", + ), textColor: TextColor, backgroundColor: BackgroundColor, } satisfies StyleSpecs; diff --git a/packages/core/src/comments/extension.ts b/packages/core/src/comments/extension.ts index 4e8e566cef..8d23c3e967 100644 --- a/packages/core/src/comments/extension.ts +++ b/packages/core/src/comments/extension.ts @@ -158,6 +158,8 @@ export const CommentsExtension = createExtension( return { key: "comments", store, + runsBefore: ["link"], + tiptapExtensions: [CommentMark], prosemirrorPlugins: [ new Plugin({ key: PLUGIN_KEY, @@ -224,7 +226,7 @@ export const CommentsExtension = createExtension( }, handleClick: (view, pos, event) => { if (event.button !== 0) { - return; + return false; } const node = view.state.doc.nodeAt(pos); @@ -235,7 +237,7 @@ export const CommentsExtension = createExtension( ...prev, selectedThreadId: undefined, })); - return; + return false; } const commentMark = node.marks.find( @@ -243,15 +245,33 @@ export const CommentsExtension = createExtension( mark.type.name === markType && mark.attrs.orphan !== true, ); - const threadId = commentMark?.attrs.threadId as - | string - | undefined; - if (threadId !== store.state.selectedThreadId) { - store.setState((prev) => ({ - ...prev, - selectedThreadId: threadId, - })); + if (!commentMark) { + // Clicked outside any comment thread. Deselect if needed but + // don't consume the event so other handlers (e.g. link + // navigation) can process it. + if (store.state.selectedThreadId !== undefined) { + store.setState((prev) => ({ + ...prev, + selectedThreadId: undefined, + })); + } + return false; + } + + const threadId = commentMark.attrs.threadId as string; + + // If the clicked thread is already selected, do nothing and let + // other handlers process the event (e.g. navigating a link). + if (threadId === store.state.selectedThreadId) { + return false; } + + store.setState((prev) => ({ + ...prev, + selectedThreadId: threadId, + })); + + return true; }, }, }), @@ -306,6 +326,11 @@ export const CommentsExtension = createExtension( selectedThreadId: undefined, pendingComment: true, })); + // Use `editor.domElement` as `editor.focus()` doesn't do anything if + // the editor is non-editable. Editor needs to be focused as + // `showSelection` will otherwise trigger a selection update which + // triggers `stopPendingComment`. + editor.domElement?.focus(); editor .getExtension(ShowSelectionExtension) ?.showSelection(true, "comments"); @@ -351,7 +376,6 @@ export const CommentsExtension = createExtension( }, userStore, commentEditorSchema, - tiptapExtensions: [CommentMark], } as const; }, ); diff --git a/packages/core/src/comments/threadstore/yjs/YjsThreadStore.test.ts b/packages/core/src/comments/threadstore/yjs/YjsThreadStore.test.ts index 8f967eb547..b73b7c1ec8 100644 --- a/packages/core/src/comments/threadstore/yjs/YjsThreadStore.test.ts +++ b/packages/core/src/comments/threadstore/yjs/YjsThreadStore.test.ts @@ -6,8 +6,9 @@ import { YjsThreadStore } from "./YjsThreadStore.js"; // Mock UUID to generate sequential IDs let mockUuidCounter = 0; -vi.mock("uuid", () => ({ - v4: () => `mocked-uuid-${++mockUuidCounter}`, +vi.mock("lib0/random", async (importOriginal) => ({ + ...(await importOriginal()), + uuidv4: () => `mocked-uuid-${++mockUuidCounter}`, })); describe("YjsThreadStore", () => { diff --git a/packages/core/src/comments/threadstore/yjs/YjsThreadStore.ts b/packages/core/src/comments/threadstore/yjs/YjsThreadStore.ts index 7504e43fb1..f9754c6063 100644 --- a/packages/core/src/comments/threadstore/yjs/YjsThreadStore.ts +++ b/packages/core/src/comments/threadstore/yjs/YjsThreadStore.ts @@ -1,4 +1,4 @@ -import { v4 } from "uuid"; +import { uuidv4 } from "lib0/random"; import * as Y from "yjs"; import { CommentBody, CommentData, ThreadData } from "../../types.js"; import { ThreadStoreAuth } from "../ThreadStoreAuth.js"; @@ -57,7 +57,7 @@ export class YjsThreadStore extends YjsThreadStoreBase { const comment: CommentData = { type: "comment", - id: v4(), + id: uuidv4(), userId: this.userId, createdAt: date, updatedAt: date, @@ -68,7 +68,7 @@ export class YjsThreadStore extends YjsThreadStoreBase { const thread: ThreadData = { type: "thread", - id: v4(), + id: uuidv4(), createdAt: date, updatedAt: date, comments: [comment], @@ -105,7 +105,7 @@ export class YjsThreadStore extends YjsThreadStoreBase { const date = new Date(); const comment: CommentData = { type: "comment", - id: v4(), + id: uuidv4(), userId: this.userId, createdAt: date, updatedAt: date, diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index 6a079f7863..fca4e504ad 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -49,6 +49,11 @@ BASIC STYLES white-space: pre-wrap; } +.bn-trailing-block { + cursor: text; + height: 30px; +} + /* NESTED BLOCKS */ @@ -127,6 +132,7 @@ NESTED BLOCKS /* HEADINGS*/ [data-content-type="heading"] { + padding-top: 18px; --level: 3em; } [data-content-type="heading"][data-level="2"] { @@ -467,6 +473,9 @@ NESTED BLOCKS cursor: pointer; display: flex; flex-direction: column; + /* Reset default
        browser margins (the wrapper becomes a
        + when the block has a caption). */ + margin: 0; user-select: none; } @@ -486,12 +495,25 @@ NESTED BLOCKS padding: 12px; } +[data-file-block] .bn-add-file-button:where(.dark, .dark *) { + background-color: rgb(70, 70, 70); + color: rgb(190, 190, 190); +} + .bn-editor[contenteditable="true"] [data-file-block] .bn-add-file-button:hover, [data-file-block] .bn-file-name-with-icon:hover, .ProseMirror-selectednode .bn-file-name-with-icon { background-color: rgb(225, 225, 225); } +.bn-editor[contenteditable="true"] + [data-file-block] + .bn-add-file-button:hover:where(.dark, .dark *), +[data-file-block] .bn-file-name-with-icon:hover:where(.dark, .dark *), +.ProseMirror-selectednode .bn-file-name-with-icon:where(.dark, .dark *) { + background-color: rgb(90, 90, 90); +} + [data-file-block] .bn-add-file-button-icon, [data-file-block] .bn-file-icon { width: 24px; @@ -561,110 +583,92 @@ NESTED BLOCKS /* TEXT COLORS */ [data-style-type="textColor"][data-value="gray"], -[data-text-color="gray"], .bn-block:has(> .bn-block-content[data-text-color="gray"]) { color: #9b9a97; } [data-style-type="textColor"][data-value="brown"], -[data-text-color="brown"], .bn-block:has(> .bn-block-content[data-text-color="brown"]) { color: #64473a; } [data-style-type="textColor"][data-value="red"], -[data-text-color="red"], .bn-block:has(> .bn-block-content[data-text-color="red"]) { color: #e03e3e; } [data-style-type="textColor"][data-value="orange"], -[data-text-color="orange"], .bn-block:has(> .bn-block-content[data-text-color="orange"]) { color: #d9730d; } [data-style-type="textColor"][data-value="yellow"], -[data-text-color="yellow"], .bn-block:has(> .bn-block-content[data-text-color="yellow"]) { color: #dfab01; } [data-style-type="textColor"][data-value="green"], -[data-text-color="green"], .bn-block:has(> .bn-block-content[data-text-color="green"]) { color: #4d6461; } [data-style-type="textColor"][data-value="blue"], -[data-text-color="blue"], .bn-block:has(> .bn-block-content[data-text-color="blue"]) { color: #0b6e99; } [data-style-type="textColor"][data-value="purple"], -[data-text-color="purple"], .bn-block:has(> .bn-block-content[data-text-color="purple"]) { color: #6940a5; } [data-style-type="textColor"][data-value="pink"], -[data-text-color="pink"], .bn-block:has(> .bn-block-content[data-text-color="pink"]) { color: #ad1a72; } /* BACKGROUND COLORS */ [data-style-type="backgroundColor"][data-value="gray"], -[data-background-color="gray"], .bn-block:has(> .bn-block-content[data-background-color="gray"]) { background-color: #ebeced; } [data-style-type="backgroundColor"][data-value="brown"], -[data-background-color="brown"], .bn-block:has(> .bn-block-content[data-background-color="brown"]) { background-color: #e9e5e3; } [data-style-type="backgroundColor"][data-value="red"], -[data-background-color="red"], .bn-block:has(> .bn-block-content[data-background-color="red"]) { background-color: #fbe4e4; } [data-style-type="backgroundColor"][data-value="orange"], -[data-background-color="orange"], .bn-block:has(> .bn-block-content[data-background-color="orange"]) { background-color: #f6e9d9; } [data-style-type="backgroundColor"][data-value="yellow"], -[data-background-color="yellow"], .bn-block:has(> .bn-block-content[data-background-color="yellow"]) { background-color: #fbf3db; } [data-style-type="backgroundColor"][data-value="green"], -[data-background-color="green"], .bn-block:has(> .bn-block-content[data-background-color="green"]) { background-color: #ddedea; } [data-style-type="backgroundColor"][data-value="blue"], -[data-background-color="blue"], .bn-block:has(> .bn-block-content[data-background-color="blue"]) { background-color: #ddebf1; } [data-style-type="backgroundColor"][data-value="purple"], -[data-background-color="purple"], .bn-block:has(> .bn-block-content[data-background-color="purple"]) { background-color: #eae4f2; } [data-style-type="backgroundColor"][data-value="pink"], -[data-background-color="pink"], .bn-block:has(> .bn-block-content[data-background-color="pink"]) { background-color: #f4dfeb; } diff --git a/packages/core/src/editor/BlockNoteEditor.test.ts b/packages/core/src/editor/BlockNoteEditor.test.ts index 120847bffa..6a4f5f023e 100644 --- a/packages/core/src/editor/BlockNoteEditor.test.ts +++ b/packages/core/src/editor/BlockNoteEditor.test.ts @@ -187,7 +187,7 @@ it("sets an initial block id when using Y.js", async () => { expect(transactionCount).toBe(2); // Only after a real modification is made, will the fragment be updated expect(fragment.toJSON()).toMatchInlineSnapshot( - `"Hello"`, + `"Hello"`, ); }); @@ -213,8 +213,14 @@ it("onBeforeChange", () => { { "block": { "children": [], - "content": [], - "id": "3", + "content": [ + { + "styles": {}, + "text": "Hello", + "type": "text", + }, + ], + "id": "1", "props": { "backgroundColor": "default", "textAlignment": "left", @@ -232,7 +238,7 @@ it("onBeforeChange", () => { "block": { "children": [], "content": [], - "id": "2", + "id": "0", "props": { "backgroundColor": "default", "textAlignment": "left", diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 561f77b158..e4888f50f6 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -140,6 +140,54 @@ export interface BlockNoteEditorOptions< NoInfer >[]; + /** + * Options for configuring how links behave in the editor. + */ + links?: { + /** + * HTML attributes to add to rendered link elements. + * + * @default {} + * @example { class: "my-link-class", target: "_blank" } + */ + HTMLAttributes?: Record; + /** + * Custom handler invoked when a link is clicked. If left `undefined`, + * links are opened in a new window on click. If provided, the default + * open-on-click behavior is disabled and this function is called instead. + * + * Return `false` to let ProseMirror continue handling the click event. + * Returning `true` or nothing (the default) marks the event as handled. + */ + onClick?: ( + event: MouseEvent, + editor: BlockNoteEditor, + ) => boolean | void; + /** + * Callback that decides whether a given `href` is a valid link. Applied at + * every gate where a link enters the document: HTML import, HTML export, + * paste, and autolink. Useful for supporting additional URI schemes (e.g. + * `vscode:`, `myapp:`) or tightening the default allowlist. + * + * Defaults to `isAllowedUri`, which allows + * `http|https|ftp|ftps|mailto|tel|callto|sms|cid|xmpp`. Import + * `isAllowedUri` from `@blocknote/core` to layer on top of the default. + * + * @example + * ```ts + * import { isAllowedUri } from "@blocknote/core"; + * + * BlockNoteEditor.create({ + * links: { + * isValidLink: (href) => + * isAllowedUri(href) || href.startsWith("myapp:"), + * }, + * }); + * ``` + */ + isValidLink?: (href: string) => boolean; + }; + /** * @deprecated, provide placeholders via dictionary instead * @internal @@ -258,7 +306,8 @@ export interface BlockNoteEditorOptions< }; /** - * An option which user can pass with `false` value to disable the automatic creation of a trailing new block on the next line when the user types or edits any block. + * When the editor document doesn't end in an empty paragraph block, this option causes the editor to render an element simulating one. + * When clicked by the user, it gets turned into an actual block at the end of the document. This element is not shown when the option is `false`. * * @default true */ @@ -562,6 +611,13 @@ export class BlockNoteEditor< }; this.pmSchema.cached.blockNoteEditor = this; + this._tiptapEditor.on("mount", () => { + this.headless = false; + }); + this._tiptapEditor.on("unmount", () => { + this.headless = true; + }); + // Initialize managers this._blockManager = new BlockManager(this as any); @@ -676,15 +732,27 @@ export class BlockNoteEditor< /** * Mount the editor to a DOM element. * + * @param element The DOM element to mount the editor's contenteditable into. + * @param options.portalTarget Where to mount `editor.portalElement` — the + * container that floating UI (toolbars, menus, etc) portals into. When + * omitted, defaults to `element.parentElement` (which is the editor's + * `bn-container` in typical React usage), or to `document.body` / + * the surrounding shadow root when no parent is available. + * * @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting */ - public mount = (element: HTMLElement) => { + public mount = ( + element: HTMLElement, + options?: { portalTarget?: HTMLElement | null }, + ) => { const root = element.getRootNode(); - if (typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot) { - root.appendChild(this.portalElement); - } else { - document.body.appendChild(this.portalElement); - } + const isInShadowRoot = + typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot; + const target = + options?.portalTarget ?? + element.parentElement ?? + (isInShadowRoot ? (root as ShadowRoot) : document.body); + target.appendChild(this.portalElement); this._tiptapEditor.mount({ mount: element }); }; @@ -758,9 +826,7 @@ export class BlockNoteEditor< return this.prosemirrorView?.hasFocus() || false; } - public get headless() { - return !this._tiptapEditor.isInitialized; - } + public headless = true; /** * Focus on the editor @@ -1130,6 +1196,32 @@ export class BlockNoteEditor< this._styleManager.createLink(url, text); } + /** + * Find the link mark and its range at the given position. + * Returns undefined if there is no link at that position. + */ + public getLinkMarkAtPos(pos: number) { + return this._styleManager.getLinkMarkAtPos(pos); + } + + /** + * Updates the link at the given position with a new URL and text. + * @param url The new link URL. + * @param text The new text to display. + * @param position The position inside the link to edit. Defaults to the current selection anchor. + */ + public editLink(url: string, text: string, position?: number) { + this._styleManager.editLink(url, text, position); + } + + /** + * Removes the link at the given position, keeping the text. + * @param position The position inside the link to remove. Defaults to the current selection anchor. + */ + public deleteLink(position?: number) { + this._styleManager.deleteLink(position); + } + /** * Checks if the block containing the text cursor can be nested. */ @@ -1161,19 +1253,23 @@ export class BlockNoteEditor< /** * Moves the selected blocks up. If the previous block has children, moves * them to the end of its children. If there is no previous block, but the - * current blocks share a common parent, moves them out of & before it. + * current blocks share a common parent, moves them out of & before it. If a + * `blockIdentifier` is provided, that block is moved instead of the + * selection, and the selection is left unchanged. */ - public moveBlocksUp() { - return this._blockManager.moveBlocksUp(); + public moveBlocksUp(blockIdentifier?: BlockIdentifier) { + return this._blockManager.moveBlocksUp(blockIdentifier); } /** * Moves the selected blocks down. If the next block has children, moves * them to the start of its children. If there is no next block, but the - * current blocks share a common parent, moves them out of & after it. + * current blocks share a common parent, moves them out of & after it. If a + * `blockIdentifier` is provided, that block is moved instead of the + * selection, and the selection is left unchanged. */ - public moveBlocksDown() { - return this._blockManager.moveBlocksDown(); + public moveBlocksDown(blockIdentifier?: BlockIdentifier) { + return this._blockManager.moveBlocksDown(blockIdentifier); } /** @@ -1296,7 +1392,7 @@ export class BlockNoteEditor< editor: BlockNoteEditor; }) => void, ) { - this._eventManager.onMount(callback); + return this._eventManager.onMount(callback); } /** @@ -1312,7 +1408,7 @@ export class BlockNoteEditor< editor: BlockNoteEditor; }) => void, ) { - this._eventManager.onUnmount(callback); + return this._eventManager.onUnmount(callback); } /** diff --git a/packages/core/src/editor/editor.css b/packages/core/src/editor/editor.css index 11b0e66841..a1a3dda7b0 100644 --- a/packages/core/src/editor/editor.css +++ b/packages/core/src/editor/editor.css @@ -80,6 +80,11 @@ hidden. So setting it to an extremely low value instead makes the element functionally invisible while not affecting the drag preview itself. */ opacity: 0.001; + /* The element is kept in the DOM after setDragImage captures its snapshot, + so it can overlap the editor and block drops in that area. Disabling + pointer-events lets drag/drop events pass through to the editor while + leaving the captured preview unaffected. */ + pointer-events: none; } .bn-editor .bn-collaboration-cursor__base { diff --git a/packages/core/src/editor/managers/BlockManager.ts b/packages/core/src/editor/managers/BlockManager.ts index 2ea90ae984..ea9d9a5680 100644 --- a/packages/core/src/editor/managers/BlockManager.ts +++ b/packages/core/src/editor/managers/BlockManager.ts @@ -234,18 +234,22 @@ export class BlockManager< /** * Moves the selected blocks up. If the previous block has children, moves * them to the end of its children. If there is no previous block, but the - * current blocks share a common parent, moves them out of & before it. + * current blocks share a common parent, moves them out of & before it. If a + * `blockIdentifier` is provided, that block is moved instead of the + * selection, and the selection is left unchanged. */ - public moveBlocksUp() { - return moveBlocksUp(this.editor); + public moveBlocksUp(blockIdentifier?: BlockIdentifier) { + return moveBlocksUp(this.editor, blockIdentifier); } /** * Moves the selected blocks down. If the next block has children, moves * them to the start of its children. If there is no next block, but the - * current blocks share a common parent, moves them out of & after it. + * current blocks share a common parent, moves them out of & after it. If a + * `blockIdentifier` is provided, that block is moved instead of the + * selection, and the selection is left unchanged. */ - public moveBlocksDown() { - return moveBlocksDown(this.editor); + public moveBlocksDown(blockIdentifier?: BlockIdentifier) { + return moveBlocksDown(this.editor, blockIdentifier); } } diff --git a/packages/core/src/editor/managers/ExtensionManager/extensions.ts b/packages/core/src/editor/managers/ExtensionManager/extensions.ts index 4364afaaa0..7be7070865 100644 --- a/packages/core/src/editor/managers/ExtensionManager/extensions.ts +++ b/packages/core/src/editor/managers/ExtensionManager/extensions.ts @@ -5,7 +5,7 @@ import { Extension as TiptapExtension, } from "@tiptap/core"; import { Gapcursor } from "@tiptap/extensions/gap-cursor"; -import { Link } from "@tiptap/extension-link"; +import { LinkExtension } from "../../../extensions/tiptap-extensions/Link/link.js"; import { Text } from "@tiptap/extension-text"; import { createDropFileExtension } from "../../../api/clipboard/fromClipboard/fileDropExtension.js"; import { createPasteFromClipboardExtension } from "../../../api/clipboard/fromClipboard/pasteExtension.js"; @@ -26,10 +26,6 @@ import { TableHandlesExtension, TrailingNodeExtension, } from "../../../extensions/index.js"; -import { - DEFAULT_LINK_PROTOCOL, - VALID_LINK_PROTOCOLS, -} from "../../../extensions/LinkToolbar/protocols.js"; import { BackgroundColorExtension, HardBreak, @@ -49,9 +45,6 @@ import { import { ExtensionFactoryInstance } from "../../BlockNoteExtension.js"; import { CollaborationExtension } from "../../../extensions/Collaboration/Collaboration.js"; -// TODO remove linkify completely by vendoring the link extension & dropping linkifyjs as a dependency -let LINKIFY_INITIALIZED = false; - /** * Get all the Tiptap extensions BlockNote is configured with by default */ @@ -80,23 +73,6 @@ export function getDefaultTiptapExtensions( SuggestionAddMark, SuggestionDeleteMark, SuggestionModificationMark, - Link.extend({ - inclusive: false, - }) - .extend({ - // Remove the title attribute added in newer versions of @tiptap/extension-link - // to avoid unnecessary null attributes in serialized output - addAttributes() { - const attrs = this.parent?.() || {}; - delete (attrs as Record).title; - return attrs; - }, - }) - .configure({ - defaultProtocol: DEFAULT_LINK_PROTOCOL, - // only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450 - protocols: LINKIFY_INITIALIZED ? [] : VALID_LINK_PROTOCOLS, - }), ...(Object.values(editor.schema.styleSpecs).map((styleSpec) => { return styleSpec.implementation.mark.configure({ editor: editor, @@ -173,8 +149,6 @@ export function getDefaultTiptapExtensions( createDropFileExtension(editor), ]; - LINKIFY_INITIALIZED = true; - return tiptapExtensions; } @@ -187,6 +161,13 @@ export function getDefaultExtensions( DropCursorExtension(options), FilePanelExtension(options), FormattingToolbarExtension(options), + LinkExtension({ + HTMLAttributes: options.links?.HTMLAttributes ?? {}, + onClick: options.links?.onClick, + ...(options.links?.isValidLink + ? { isValidLink: options.links.isValidLink } + : {}), + }), LinkToolbarExtension(options), NodeSelectionKeyboardExtension(), PlaceholderExtension(options), diff --git a/packages/core/src/editor/managers/ExtensionManager/index.ts b/packages/core/src/editor/managers/ExtensionManager/index.ts index 67b50871ed..d34521fecc 100644 --- a/packages/core/src/editor/managers/ExtensionManager/index.ts +++ b/packages/core/src/editor/managers/ExtensionManager/index.ts @@ -7,8 +7,9 @@ import { Extension as TiptapExtension, } from "@tiptap/core"; import { keymap } from "@tiptap/pm/keymap"; -import { Plugin } from "prosemirror-state"; +import { Plugin, TextSelection } from "prosemirror-state"; import { updateBlockTr } from "../../../api/blockManipulation/commands/updateBlock/updateBlock.js"; +import { setTextCursorPosition } from "../../../api/blockManipulation/selections/textCursorPosition.js"; import { getBlockInfoFromTransaction } from "../../../api/getBlockInfoFromPos.js"; import { sortByDependencies } from "../../../util/topo-sort.js"; import type { @@ -369,7 +370,49 @@ export class ExtensionManager { // Append in reverse priority order rules.push(...inputRulesByPriority.get(priority)!); }); - return [inputRulesPlugin({ rules })]; + const inputRules = inputRulesPlugin({ rules }); + // Sidecar plugin: triggers the same input rules on Enter by + // delegating to the inputRules plugin's handleTextInput with a + // synthetic "\n" insertion. The handlewithcare regex `\s$` already + // matches `\n`, so any rule that fires on space fires on Enter too. + // We call its handleTextInput directly (rather than via + // view.someProp) so other plugins don't observe the synthetic input, + // and so the rule's undo metadata is keyed to the same plugin + // instance that Tiptap's `commands.undoInputRule` reads from. + const inputRulesEnter = new Plugin({ + props: { + handleKeyDown(view, event) { + if (event.key !== "Enter") { + return false; + } + // Only trigger on plain Enter — modifier combos like + // Shift/Cmd/Ctrl/Alt+Enter are reserved for other handlers + // (e.g. soft-break, submit) and should fall through. + if ( + event.shiftKey || + event.ctrlKey || + event.metaKey || + event.altKey + ) { + return false; + } + const { $cursor } = view.state.selection as TextSelection; + if (!$cursor) { + return false; + } + return !!inputRules.props.handleTextInput?.call( + inputRules, + view, + $cursor.pos, + $cursor.pos, + "\n", + () => + view.state.tr.insertText("\n", $cursor.pos, $cursor.pos), + ); + }, + }, + }); + return [inputRules, inputRulesEnter]; }, }), ); @@ -408,30 +451,42 @@ export class ExtensionManager { if (extension.inputRules?.length) { inputRules.push( ...extension.inputRules.map((inputRule) => { - return new InputRule(inputRule.find, (state, match, start, end) => { - const replaceWith = inputRule.replace({ - match, - range: { from: start, to: end }, - editor: this.editor, - }); - if (replaceWith) { - const cursorPosition = this.editor.getTextCursorPosition(); - - if ( - this.editor.schema.blockSchema[cursorPosition.block.type] - .content !== "inline" - ) { - return null; + return new InputRule( + inputRule.find, + (state, match, start, end) => { + const replaceWith = inputRule.replace({ + match, + range: { from: start, to: end }, + editor: this.editor, + }); + if (replaceWith) { + const tr = state.tr; + const blockInfo = getBlockInfoFromTransaction(tr); + + if ( + !blockInfo.isBlockContainer || + this.editor.schema.blockSchema[blockInfo.blockNoteType] + ?.content !== "inline" + ) { + return null; + } + + tr.deleteRange(start, end); + updateBlockTr(tr, blockInfo.bnBlock.beforePos, replaceWith); + // updateBlockTr's replaceWith path leaves the selection after + // the new block when the content is replaced wholesale (e.g. + // when the rule returns content: []). Move the cursor back + // inside the new block so the user can keep typing. + const blockId = blockInfo.bnBlock.node.attrs.id; + if (blockId) { + setTextCursorPosition(tr, blockId, "start"); + } + return tr; } - - const blockInfo = getBlockInfoFromTransaction(state.tr); - const tr = state.tr.deleteRange(start, end); - - updateBlockTr(tr, blockInfo.bnBlock.beforePos, replaceWith); - return tr; - } - return null; - }); + return null; + }, + { undoable: true }, + ); }), ); } diff --git a/packages/core/src/editor/managers/StyleManager.ts b/packages/core/src/editor/managers/StyleManager.ts index e03c46a6d1..123ac6187b 100644 --- a/packages/core/src/editor/managers/StyleManager.ts +++ b/packages/core/src/editor/managers/StyleManager.ts @@ -1,3 +1,4 @@ +import { getMarkRange } from "@tiptap/core"; import { insertContentAt } from "../../api/blockManipulation/insertContentAt.js"; import { inlineContentToNodes } from "../../api/nodeConversions/blockToNode.js"; import { @@ -12,7 +13,6 @@ import { DefaultInlineContentSchema, DefaultStyleSchema, } from "../../blocks/defaultBlocks.js"; -import { TextSelection } from "@tiptap/pm/state"; import { UnreachableCaseError } from "../../util/typescript.js"; import { BlockNoteEditor } from "../BlockNoteEditor.js"; @@ -146,13 +146,42 @@ export class StyleManager< }); } + /** + * Find the link mark and its range at the given position. + * Returns undefined if there is no link at that position. + */ + public getLinkMarkAtPos(pos: number) { + return this.editor.transact((tr) => { + const resolvedPos = tr.doc.resolve(pos); + const linkMark = resolvedPos + .marks() + .find((mark) => mark.type.name === "link"); + + if (!linkMark) { + return undefined; + } + + const range = getMarkRange(resolvedPos, linkMark.type); + if (!range) { + return undefined; + } + + return { + href: linkMark.attrs.href as string, + from: range.from, + to: range.to, + text: tr.doc.textBetween(range.from, range.to), + }; + }); + } + /** * Gets the URL of the last link in the current selection, or `undefined` if there are no links in the selection. */ public getSelectedLinkUrl() { - return this.editor._tiptapEditor.getAttributes("link").href as - | string - | undefined; + return this.editor.transact((tr) => { + return this.getLinkMarkAtPos(tr.selection.from)?.href; + }); } /** @@ -164,19 +193,70 @@ export class StyleManager< if (url === "") { return; } - const mark = this.editor.pmSchema.mark("link", { href: url }); + this.editor.transact((tr) => { const { from, to } = tr.selection; + const linkMark = this.editor.pmSchema.mark("link", { href: url }); if (text) { - tr.insertText(text, from, to).addMark(from, from + text.length, mark); - } else { - tr.setSelection(TextSelection.create(tr.doc, to)).addMark( + tr.insertText(text, from, to).addMark( from, - to, - mark, + from + text.length, + linkMark, ); + } else { + tr.addMark(from, to, linkMark); + } + }); + } + + /** + * Updates the link at the given position with a new URL and text. + * @param url The new link URL. + * @param text The new text to display. + * @param position The position inside the link to edit. Defaults to the current selection anchor. + */ + public editLink( + url: string, + text: string, + position = this.editor.transact((tr) => tr.selection.anchor), + ) { + this.editor.transact((tr) => { + const linkData = this.getLinkMarkAtPos(position + 1); + const { from, to } = linkData || { + from: tr.selection.from, + to: tr.selection.to, + }; + + const linkMark = this.editor.pmSchema.mark("link", { href: url }); + const existingText = tr.doc.textBetween(from, to); + if (text !== existingText) { + tr.insertText(text, from, to); } + tr.addMark(from, from + text.length, linkMark); + }); + this.editor.prosemirrorView.focus(); + } + + /** + * Removes the link at the given position, keeping the text. + * @param position The position inside the link to remove. Defaults to the current selection anchor. + */ + public deleteLink( + position = this.editor.transact((tr) => tr.selection.anchor), + ) { + this.editor.transact((tr) => { + const linkData = this.getLinkMarkAtPos(position + 1); + const { from, to } = linkData || { + from: tr.selection.from, + to: tr.selection.to, + }; + + tr.removeMark(from, to, this.editor.pmSchema.marks["link"]).setMeta( + "preventAutolink", + true, + ); }); + this.editor.prosemirrorView.focus(); } } diff --git a/packages/core/src/editor/performance.test.ts b/packages/core/src/editor/performance.test.ts index 88657cb544..6564e1a72d 100644 --- a/packages/core/src/editor/performance.test.ts +++ b/packages/core/src/editor/performance.test.ts @@ -114,11 +114,11 @@ describe("Performance: transaction processing scales sub-linearly (#2595)", () = const smallAvg = measureAvgInsertTime( smallEditor, - smallEditor._tiptapEditor.view.state.doc.content.size - 4, + smallEditor._tiptapEditor.view.state.doc.content.size - 2, ); const largeAvg = measureAvgInsertTime( largeEditor, - largeEditor._tiptapEditor.view.state.doc.content.size - 4, + largeEditor._tiptapEditor.view.state.doc.content.size - 2, ); const ratio = largeAvg / smallAvg; diff --git a/packages/core/src/editor/transformPasted.test.ts b/packages/core/src/editor/transformPasted.test.ts new file mode 100644 index 0000000000..a9114b7066 --- /dev/null +++ b/packages/core/src/editor/transformPasted.test.ts @@ -0,0 +1,295 @@ +import { TextSelection } from "@tiptap/pm/state"; +import { afterEach, describe, expect, it } from "vitest"; + +import { BlockNoteEditor } from "./BlockNoteEditor.js"; + +/** + * @vitest-environment jsdom + */ + +const editorsToCleanup: BlockNoteEditor[] = []; + +afterEach(() => { + for (const editor of editorsToCleanup) { + editor.unmount(); + } + editorsToCleanup.length = 0; +}); + +function mountEditor(editor: BlockNoteEditor) { + editorsToCleanup.push(editor); + editor.mount(document.createElement("div")); +} + +function selectStartOfFirstBlock(editor: BlockNoteEditor) { + editor.transact((tr) => { + let pos: number | undefined; + tr.doc.descendants((node, nodePos) => { + if (node.type.spec.group === "blockContent") { + pos = nodePos + 1; + return false; + } + return pos === undefined; + }); + tr.setSelection(TextSelection.create(tr.doc, pos!)); + }); +} + +describe("paste into empty inline-content block", () => { + it.each(["bulletListItem", "numberedListItem", "checkListItem"] as const)( + "pastes a paragraph into an empty %s without replacing it", + (type) => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type, content: "" }], + }); + mountEditor(editor); + selectStartOfFirstBlock(editor); + + editor.pasteHTML(`

        Pasted

        `); + + const blocks = editor.document; + expect(blocks[0].type).toBe(type); + expect(blocks[0].content).toEqual([ + { type: "text", text: "Pasted", styles: {} }, + ]); + }, + ); + + it("inserts paragraph content into an empty list item without dropping marks", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "" }], + }); + mountEditor(editor); + selectStartOfFirstBlock(editor); + + editor.pasteHTML(`

        Hello world

        `); + + const blocks = editor.document; + expect(blocks[0].type).toBe("bulletListItem"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "Hello ", styles: {} }, + { type: "text", text: "world", styles: { bold: true } }, + ]); + }); + + it("merges leading paragraph into empty list item and inserts rest as siblings", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "" }], + }); + mountEditor(editor); + selectStartOfFirstBlock(editor); + + editor.pasteHTML(`

        First

        Second

        `); + + const blocks = editor.document; + expect(blocks[0].type).toBe("bulletListItem"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "First", styles: {} }, + ]); + expect(blocks[1].type).toBe("paragraph"); + expect(blocks[1].content).toEqual([ + { type: "text", text: "Second", styles: {} }, + ]); + }); + + it("replaces an empty list item with a heading when pasting a heading", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "" }], + }); + mountEditor(editor); + selectStartOfFirstBlock(editor); + + editor.pasteHTML(`

        Heading

        `); + + // The empty list item is replaced by the heading rather than absorbing + // its inline content. Headings carry semantic meaning the user explicitly + // chose, so we keep them as-is. + const blocks = editor.document; + expect(blocks[0].type).toBe("heading"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "Heading", styles: {} }, + ]); + }); + + it("keeps the list item but discards the heading wrapper when a paragraph follows the heading", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "" }], + }); + mountEditor(editor); + selectStartOfFirstBlock(editor); + + // Heading first means the leading block is not a paragraph, so the + // unwrap/retype rule doesn't apply: the empty list item gets replaced + // by the pasted heading and the trailing paragraph follows as a sibling. + editor.pasteHTML(`

        Heading

        Body

        `); + + const blocks = editor.document; + expect(blocks[0].type).toBe("heading"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "Heading", styles: {} }, + ]); + expect(blocks[1].type).toBe("paragraph"); + expect(blocks[1].content).toEqual([ + { type: "text", text: "Body", styles: {} }, + ]); + }); + + it("still replaces the empty list item when pasting another list item", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "" }], + }); + mountEditor(editor); + selectStartOfFirstBlock(editor); + + editor.pasteHTML(`
        • Pasted item
        `); + + const blocks = editor.document; + expect(blocks[0].type).toBe("bulletListItem"); + // The empty list item should be replaced (not have inline content + // appended in-place), which matches the existing behavior. + expect(blocks[0].content).toEqual([ + { type: "text", text: "Pasted item", styles: {} }, + ]); + }); + + it("pastes a paragraph into a non-empty list item without replacing it", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "abc" }], + }); + mountEditor(editor); + editor.setTextCursorPosition(editor.document[0].id, "end"); + + editor.pasteHTML(`

        hello

        `); + + const blocks = editor.document; + expect(blocks[0].type).toBe("bulletListItem"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "abchello", styles: {} }, + ]); + }); + + it("pastes bare
      2. a
      3. b
      4. into an empty list item as two list items", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "" }], + }); + mountEditor(editor); + selectStartOfFirstBlock(editor); + + editor.pasteHTML(`
      5. a
      6. b
      7. `); + + const blocks = editor.document; + expect(blocks[0].type).toBe("bulletListItem"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "a", styles: {} }, + ]); + expect(blocks[1].type).toBe("bulletListItem"); + expect(blocks[1].content).toEqual([ + { type: "text", text: "b", styles: {} }, + ]); + }); + + it("pastes bare
      8. a
      9. b
      10. into a non-empty list item, splicing the first into the cursor", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "X" }], + }); + mountEditor(editor); + editor.setTextCursorPosition(editor.document[0].id, "end"); + + editor.pasteHTML(`
      11. a
      12. b
      13. `); + + const blocks = editor.document; + expect(blocks[0].type).toBe("bulletListItem"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "Xa", styles: {} }, + ]); + expect(blocks[1].type).toBe("bulletListItem"); + expect(blocks[1].content).toEqual([ + { type: "text", text: "b", styles: {} }, + ]); + }); + + it("pastes two list items into an empty list item as two siblings", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "" }], + }); + mountEditor(editor); + selectStartOfFirstBlock(editor); + + editor.pasteHTML(`
        • a
        • b
        `); + + // The empty list item is replaced by the two pasted list items. + const blocks = editor.document; + expect(blocks[0].type).toBe("bulletListItem"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "a", styles: {} }, + ]); + expect(blocks[1].type).toBe("bulletListItem"); + expect(blocks[1].content).toEqual([ + { type: "text", text: "b", styles: {} }, + ]); + }); + + it("pastes two list items into a non-empty list item, splicing the first into the cursor", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "X" }], + }); + mountEditor(editor); + editor.setTextCursorPosition(editor.document[0].id, "end"); + + editor.pasteHTML(`
        • a
        • b
        `); + + // The first list item's inline content is spliced at the cursor (the + // existing list item becomes "Xa") and the second list item becomes a + // new sibling. + const blocks = editor.document; + expect(blocks[0].type).toBe("bulletListItem"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "Xa", styles: {} }, + ]); + expect(blocks[1].type).toBe("bulletListItem"); + expect(blocks[1].content).toEqual([ + { type: "text", text: "b", styles: {} }, + ]); + }); + + it("preserves nested list structure when pasting a nested list into an empty list item", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "bulletListItem", content: "" }], + }); + mountEditor(editor); + selectStartOfFirstBlock(editor); + + // Pasting a list with nested children: the leading block is a list item, + // so the unwrap/retype rule does not apply and the existing slice-level + // list-nesting fix in `transformPasted` keeps the nested structure + // intact. + editor.pasteHTML(`
        • Outer
          • Inner
        `); + + const blocks = editor.document; + expect(blocks[0].type).toBe("bulletListItem"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "Outer", styles: {} }, + ]); + expect(blocks[0].children).toHaveLength(1); + expect(blocks[0].children[0].type).toBe("bulletListItem"); + expect(blocks[0].children[0].content).toEqual([ + { type: "text", text: "Inner", styles: {} }, + ]); + }); + + it("preserves the existing paragraph behavior when pasting into a non-empty paragraph", () => { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "paragraph", content: "Existing " }], + }); + mountEditor(editor); + editor.setTextCursorPosition(editor.document[0].id, "end"); + + editor.pasteHTML(`

        added

        `); + + const blocks = editor.document; + expect(blocks[0].type).toBe("paragraph"); + expect(blocks[0].content).toEqual([ + { type: "text", text: "Existing added", styles: {} }, + ]); + }); +}); diff --git a/packages/core/src/editor/transformPasted.ts b/packages/core/src/editor/transformPasted.ts index 2985ad33dc..4f0515df95 100644 --- a/packages/core/src/editor/transformPasted.ts +++ b/packages/core/src/editor/transformPasted.ts @@ -113,6 +113,11 @@ export function transformPasted(slice: Slice, view: EditorView) { let f = Fragment.from(slice.content); f = wrapTableRows(f, view.state.schema); + const retyped = retypeLeadingParagraphForEmptyTarget(f, view, slice); + if (retyped) { + return retyped; + } + if (isInTableCell(view)) { let hasTableContent = false; f.descendants((node) => { @@ -173,6 +178,78 @@ export function transformPasted(slice: Slice, view: EditorView) { return new Slice(f, slice.openStart, slice.openEnd); } +/** + * Pasting plain text into an empty inline-content block (e.g. an empty + * bullet list item) would normally replace that block with a paragraph: + * BlockNote's serializer always wraps content in + * `blockGroup > blockContainer > `, producing a closed slice that + * ProseMirror inserts as a new block rather than splicing inline. + * + * To preserve the empty block's type, retype the leading paragraph in the + * slice to match the target block. Subsequent blocks in the slice are left + * alone and end up as siblings. + * + * Scoped to: empty, non-paragraph, inline-content target + paragraph leading + * the slice. A non-empty target already gives ProseMirror a valid inline + * insertion point so it splices correctly on its own; non-paragraph leading + * blocks (heading, list item, …) carry semantic meaning the user picked, so + * we keep the existing replace behavior. + */ +function retypeLeadingParagraphForEmptyTarget( + fragment: Fragment, + view: EditorView, + slice: Slice, +): Slice | null { + if (isInTableCell(view)) { + return null; + } + + // `transformPasted` is also called for drop events, where the slice will be + // inserted at the drop position rather than the current selection. In that + // case the selection-derived target is wrong, so bail out and let the + // default behavior handle drops. + if (view.dragging) { + return null; + } + + const blockInfo = getBlockInfoFromSelection(view.state); + const target = blockInfo.isBlockContainer + ? blockInfo.blockContent.node + : null; + if ( + !target || + target.type.name === "paragraph" || + target.type.spec.content !== "inline*" || + target.childCount > 0 + ) { + return null; + } + + const blockGroup = fragment.firstChild; + const blockContainer = blockGroup?.firstChild; + const leading = blockContainer?.firstChild; + if ( + blockGroup?.type.name !== "blockGroup" || + blockContainer?.type.name !== "blockContainer" || + leading?.type.name !== "paragraph" + ) { + return null; + } + + const retyped = target.type.create(target.attrs, leading.content); + const newBlockContainer = blockContainer.copy( + blockContainer.content.replaceChild(0, retyped), + ); + const newBlockGroup = blockGroup.copy( + blockGroup.content.replaceChild(0, newBlockContainer), + ); + return new Slice( + fragment.replaceChild(0, newBlockGroup), + slice.openStart, + slice.openEnd, + ); +} + /** * Used in `transformPasted` to check if the fix there should be applied, i.e. * if the pasted fragment should be wrapped in a `blockContainer` node. This diff --git a/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor-forked.json b/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor-forked.json index 786e727b7e..6af753a17a 100644 --- a/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor-forked.json +++ b/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor-forked.json @@ -8,18 +8,7 @@ "type": "text", }, ], - "id": "2", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [], - "id": "3", + "id": "1", "props": { "backgroundColor": "default", "textAlignment": "left", diff --git a/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor.json b/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor.json index e7580c5b7b..f1195c7f24 100644 --- a/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor.json +++ b/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-editor.json @@ -16,15 +16,4 @@ }, "type": "paragraph", }, - { - "children": [], - "content": [], - "id": "1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, ] \ No newline at end of file diff --git a/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-forked.html b/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-forked.html index 8957bbb259..dcee61e04e 100644 --- a/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-forked.html +++ b/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap-forked.html @@ -1 +1 @@ -Hello World \ No newline at end of file +Hello World \ No newline at end of file diff --git a/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap.html b/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap.html index 063ddebeac..d804ca0a6f 100644 --- a/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap.html +++ b/packages/core/src/extensions/Collaboration/__snapshots__/fork-yjs-snap.html @@ -1 +1 @@ -Hello \ No newline at end of file +Hello \ No newline at end of file diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts index 021c20ea3d..84524eee2e 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts @@ -1,4 +1,4 @@ -import { NodeSelection, TextSelection } from "prosemirror-state"; +import { TextSelection } from "prosemirror-state"; import { createExtension, @@ -16,15 +16,6 @@ export const FormattingToolbarExtension = createExtension(({ editor }) => { return false; } - // Don't show if a block with inline content is selected. - if ( - tr.selection instanceof NodeSelection && - (tr.selection.node.type.spec.content === "inline*" || - tr.selection.node.firstChild?.type.spec.content === "inline*") - ) { - return false; - } - // Don't show if the selection is a text selection but contains no text. if ( tr.selection instanceof TextSelection && @@ -61,16 +52,17 @@ export const FormattingToolbarExtension = createExtension(({ editor }) => { * We want to mimic the Notion behavior of not showing the toolbar while the user is holding down the mouse button (to create a selection) */ let preventShowWhileMouseDown = false; + let preventShowWhileDragging = false; const unsubscribeOnChange = editor.onChange(() => { - if (preventShowWhileMouseDown) { + if (preventShowWhileMouseDown || preventShowWhileDragging) { return; } // re-evaluate whether the toolbar should be shown store.setState(shouldShow()); }); const unsubscribeOnSelectionChange = editor.onSelectionChange(() => { - if (preventShowWhileMouseDown) { + if (preventShowWhileMouseDown || preventShowWhileDragging) { return; } // re-evaluate whether the toolbar should be shown @@ -91,6 +83,7 @@ export const FormattingToolbarExtension = createExtension(({ editor }) => { "pointerup", () => { preventShowWhileMouseDown = false; + // We only want to re-show the toolbar if the mouse made the selection if (editor.isFocused()) { store.setState(shouldShow()); @@ -102,12 +95,26 @@ export const FormattingToolbarExtension = createExtension(({ editor }) => { dom.addEventListener( "pointercancel", () => { - preventShowWhileMouseDown = false; + preventShowWhileMouseDown = true; }, - { - signal, - capture: true, + { signal, capture: true }, + ); + + editor.prosemirrorView.root.addEventListener( + "dragstart", + () => { + preventShowWhileDragging = true; + store.setState(false); + }, + { signal }, + ); + + editor.prosemirrorView.root.addEventListener( + "dragend", + () => { + preventShowWhileDragging = false; }, + { signal }, ); signal.addEventListener("abort", () => { diff --git a/packages/core/src/extensions/LinkToolbar/LinkToolbar.ts b/packages/core/src/extensions/LinkToolbar/LinkToolbar.ts index 1a61d67d44..a4377ab599 100644 --- a/packages/core/src/extensions/LinkToolbar/LinkToolbar.ts +++ b/packages/core/src/extensions/LinkToolbar/LinkToolbar.ts @@ -1,5 +1,4 @@ -import { getMarkRange, posToDOMRect } from "@tiptap/core"; -import { getPmSchema } from "../../api/pmUtil.js"; +import { posToDOMRect } from "@tiptap/core"; import { createExtension } from "../../editor/BlockNoteExtension.js"; export const LinkToolbarExtension = createExtension(({ editor }) => { @@ -14,47 +13,35 @@ export const LinkToolbarExtension = createExtension(({ editor }) => { return null; } - function getMarkAtPos(pos: number, markType: string) { - return editor.transact((tr) => { - const resolvedPos = tr.doc.resolve(pos); - const mark = resolvedPos - .marks() - .find((mark) => mark.type.name === markType); - - if (!mark) { - return; - } - - const markRange = getMarkRange(resolvedPos, mark.type); - if (!markRange) { - return; - } + function getLinkAtPos(pos: number) { + const linkData = editor.getLinkMarkAtPos(pos); + if (!linkData) { + return undefined; + } - return { - range: markRange, - mark, - get text() { - return tr.doc.textBetween(markRange.from, markRange.to); - }, - get position() { - // to minimize re-renders, we convert to JSON, which is the same shape anyway - return posToDOMRect( - editor.prosemirrorView, - markRange.from, - markRange.to, - ).toJSON() as DOMRect; - }, - }; - }); + return { + range: { from: linkData.from, to: linkData.to }, + // Expose mark-like attrs for backward compat with React LinkToolbarController + mark: { attrs: { href: linkData.href } }, + get text() { + return linkData.text; + }, + get position() { + return posToDOMRect( + editor.prosemirrorView, + linkData.from, + linkData.to, + ).toJSON() as DOMRect; + }, + }; } function getLinkAtSelection() { return editor.transact((tr) => { - const selection = tr.selection; - if (!selection.empty) { + if (!tr.selection.empty) { return undefined; } - return getMarkAtPos(selection.anchor, "link"); + return getLinkAtPos(tr.selection.anchor); }); } @@ -63,12 +50,14 @@ export const LinkToolbarExtension = createExtension(({ editor }) => { getLinkAtSelection, getLinkElementAtPos, - getMarkAtPos, + getMarkAtPos(pos: number, _markType: string) { + return getLinkAtPos(pos); + }, getLinkAtElement(element: HTMLElement) { return editor.transact(() => { const posAtElement = editor.prosemirrorView.posAtDOM(element, 0) + 1; - return getMarkAtPos(posAtElement, "link"); + return getLinkAtPos(posAtElement); }); }, @@ -77,45 +66,11 @@ export const LinkToolbarExtension = createExtension(({ editor }) => { text: string, position = editor.transact((tr) => tr.selection.anchor), ) { - editor.transact((tr) => { - const pmSchema = getPmSchema(tr); - const { range } = getMarkAtPos(position + 1, "link") || { - range: { - from: tr.selection.from, - to: tr.selection.to, - }, - }; - if (!range) { - return; - } - tr.insertText(text, range.from, range.to); - tr.addMark( - range.from, - range.from + text.length, - pmSchema.mark("link", { href: url }), - ); - }); - editor.prosemirrorView.focus(); + editor.editLink(url, text, position); }, - deleteLink(position = editor.transact((tr) => tr.selection.anchor)) { - editor.transact((tr) => { - const pmSchema = getPmSchema(tr); - const { range } = getMarkAtPos(position + 1, "link") || { - range: { - from: tr.selection.from, - to: tr.selection.to, - }, - }; - if (!range) { - return; - } - tr.removeMark(range.from, range.to, pmSchema.marks["link"]).setMeta( - "preventAutolink", - true, - ); - }); - editor.prosemirrorView.focus(); + deleteLink(position = editor.transact((tr) => tr.selection.anchor)) { + editor.deleteLink(position); }, } as const; }); diff --git a/packages/core/src/extensions/Placeholder/Placeholder.ts b/packages/core/src/extensions/Placeholder/Placeholder.ts index 4a3185e353..b8ff2e14ed 100644 --- a/packages/core/src/extensions/Placeholder/Placeholder.ts +++ b/packages/core/src/extensions/Placeholder/Placeholder.ts @@ -1,6 +1,6 @@ import { Plugin, PluginKey } from "prosemirror-state"; import { Decoration, DecorationSet } from "prosemirror-view"; -import { v4 } from "uuid"; +import { uuidv4 } from "lib0/random"; import { createExtension, ExtensionOptions, @@ -23,7 +23,7 @@ export const PlaceholderExtension = createExtension( new Plugin({ key: PLUGIN_KEY, view: (view) => { - const uniqueEditorSelector = `placeholder-selector-${v4()}`; + const uniqueEditorSelector = `placeholder-selector-${uuidv4()}`; view.dom.classList.add(uniqueEditorSelector); const styleEl = document.createElement("style"); diff --git a/packages/core/src/extensions/SideMenu/SideMenu.ts b/packages/core/src/extensions/SideMenu/SideMenu.ts index 635929a756..e98059b585 100644 --- a/packages/core/src/extensions/SideMenu/SideMenu.ts +++ b/packages/core/src/extensions/SideMenu/SideMenu.ts @@ -785,8 +785,8 @@ export const SideMenuExtension = createExtension(({ editor }) => { * interfering with open submenus. */ hideMenuIfNotFrozen() { - if (!view!.menuFrozen && view!.state!.show) { - view!.state!.show = false; + if (!view!.menuFrozen && view!.state?.show) { + view!.state.show = false; view!.emitUpdate(view!.state!); } }, diff --git a/packages/core/src/extensions/SideMenu/dragging.ts b/packages/core/src/extensions/SideMenu/dragging.ts index 54dc3eeae2..f8ba326538 100644 --- a/packages/core/src/extensions/SideMenu/dragging.ts +++ b/packages/core/src/extensions/SideMenu/dragging.ts @@ -101,16 +101,13 @@ function setDragImage(view: EditorView, from: number, to = from) { // Browsers may have CORS policies which prevents iframes from being // manipulated, so better to stay on the safe side and remove them from the - // drag preview. The drag preview doesn't work with iframes anyway. - const iframes = dragImageElement.getElementsByTagName("iframe"); - for (let i = 0; i < iframes.length; i++) { - const iframe = iframes[i]; - const parent = iframe.parentElement; - - if (parent) { - parent.removeChild(iframe); - } - } + // drag preview. The drag preview doesn't work with embedded documents + // (iframe/embed/object) anyway, and including an (e.g. a PDF) + // can prevent the drag from initiating at all. + const embeddedDocs = dragImageElement.querySelectorAll( + "iframe, embed, object", + ); + embeddedDocs.forEach((el) => el.parentElement?.removeChild(el)); // TODO: This is hacky, need a better way of assigning classes to the editor so that they can also be applied to the // drag preview. diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index bd4a517900..015cf22420 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -9,6 +9,7 @@ import { } from "../../schema/index.js"; import { formatKeyboardShortcut } from "../../util/browser.js"; import { FilePanelExtension } from "../FilePanel/FilePanel.js"; +import { FormattingToolbarExtension } from "../FormattingToolbar/FormattingToolbar.js"; import { DefaultSuggestionItem } from "./DefaultSuggestionItem.js"; import { SuggestionMenu } from "./SuggestionMenu.js"; @@ -242,6 +243,11 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id); + // Immediately hide the formatting toolbar. This is only necessary for + // when the `trailingBlock` editor option is set to `false` and the + // inserted block is at the end of the document. Otherwise, the + // selection moves to the next block with inline content. + editor.getExtension(FormattingToolbarExtension)?.store.setState(false); }, key: "image", ...editor.dictionary.slash_menu.image, @@ -257,6 +263,11 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id); + // Immediately hide the formatting toolbar. This is only necessary for + // when the `trailingBlock` editor option is set to `false` and the + // inserted block is at the end of the document. Otherwise, the + // selection moves to the next block with inline content. + editor.getExtension(FormattingToolbarExtension)?.store.setState(false); }, key: "video", ...editor.dictionary.slash_menu.video, @@ -272,6 +283,11 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id); + // Immediately hide the formatting toolbar. This is only necessary for + // when the `trailingBlock` editor option is set to `false` and the + // inserted block is at the end of the document. Otherwise, the + // selection moves to the next block with inline content. + editor.getExtension(FormattingToolbarExtension)?.store.setState(false); }, key: "audio", ...editor.dictionary.slash_menu.audio, @@ -287,6 +303,11 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id); + // Immediately hide the formatting toolbar. This is only necessary for + // when the `trailingBlock` editor option is set to `false` and the + // inserted block is at the end of the document. Otherwise, the + // selection moves to the next block with inline content. + editor.getExtension(FormattingToolbarExtension)?.store.setState(false); }, key: "file", ...editor.dictionary.slash_menu.file, diff --git a/packages/core/src/extensions/TableHandles/TableHandles.ts b/packages/core/src/extensions/TableHandles/TableHandles.ts index 7f3cf774b5..d957056f4e 100644 --- a/packages/core/src/extensions/TableHandles/TableHandles.ts +++ b/packages/core/src/extensions/TableHandles/TableHandles.ts @@ -466,6 +466,8 @@ export class TableHandlesView implements PluginView { event.preventDefault(); const { draggingState, colIndex, rowIndex } = this.state; + // Clear so a re-dispatched drop short-circuits above (issue #2691). + this.state.draggingState = undefined; const columnWidths = this.state.block.content.columnWidths; diff --git a/packages/core/src/extensions/TrailingNode/TrailingNode.ts b/packages/core/src/extensions/TrailingNode/TrailingNode.ts index 523c5fef4a..59f95d2bed 100644 --- a/packages/core/src/extensions/TrailingNode/TrailingNode.ts +++ b/packages/core/src/extensions/TrailingNode/TrailingNode.ts @@ -1,76 +1,135 @@ -import { Plugin, PluginKey } from "prosemirror-state"; -import { createExtension } from "../../editor/BlockNoteExtension.js"; +import type { Node as PMNode } from "prosemirror-model"; +import { Plugin, PluginKey, type Transaction } from "prosemirror-state"; +import { Decoration, DecorationSet } from "prosemirror-view"; +import { + createExtension, + ExtensionOptions, +} from "../../editor/BlockNoteExtension.js"; -// based on https://github.com/ueberdosis/tiptap/blob/40a9404c94c7fef7900610c195536384781ae101/demos/src/Experiments/TrailingNode/Vue/trailing-node.ts +const PLUGIN_KEY = new PluginKey("trailingNode"); -/** - * Extension based on: - * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js - * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts - */ -const plugin = new PluginKey("trailingNode"); +// Skip the widget when the editor isn't editable, or when the document already +// ends with an empty paragraph block (since the user can just type into it). +function shouldShowTrailingWidget(doc: PMNode, isEditable: boolean): boolean { + if (!isEditable) { + return false; + } -/** - * Add a trailing node to the document so the user can always click at the bottom of the document and start typing - */ -export const TrailingNodeExtension = createExtension(() => { - return { - key: "trailingNode", - prosemirrorPlugins: [ - new Plugin({ - key: plugin, - appendTransaction: (_, __, state) => { - const { doc, tr, schema } = state; - const shouldInsertNodeAtEnd = plugin.getState(state); - const endPosition = doc.content.size - 2; - const type = schema.nodes["blockContainer"]; - const contentType = schema.nodes["paragraph"]; - if (!shouldInsertNodeAtEnd) { - return; - } - - return tr.insert( - endPosition, - type.create(undefined, contentType.create()), - ); - }, - state: { - init: (_, _state) => { - // (maybe fix): use same logic as apply() here - // so it works when initializing - }, - apply: (tr, value) => { - if (!tr.docChanged) { - return value; - } + const rootGroup = doc.lastChild; + const lastBlock = rootGroup?.lastChild; + const lastContent = lastBlock?.firstChild; - let lastNode = tr.doc.lastChild; + return !( + lastBlock?.type.name === "blockContainer" && + lastContent?.type.name === "paragraph" && + lastContent.content.size === 0 + ); +} - if (!lastNode || lastNode.type.name !== "blockGroup") { - throw new Error("Expected blockGroup"); - } +/** + * Renders a fake trailing block as a widget decoration after the last block of + * the document. Clicking it inserts a real trailing block and moves the + * selection into it. This way the trailing block is not part of the document + * content, so it doesn't appear when the editor is read-only or when the + * content is exported. + */ +export const TrailingNodeExtension = createExtension( + ({ editor }: ExtensionOptions) => { + function createTrailingWidget(pos: number): Decoration { + return Decoration.widget( + pos, + () => { + const el = document.createElement("div"); + el.className = "bn-trailing-block"; + el.contentEditable = "false"; + el.addEventListener("mousedown", (event) => { + // Stop ProseMirror from trying to place the selection somewhere + // based on this click. + event.preventDefault(); - lastNode = lastNode.lastChild; + editor.transact((tr) => { + const [insertedBlock] = editor.insertBlocks( + [{ type: "paragraph" }], + editor.document[editor.document.length - 1], + "after", + ); + editor.setTextCursorPosition(insertedBlock, "start"); + tr.scrollIntoView(); + }); - if (!lastNode || lastNode.type.name !== "blockContainer") { - return true; // not a blockContainer, but for example Columns. Insert trailing node - } + editor.prosemirrorView?.focus(); + }); + return el; + }, + { side: 1 }, + ); + } - const lastContentNode = lastNode.firstChild; + // Maps the existing DecorationSet through the transaction, then + // incrementally adds or removes the widget only if the show/hide state + // crossed over. The underlying Decoration (and its rendered DOM) stays + // reference-stable across transactions. + function nextDecorationSet( + tr: Transaction, + oldSet: DecorationSet, + isEditable: boolean, + ): DecorationSet { + const mapped = oldSet.map(tr.mapping, tr.doc); + const existing = mapped.find(); + const wasShowing = existing.length > 0; + const shouldShow = shouldShowTrailingWidget(tr.doc, isEditable); - if (!lastContentNode) { - throw new Error("Expected blockContent"); - } + if (wasShowing === shouldShow) { + return mapped; + } + if (wasShowing) { + return mapped.remove(existing); + } + return mapped.add(tr.doc, [ + createTrailingWidget(tr.doc.content.size - 1), + ]); + } - // If last node is not empty (size > 4) or it doesn't contain - // inline content, we need to add a trailing node. - return ( - lastNode.nodeSize > 4 || - lastContentNode.type.spec.content !== "inline*" - ); + return { + key: "trailingNode", + prosemirrorPlugins: [ + new Plugin({ + key: PLUGIN_KEY, + state: { + init: (_, state) => + nextDecorationSet( + state.tr, + DecorationSet.empty, + editor.isEditable, + ), + apply: (tr, oldSet) => { + if (!tr.docChanged && !tr.getMeta(PLUGIN_KEY)) { + return oldSet; + } + return nextDecorationSet(tr, oldSet, editor.isEditable); + }, }, - }, - }), - ], - } as const; -}); + // Editable changes don't dispatch a transaction on their own, so the + // plugin state can't re-evaluate on its own. Watch for the change + // and dispatch a no-op transaction tagged with this plugin's key so + // `apply` re-runs and adds or removes the widget. + view(view) { + let lastEditable = view.editable; + return { + update(view) { + if (view.editable === lastEditable) { + return; + } + lastEditable = view.editable; + view.dispatch(view.state.tr.setMeta(PLUGIN_KEY, true)); + }, + }; + }, + props: { + decorations: (state) => PLUGIN_KEY.getState(state), + }, + }), + ], + } as const; + }, +); diff --git a/packages/core/src/extensions/tiptap-extensions/Link/helpers/autolink.ts b/packages/core/src/extensions/tiptap-extensions/Link/helpers/autolink.ts new file mode 100644 index 0000000000..88ca510c2a --- /dev/null +++ b/packages/core/src/extensions/tiptap-extensions/Link/helpers/autolink.ts @@ -0,0 +1,179 @@ +import type { NodeWithPos } from "@tiptap/core"; +import { + combineTransactionSteps, + findChildrenInRange, + getChangedRanges, + getMarksBetween, +} from "@tiptap/core"; +import type { MarkType } from "@tiptap/pm/model"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import type { LinkMatch } from "./linkDetector.js"; +import { tokenizeLink } from "./linkDetector.js"; + +import { + UNICODE_WHITESPACE_REGEX, + UNICODE_WHITESPACE_REGEX_END, +} from "./whitespace.js"; + +/** + * Check if the provided tokens form a valid link structure, which can either be a single link token + * or a link token surrounded by parentheses or square brackets. + */ +function isValidLinkStructure(tokens: LinkMatch[]) { + if (tokens.length === 1) { + return tokens[0].isLink; + } + + if (tokens.length === 3 && tokens[1].isLink) { + return ["()", "[]"].includes(tokens[0].value + tokens[2].value); + } + + return false; +} + +type AutolinkOptions = { + type: MarkType; + defaultProtocol: string; + validate: (url: string) => boolean; + shouldAutoLink: (url: string) => boolean; +}; + +/** + * Plugin that automatically adds link marks when typing URLs. + */ +export function autolink(options: AutolinkOptions): Plugin { + return new Plugin({ + key: new PluginKey("autolink"), + appendTransaction: (transactions, oldState, newState) => { + const docChanges = + transactions.some((transaction) => transaction.docChanged) && + !oldState.doc.eq(newState.doc); + + const preventAutolink = transactions.some((transaction) => + transaction.getMeta("preventAutolink") + ); + + if (!docChanges || preventAutolink) { + return; + } + + const { tr } = newState; + const transform = combineTransactionSteps(oldState.doc, [ + ...transactions, + ]); + const changes = getChangedRanges(transform); + + changes.forEach(({ newRange }) => { + const nodesInChangedRanges = findChildrenInRange( + newState.doc, + newRange, + (node) => node.isTextblock + ); + + let textBlock: NodeWithPos | undefined; + let textBeforeWhitespace: string | undefined; + + if (nodesInChangedRanges.length > 1) { + textBlock = nodesInChangedRanges[0]; + textBeforeWhitespace = newState.doc.textBetween( + textBlock.pos, + textBlock.pos + textBlock.node.nodeSize, + undefined, + " " + ); + } else if (nodesInChangedRanges.length) { + const endText = newState.doc.textBetween( + newRange.from, + newRange.to, + " ", + " " + ); + if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) { + return; + } + textBlock = nodesInChangedRanges[0]; + textBeforeWhitespace = newState.doc.textBetween( + textBlock.pos, + newRange.to, + undefined, + " " + ); + } + + if (textBlock && textBeforeWhitespace) { + const wordsBeforeWhitespace = textBeforeWhitespace + .split(UNICODE_WHITESPACE_REGEX) + .filter(Boolean); + + if (wordsBeforeWhitespace.length <= 0) { + return; + } + + const lastWordBeforeSpace = + wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]; + const lastWordAndBlockOffset = + textBlock.pos + + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace); + + if (!lastWordBeforeSpace) { + return; + } + + const linksBeforeSpace = tokenizeLink( + lastWordBeforeSpace, + options.defaultProtocol + ); + + if (!isValidLinkStructure(linksBeforeSpace)) { + return; + } + + linksBeforeSpace + .filter((link) => link.isLink) + .map((link) => ({ + ...link, + from: lastWordAndBlockOffset + link.start + 1, + to: lastWordAndBlockOffset + link.end + 1, + })) + // ignore link inside code mark + .filter((link) => { + if (!newState.schema.marks.code) { + return true; + } + + return !newState.doc.rangeHasMark( + link.from, + link.to, + newState.schema.marks.code + ); + }) + .filter((link) => options.validate(link.value)) + .filter((link) => options.shouldAutoLink(link.value)) + .forEach((link) => { + if ( + getMarksBetween(link.from, link.to, newState.doc).some( + (item) => item.mark.type === options.type + ) + ) { + return; + } + + tr.addMark( + link.from, + link.to, + options.type.create({ + href: link.href, + }) + ); + }); + } + }); + + if (!tr.steps.length) { + return; + } + + return tr; + }, + }); +} diff --git a/packages/core/src/extensions/tiptap-extensions/Link/helpers/clickHandler.ts b/packages/core/src/extensions/tiptap-extensions/Link/helpers/clickHandler.ts new file mode 100644 index 0000000000..d41082cc17 --- /dev/null +++ b/packages/core/src/extensions/tiptap-extensions/Link/helpers/clickHandler.ts @@ -0,0 +1,82 @@ +import type { Editor } from "@tiptap/core"; +import { getAttributes } from "@tiptap/core"; +import type { MarkType } from "@tiptap/pm/model"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; + +type ClickHandlerOptions = { + type: MarkType; + tiptapEditor: Editor; + editor?: BlockNoteEditor; + onClick?: ( + event: MouseEvent, + editor: BlockNoteEditor, + ) => boolean | void; +}; + +export function clickHandler(options: ClickHandlerOptions): Plugin { + return new Plugin({ + key: new PluginKey("handleClickLink"), + props: { + handleClick: (view, _pos, event) => { + if (event.button !== 0) { + return false; + } + + if (!view.editable) { + return false; + } + + let link: HTMLAnchorElement | null = null; + + if ( + event.target instanceof HTMLAnchorElement && + // Differentiate between link inline content and read-only links. + event.target.getAttribute("data-inline-content-type") === "link" + ) { + link = event.target; + } else { + const target = event.target as HTMLElement | null; + if (!target) { + return false; + } + + const root = options.tiptapEditor.view.dom; + + // Intentionally limit the lookup to the editor root. + // Using tag names like DIV as boundaries breaks with custom NodeViews, + link = target.closest( + 'a[data-inline-content-type="link"]', + ); + + if (link && !root.contains(link)) { + link = null; + } + } + + if (!link) { + return false; + } + + if (options.onClick) { + if (!options.editor) { + throw new Error("BlockNoteEditor not found in Link click handler"); + } + const result = options.onClick(event, options.editor); + return result ?? true; + } + + const attrs = getAttributes(view.state, options.type.name); + const href = link.href ?? attrs.href; + const target = link.target ?? attrs.target; + + if (href) { + window.open(href, target); + return true; + } + + return false; + }, + }, + }); +} diff --git a/packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts b/packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts new file mode 100644 index 0000000000..310bb9a5d8 --- /dev/null +++ b/packages/core/src/extensions/tiptap-extensions/Link/helpers/linkDetector.ts @@ -0,0 +1,414 @@ +/** + * Lightweight URL detection module replacing linkifyjs. + * + * Provides two functions: + * - findLinks(): find all URLs/emails in arbitrary text (replaces linkifyjs find()) + * - tokenizeLink(): tokenize a single word for autolink validation (replaces linkifyjs tokenize()) + */ + +import { ENCODED_TLDS } from "./tlds.js"; + +export interface LinkMatch { + type: string; + value: string; + isLink: boolean; + href: string; + start: number; + end: number; +} + +// --------------------------------------------------------------------------- +// TLD set – used only for schemeless URL validation. +// Protocol URLs (http://, https://, etc.) skip TLD checks. +// Decoded once at module load from the trie-encoded IANA list in tlds.ts. +// --------------------------------------------------------------------------- + +function decodeTlds(encoded: string): string[] { + const words: string[] = []; + const stack: string[] = []; + let i = 0; + while (i < encoded.length) { + let popDigitCount = 0; + while ( + i + popDigitCount < encoded.length && + encoded.charCodeAt(i + popDigitCount) >= 48 && + encoded.charCodeAt(i + popDigitCount) <= 57 + ) { + popDigitCount++; + } + if (popDigitCount > 0) { + words.push(stack.join("")); + let popCount = parseInt(encoded.substring(i, i + popDigitCount), 10); + while (popCount-- > 0) { + stack.pop(); + } + i += popDigitCount; + } else { + stack.push(encoded[i]); + i++; + } + } + return words; +} + +const TLD_SET = new Set(decodeTlds(ENCODED_TLDS)); + +// Special hostnames recognized without a TLD +const SPECIAL_HOSTS = new Set(["localhost"]); + +// --------------------------------------------------------------------------- +// Regex building blocks +// --------------------------------------------------------------------------- + +// Characters that are unlikely to be part of a URL when they appear at the end +const TRAILING_PUNCT = /[.,;:!?"']+$/; + +// Protocol URLs: http:// https:// ftp:// ftps:// +const PROTOCOL_RE = + /(?:https?|ftp|ftps):\/\/[^\s]+/g; + +// Mailto URLs: mailto:... +const MAILTO_RE = /mailto:[^\s]+/g; + +// Bare email addresses: user@domain.tld +const EMAIL_RE = + /[a-zA-Z0-9._%+-]+@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}/g; + +// Schemeless URLs: domain.tld with optional port and path +// Hostname: one or more labels separated by dots, TLD is alpha-only 2+ chars +const SCHEMELESS_RE = + /(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?::\d{1,5})?(?:[/?#][^\s]*)?/g; + +// --------------------------------------------------------------------------- +// Post-processing helpers +// --------------------------------------------------------------------------- + +/** + * Trim trailing punctuation and unbalanced closing brackets from a URL match. + */ +function trimTrailing(value: string): string { + let v = value; + + // Iteratively trim trailing punctuation and unbalanced brackets + let changed = true; + while (changed) { + changed = false; + + // Trim trailing punctuation chars + const before = v; + v = v.replace(TRAILING_PUNCT, ""); + if (v !== before) { + changed = true; + } + + // Trim unbalanced closing brackets from the end + for (const [open, close] of [ + ["(", ")"], + ["[", "]"], + ] as const) { + while (v.endsWith(close)) { + const openCount = countChar(v, open); + const closeCount = countChar(v, close); + if (closeCount > openCount) { + v = v.slice(0, -1); + changed = true; + } else { + break; + } + } + } + } + + return v; +} + +function countChar(str: string, ch: string): number { + let count = 0; + for (let i = 0; i < str.length; i++) { + if (str[i] === ch) { + count++; + } + } + return count; +} + +/** + * Extract the TLD from a hostname string. + * Returns the last dot-separated segment. + */ +function extractTld(hostname: string): string { + const parts = hostname.split("."); + return parts[parts.length - 1].toLowerCase(); +} + +function isValidTld(hostname: string): boolean { + const tld = extractTld(hostname); + return TLD_SET.has(tld); +} + +/** + * Build the href for a URL value, prepending the default protocol if needed. + */ +function buildHref( + value: string, + type: string, + defaultProtocol: string +): string { + if (type === "email") { + return "mailto:" + value; + } + if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value) || /^mailto:/i.test(value)) { + // Already has a protocol + return value; + } + return defaultProtocol + "://" + value; +} + +// --------------------------------------------------------------------------- +// findLinks() +// --------------------------------------------------------------------------- + +export interface FindOptions { + defaultProtocol?: string; +} + +interface RawMatch { + type: string; + value: string; + start: number; + end: number; +} + +/** + * Find all URLs and email addresses in the given text. + * Drop-in replacement for linkifyjs find(). + */ +export function findLinks( + text: string, + options?: FindOptions +): LinkMatch[] { + if (!text) { + return []; + } + + const defaultProtocol = options?.defaultProtocol || "http"; + const rawMatches: RawMatch[] = []; + + // 1. Protocol URLs + for (const m of text.matchAll(PROTOCOL_RE)) { + rawMatches.push({ + type: "url", + value: m[0], + start: m.index!, + end: m.index! + m[0].length, + }); + } + + // 2. Mailto URLs + for (const m of text.matchAll(MAILTO_RE)) { + rawMatches.push({ + type: "url", + value: m[0], + start: m.index!, + end: m.index! + m[0].length, + }); + } + + // 3. Bare email addresses + for (const m of text.matchAll(EMAIL_RE)) { + rawMatches.push({ + type: "email", + value: m[0], + start: m.index!, + end: m.index! + m[0].length, + }); + } + + // 4. Schemeless URLs + for (const m of text.matchAll(SCHEMELESS_RE)) { + rawMatches.push({ + type: "url", + value: m[0], + start: m.index!, + end: m.index! + m[0].length, + }); + } + + // Sort by start position + rawMatches.sort((a, b) => a.start - b.start || b.end - a.end); + + // Deduplicate overlapping matches (prefer earlier & longer) + const deduped: RawMatch[] = []; + let lastEnd = -1; + for (const match of rawMatches) { + if (match.start >= lastEnd) { + deduped.push(match); + lastEnd = match.end; + } + } + + // Post-process each match + const results: LinkMatch[] = []; + for (const raw of deduped) { + const value = trimTrailing(raw.value); + if (!value) { + continue; + } + + const start = raw.start; + const end = start + value.length; + + // For schemeless URLs, validate TLD + if (raw.type === "url" && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value)) { + const hostname = new URL("http://" + value).hostname; + if (!isValidTld(hostname)) { + continue; + } + } + + // For emails, validate TLD + if (raw.type === "email") { + const hostname = value.split("@")[1]; + if (!isValidTld(hostname)) { + continue; + } + } + + const href = buildHref(value, raw.type, defaultProtocol); + + results.push({ + type: raw.type, + value, + isLink: true, + href, + start, + end, + }); + } + + return results; +} + +// --------------------------------------------------------------------------- +// tokenizeLink() +// --------------------------------------------------------------------------- + +/** + * Tokenize a single word for autolink validation. + * Drop-in replacement for: tokenize(word).map(t => t.toObject(defaultProtocol)) + * + * Returns an array of LinkMatch tokens. The autolink code checks: + * - 1 token with isLink=true → valid single link + * - 3 tokens with middle isLink=true and outer brackets → valid wrapped link + */ +export function tokenizeLink( + text: string, + defaultProtocol = "http" +): LinkMatch[] { + if (!text) { + return [nonLinkToken(text, 0, 0)]; + } + + // Check for bracket wrapping: (url), [url], {url} + const brackets: Array<[string, string]> = [ + ["(", ")"], + ["[", "]"], + ["{", "}"], + ]; + for (const [open, close] of brackets) { + if (text.startsWith(open) && text.endsWith(close) && text.length > 2) { + const inner = text.slice(1, -1); + if (isSingleUrl(inner)) { + return [ + nonLinkToken(open, 0, 1), + linkToken(inner, 1, 1 + inner.length, defaultProtocol), + nonLinkToken(close, 1 + inner.length, text.length), + ]; + } + } + } + + // Check for trailing punctuation (e.g., "example.com." → link + dot) + if (text.endsWith(".") && text.length > 1) { + const withoutDot = text.slice(0, -1); + if (isSingleUrl(withoutDot)) { + return [ + linkToken(withoutDot, 0, withoutDot.length, defaultProtocol), + nonLinkToken(".", withoutDot.length, text.length), + ]; + } + } + + // Check if the whole text is a single URL + if (isSingleUrl(text)) { + return [linkToken(text, 0, text.length, defaultProtocol)]; + } + + // Not a link + return [nonLinkToken(text, 0, text.length)]; +} + +/** + * Check if a string is a single complete URL (no extra chars). + */ +function isSingleUrl(text: string): boolean { + // Protocol URLs + if (/^(?:https?|ftp|ftps):\/\/[^\s]+$/.test(text)) { + return true; + } + + // Mailto URLs + if (/^mailto:[^\s]+$/.test(text)) { + return true; + } + + // Special hosts (e.g., localhost) + if (SPECIAL_HOSTS.has(text.toLowerCase())) { + return true; + } + + // Schemeless URLs: hostname.tld with optional port and path + const schemelessFull = + /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+([a-zA-Z]{2,})(?::\d{1,5})?(?:[/?#][^\s]*)?$/; + const match = text.match(schemelessFull); + if (match) { + const tld = match[1].toLowerCase(); + // TLD must be a-z only (no numbers) and recognized + if (TLD_SET.has(tld)) { + return true; + } + } + + return false; +} + +function linkToken( + value: string, + start: number, + end: number, + defaultProtocol: string +): LinkMatch { + const type = + value.includes("@") && !value.includes("://") && !value.startsWith("mailto:") + ? "email" + : "url"; + return { + type, + value, + isLink: true, + href: buildHref(value, type, defaultProtocol), + start, + end, + }; +} + +function nonLinkToken(value: string, start: number, end: number): LinkMatch { + return { + type: "text", + value, + isLink: false, + href: value, + start, + end, + }; +} diff --git a/packages/core/src/extensions/tiptap-extensions/Link/helpers/pasteHandler.ts b/packages/core/src/extensions/tiptap-extensions/Link/helpers/pasteHandler.ts new file mode 100644 index 0000000000..f318d08b61 --- /dev/null +++ b/packages/core/src/extensions/tiptap-extensions/Link/helpers/pasteHandler.ts @@ -0,0 +1,53 @@ +import type { Editor } from "@tiptap/core"; +import type { MarkType } from "@tiptap/pm/model"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { findLinks } from "./linkDetector.js"; + +type PasteHandlerOptions = { + editor: Editor; + defaultProtocol: string; + type: MarkType; + shouldAutoLink?: (url: string) => boolean; + isValidLink: (href: string) => boolean; +}; + +export function pasteHandler(options: PasteHandlerOptions): Plugin { + return new Plugin({ + key: new PluginKey("handlePasteLink"), + props: { + handlePaste: (view, _event, slice) => { + const { shouldAutoLink, isValidLink } = options; + const { state } = view; + const { selection } = state; + const { empty } = selection; + + if (empty) { + return false; + } + + let textContent = ""; + + slice.content.forEach((node) => { + textContent += node.textContent; + }); + + const link = findLinks(textContent, { + defaultProtocol: options.defaultProtocol, + }).find((item) => item.isLink && item.value === textContent); + + if ( + !textContent || + !link || + !isValidLink(link.value) || + (shouldAutoLink !== undefined && !shouldAutoLink(link.value)) + ) { + return false; + } + + return options.editor.commands.setMark(options.type, { + href: link.href, + }); + }, + }, + }); +} diff --git a/packages/core/src/extensions/tiptap-extensions/Link/helpers/tlds.ts b/packages/core/src/extensions/tiptap-extensions/Link/helpers/tlds.ts new file mode 100644 index 0000000000..a1377f505a --- /dev/null +++ b/packages/core/src/extensions/tiptap-extensions/Link/helpers/tlds.ts @@ -0,0 +1,7 @@ +// THIS FILE IS AUTO-GENERATED. DO NOT EDIT DIRECTLY. +// Source: https://data.iana.org/TLD/tlds-alpha-by-domain.txt +// Regenerate with: pnpm --filter @blocknote/core update-tlds +// Encoding format ported from linkifyjs (MIT) — trie collapsed into ASCII. + +export const ENCODED_TLDS = + "aaa1rp3bb0ott3vie4c1le2ogado5udhabi7c0ademy5centure6ountant0s9o1tor4d0s1ult4e0g1ro2tna4f0l1rica5g0akhan5ency5i0g1rbus3force5tel5kdn3l0ibaba4pay4lfinanz6state5y2sace3tom5m0azon4ericanexpress7family11x2fam3ica3sterdam8nalytics7droid5quan4z2o0l2partments8p0le4q0uarelle8r0ab1mco4chi3my2pa2t0e3s0da2ia2sociates9t0hleta5torney7u0ction5di0ble3o3spost5thor3o0s4w0s2x0a2z0ure5ba0by2idu3namex4d1k2r0celona5laycard4s5efoot5gains6seball5ketball8uhaus5yern5b0c1t1va3cg1n2d1e0ats2uty4er2rlin4st0buy5t2f1g1h0arti5i0ble3d1ke2ng0o3o1z2j1lack0friday9ockbuster8g1omberg7ue3m0s1w2n0pparibas9o0ats3ehringer8fa2m1nd2o0k0ing5sch2tik2on4t1utique6x2r0adesco6idgestone9oadway5ker3ther5ussels7s1t1uild0ers6siness6y1zz3v1w1y1z0h3ca0b1fe2l0l1vinklein9m0era3p2non3petown5ital0one8r0avan4ds2e0er0s4s2sa1e1h1ino4t0ering5holic7ba1n1re3c1d1enter4o1rn3f0a1d2g1h0anel2nel4rity4se2t2eap3intai5ristmas6ome4urch5i0priani6rcle4sco3tadel4i0c2y3k1l0aims4eaning6ick2nic1que6othing5ud3ub0med6m1n1o0ach3des3ffee4llege4ogne5m0mbank4unity6pany2re3uter5sec4ndos3struction8ulting7tact3ractors9oking4l1p2rsica5untry4pon0s4rses6pa2r0edit0card4union9icket5own3s1uise0s6u0isinella9v1w1x1y0mru3ou3z2dad1nce3ta1e1ing3sun4y2clk3ds2e0al0er2s3gree4livery5l1oitte5ta3mocrat6ntal2ist5si0gn4v2hl2iamonds6et2gital5rect0ory7scount3ver5h2y2j1k1m1np2o0cs1tor4g1mains5t1wnload7rive4tv2ubai3pont4rban5vag2r2z2earth3t2c0o2deka3u0cation8e1g1mail3erck5nergy4gineer0ing9terprises10pson4quipment8r0icsson6ni3s0q1tate5t1u0rovision8s2vents5xchange6pert3osed4ress5traspace10fage2il1rwinds6th3mily4n0s2rm0ers5shion4t3edex3edback6rrari3ero6i0delity5o2lm2nal1nce1ial7re0stone6mdale6sh0ing5t0ness6j1k1lickr3ghts4r2orist4wers5y2m1o0o0d1tball6rd1ex2sale4um3undation8x2r0ee1senius7l1ogans4ntier7tr2ujitsu5n0d2rniture7tbol5yi3ga0l0lery3o1up4me0s3p1rden4y2b0iz3d0n2e0a1nt0ing5orge5f1g0ee3h1i0ft0s3ves2ing5l0ass3e1obal2o4m0ail3bh2o1x2n1odaddy5ld0point6f2odyear5g0le4p1t1v2p1q1r0ainger5phics5tis4een3ipe3ocery4up4s1t1u0cci3ge2ide2tars5ru3w1y2hair2mburg5ngout5us3bo2dfc0bank7ealth0care8lp1sinki6re1mes5iphop4samitsu7tachi5v2k0t2m1n1ockey4ldings5iday5medepot5goods5s0ense7nda3rse3spital5t0ing5t0els3mail5use3w2r1sbc3t1u0ghes5yatt3undai7ibm2cbc2e1u2d1e0ee3fm2kano4l1m0amat4db2mo0bilien9n0c1dustries8finiti5o2g1k1stitute6urance4e4t0ernational10uit4vestments10o1piranga7q1r0ish4s0maili5t0anbul7t0au2v3jaguar4va3cb2e0ep2tzt3welry6io2ll2m0p2nj2o0bs1urg4t1y2p0morgan6rs3uegos4niper7kaufen5ddi3e0rryhotels6properties14fh2g1h1i0a1ds2m1ndle4tchen5wi3m1n1oeln3matsu5sher5p0mg2n2r0d1ed3uokgroup8w1y0oto4z2la0caixa5mborghini8er3nd0rover6xess5salle5t0ino3robe5w0yer5b1c1ds2ease3clerc5frak4gal2o2xus4gbt3i0dl2fe0insurance9style7ghting6ke2lly3mited4o2ncoln4k2ve1ing5k1lc1p2oan0s3cker3us3l1ndon4tte1o3ve3pl0financial11r1s1t0d0a3u0ndbeck6xe1ury5v1y2ma0drid4if1son4keup4n0agement7go3p1rket0ing3s4riott5shalls7ttel5ba2c0kinsey7d1e0d0ia3et2lbourne7me1orial6n0u2rck0msd7g1h1iami3crosoft7l1ni1t2t0subishi9k1l0b1s2m0a2n1o0bi0le4da2e1i1m1nash3ey2ster5rmon3tgage6scow4to0rcycles9v0ie4p1q1r1s0d2t0n1r2u0seum3ic4v1w1x1y1z2na0b1goya4me2vy3ba2c1e0c1t0bank4flix4work5ustar5w0s2xt0direct7us4f0l2g0o2hk2i0co2ke1on3nja3ssan1y5l1o0kia3rton4w0ruz3tv4p1r0a1w2tt2u1yc2z2obi1server7ffice5kinawa6layan0group9lo3m0ega4ne1g1l0ine5oo2pen3racle3nge4g0anic5igins6saka4tsuka4t2vh3pa0ge2nasonic7ris2s1tners4s1y3y2ccw3e0t2f0izer5g1h0armacy6d1ilips5one2to0graphy6s4ysio5ics1tet2ures6d1n0g1k2oneer5zza4k1l0ace2y0station9umbing5s3m1n0c2ohl2ker3litie5rn2st3r0axi3ess3ime3o0d0uctions8f1gressive8mo2perties3y5tection8u0dential9s1t1ub2w0c2y2qa1pon3uebec3st5racing4dio4e0ad1lestate6tor2y4cipes5d0umbrella9hab3ise0n3t2liance6n0t0als5pair3ort3ublican8st0aurant8view0s5xroth6ich0ardli6oh3l1o1p2o0cks3deo3gers4om3s0vp3u0gby3hr2n2w0e2yukyu6sa0arland6fe0ty4kura4le1on3msclub4ung5ndvik0coromant12ofi4p1rl2s1ve2xo3b0i1s2c0b1haeffler7midt4olarships8ol3ule3warz5ience5ot3d1e0arch3t2cure1ity6ek2lect4ner3rvices6ven3w1x0y3fr2g1h0angrila6rp3ell3ia1ksha5oes2p0ping5uji3w3i0lk2na1gles5te3j1k0i0n2y0pe4l0ing4m0art3ile4n0cf3o0ccer3ial4ftbank4ware6hu2lar2utions7ng1y2y2pa0ce3ort2t3r0l2s1t0ada2ples4r1tebank4farm7c0group6ockholm6rage3e3ream4udio2y3yle4u0cks3pplies3y2ort5rf1gery5zuki5v1watch4iss4x1y0dney4stems6z2tab1ipei4lk2obao4rget4tamotors6r2too4x0i3c0i2d0k2eam2ch0nology8l1masek5nnis4va3f1g1h0d1eater2re6iaa2ckets5enda4ps2res2ol4j0maxx4x2k0maxx5l1m0all4n1o0day3kyo3ols3p1ray3shiba5tal3urs3wn2yota3s3r0ade1ing4ining5vel0ers0insurance16ust3v2t1ube2i1nes3shu4v0s2w1z2ua1bank3s2g1k1nicom3versity8o2ol2ps2s1y1z2va0cations7na1guard7c1e0gas3ntures6risign5sicherung10t2g1i0ajes4deo3g1king4llas4n1p1rgin4sa1ion4va1o3laanderen9n1odka3lvo3te1ing3o2yage5u2wales2mart4ter4ng0gou5tch0es6eather0channel12bcam3er2site5d0ding5ibo2r3f1hoswho6ien2ki2lliamhill9n0dows4e1ners6me2oodside6rk0s2ld3w2s1tc1f3xbox3erox4ihuan4n2xx2yz3yachts4hoo3maxun5ndex5e1odobashi7ga2kohama6u0tube6t1un3za0ppos4ra3ero3ip2m1one3uerich6w2"; diff --git a/packages/core/src/extensions/tiptap-extensions/Link/helpers/whitespace.ts b/packages/core/src/extensions/tiptap-extensions/Link/helpers/whitespace.ts new file mode 100644 index 0000000000..70d6612ae1 --- /dev/null +++ b/packages/core/src/extensions/tiptap-extensions/Link/helpers/whitespace.ts @@ -0,0 +1,13 @@ +// From DOMPurify +// https://github.com/cure53/DOMPurify/blob/main/src/regexp.ts +export const UNICODE_WHITESPACE_PATTERN = + "[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]"; + +export const UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN); +export const UNICODE_WHITESPACE_REGEX_END = new RegExp( + `${UNICODE_WHITESPACE_PATTERN}$` +); +export const UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp( + UNICODE_WHITESPACE_PATTERN, + "g" +); diff --git a/packages/core/src/extensions/tiptap-extensions/Link/index.ts b/packages/core/src/extensions/tiptap-extensions/Link/index.ts new file mode 100644 index 0000000000..bc54c007eb --- /dev/null +++ b/packages/core/src/extensions/tiptap-extensions/Link/index.ts @@ -0,0 +1,2 @@ +export { Link, LinkExtension } from "./link.js"; +export { isAllowedUri } from "./link.js"; diff --git a/packages/core/src/extensions/tiptap-extensions/Link/link.test.ts b/packages/core/src/extensions/tiptap-extensions/Link/link.test.ts new file mode 100644 index 0000000000..3af3046078 --- /dev/null +++ b/packages/core/src/extensions/tiptap-extensions/Link/link.test.ts @@ -0,0 +1,941 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { TextSelection } from "@tiptap/pm/state"; +import { Slice, Fragment } from "@tiptap/pm/model"; + +import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; +import { findLinks, tokenizeLink } from "./helpers/linkDetector.js"; +import { isAllowedUri } from "./link.js"; + +/** + * @vitest-environment jsdom + */ + +// ============================================================================ +// Helpers +// ============================================================================ + +/** Wrapper matching the old tokenize().map(t => t.toObject(defaultProtocol)) pattern */ +function tokenizeToObjects(text: string, defaultProtocol = "http") { + return tokenizeLink(text, defaultProtocol); +} + +/** + * Mirrors the isValidLinkStructure function from autolink.ts. + * A valid structure is either: + * - A single link token + * - A link token wrapped in () or [] + */ +function isValidLinkStructure( + tokens: Array<{ isLink: boolean; value: string }> +) { + if (tokens.length === 1) { + return tokens[0].isLink; + } + if (tokens.length === 3 && tokens[1].isLink) { + return ["()", "[]"].includes(tokens[0].value + tokens[2].value); + } + return false; +} + +function createEditor(links?: { isValidLink?: (href: string) => boolean }) { + const editor = BlockNoteEditor.create(links ? { links } : undefined); + const div = document.createElement("div"); + editor.mount(div); + return editor; +} + +/** + * Insert text at the end of a block, followed by a space to trigger autolink. + * Returns the link marks found in that block afterward. + */ +function typeTextThenSpace( + editor: BlockNoteEditor, + blockId: string, + text: string +) { + editor.setTextCursorPosition(blockId, "end"); + const view = editor._tiptapEditor.view; + const { from } = view.state.selection; + + // Insert the text + view.dispatch(view.state.tr.insertText(text, from)); + + // Now insert a space to trigger autolink + const afterInsert = view.state.selection.from; + view.dispatch(view.state.tr.insertText(" ", afterInsert)); + + return getLinksInDocument(editor); +} + +/** + * Walk the ProseMirror doc and collect all link marks with their text and href. + */ +function getLinksInDocument(editor: BlockNoteEditor) { + const links: Array<{ text: string; href: string; from: number; to: number }> = + []; + const doc = editor._tiptapEditor.state.doc; + const linkType = editor._tiptapEditor.schema.marks.link; + + doc.descendants((node, pos) => { + if (node.isText && node.marks.length > 0) { + const linkMark = node.marks.find((m) => m.type === linkType); + if (linkMark) { + links.push({ + text: node.text || "", + href: linkMark.attrs.href, + from: pos, + to: pos + node.nodeSize, + }); + } + } + }); + return links; +} + +// ============================================================================ +// Level 1: Unit tests for findLinks() and tokenizeLink() +// ============================================================================ + +describe("findLinks() baseline behavior", () => { + describe("basic URL detection", () => { + it("detects https URLs", () => { + const results = findLinks("https://example.com"); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + isLink: true, + value: "https://example.com", + href: "https://example.com", + start: 0, + end: 19, + }); + }); + + it("detects http URLs", () => { + const results = findLinks("http://example.com"); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + isLink: true, + value: "http://example.com", + href: "http://example.com", + }); + }); + + it("detects schemeless URLs and prepends default protocol", () => { + const results = findLinks("example.com"); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + isLink: true, + value: "example.com", + href: "http://example.com", + start: 0, + end: 11, + }); + }); + + it("respects defaultProtocol option", () => { + const results = findLinks("example.com", { defaultProtocol: "https" }); + expect(results).toHaveLength(1); + expect(results[0].href).toBe("https://example.com"); + }); + + it("detects www URLs", () => { + const results = findLinks("www.example.com"); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + isLink: true, + value: "www.example.com", + href: "http://www.example.com", + }); + }); + }); + + describe("multiple URLs in text", () => { + it("finds multiple URLs with correct positions", () => { + const results = findLinks("Visit https://a.com and https://b.com"); + expect(results).toHaveLength(2); + expect(results[0]).toMatchObject({ + value: "https://a.com", + start: 6, + end: 19, + }); + expect(results[1]).toMatchObject({ + value: "https://b.com", + start: 24, + end: 37, + }); + }); + + it("finds multiple schemeless URLs", () => { + const results = findLinks("Check example.com or test.org"); + expect(results).toHaveLength(2); + expect(results[0].value).toBe("example.com"); + expect(results[1].value).toBe("test.org"); + }); + }); + + describe("URLs with paths, queries, and fragments", () => { + it("includes full path", () => { + const results = findLinks("https://example.com/path/to/page"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("https://example.com/path/to/page"); + }); + + it("includes query string", () => { + const results = findLinks("https://example.com?q=hello&b=world"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("https://example.com?q=hello&b=world"); + }); + + it("includes fragment", () => { + const results = findLinks("https://example.com#section"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("https://example.com#section"); + }); + + it("includes path + query + fragment", () => { + const results = findLinks("https://example.com/path?q=1#frag"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("https://example.com/path?q=1#frag"); + }); + + it("includes encoded characters", () => { + const results = findLinks("https://example.com/path%20name"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("https://example.com/path%20name"); + }); + + it("includes trailing slash", () => { + const results = findLinks("https://example.com/"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("https://example.com/"); + }); + }); + + describe("URLs with ports", () => { + it("detects URL with port", () => { + const results = findLinks("https://example.com:8080"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("https://example.com:8080"); + }); + + it("detects schemeless URL with port and path", () => { + const results = findLinks("example.com:3000/path"); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + value: "example.com:3000/path", + href: "http://example.com:3000/path", + }); + }); + }); + + describe("trailing punctuation handling", () => { + it("excludes trailing period", () => { + const results = findLinks("Visit https://example.com."); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("https://example.com"); + }); + + it("excludes trailing comma", () => { + const results = findLinks("See https://example.com, and more"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("https://example.com"); + }); + + it("excludes surrounding parentheses", () => { + const results = findLinks("(https://example.com)"); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + value: "https://example.com", + start: 1, + end: 20, + }); + }); + + it("keeps balanced parentheses in path (Wikipedia-style)", () => { + const results = findLinks( + "https://en.wikipedia.org/wiki/Foo_(bar)" + ); + expect(results).toHaveLength(1); + expect(results[0].value).toBe( + "https://en.wikipedia.org/wiki/Foo_(bar)" + ); + }); + }); + + describe("non-links", () => { + it("returns empty for plain text", () => { + expect(findLinks("not a link")).toHaveLength(0); + }); + + it("returns empty for single word", () => { + expect(findLinks("hello")).toHaveLength(0); + }); + + it("returns empty for empty string", () => { + expect(findLinks("")).toHaveLength(0); + }); + + it("returns empty for just a protocol", () => { + expect(findLinks("https://")).toHaveLength(0); + }); + + it("does not detect bare IP addresses", () => { + expect(findLinks("192.168.1.1")).toHaveLength(0); + }); + }); + + describe("domain variations", () => { + it("detects hyphenated domains", () => { + const results = findLinks("my-site.example.com"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("my-site.example.com"); + }); + + it("detects subdomains", () => { + const results = findLinks("sub.domain.example.com"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("sub.domain.example.com"); + }); + }); + + describe("URL position in text", () => { + it("detects URL at end of text", () => { + const results = findLinks("go to example.com"); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + value: "example.com", + start: 6, + end: 17, + }); + }); + + it("detects URL at start of text", () => { + const results = findLinks("example.com is great"); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + value: "example.com", + start: 0, + end: 11, + }); + }); + }); + + describe("protocol variations", () => { + it("detects ftp URLs", () => { + const results = findLinks("ftp://files.example.com"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("ftp://files.example.com"); + }); + + it("detects mailto URLs", () => { + const results = findLinks("mailto:user@example.com"); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + value: "mailto:user@example.com", + href: "mailto:user@example.com", + }); + }); + + it("detects bare email addresses as links", () => { + const results = findLinks("user@example.com"); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + isLink: true, + value: "user@example.com", + href: "mailto:user@example.com", + }); + }); + }); + + describe("boundary handling", () => { + it("stops at whitespace", () => { + const results = findLinks("https://example.com/path with spaces"); + expect(results).toHaveLength(1); + expect(results[0].value).toBe("https://example.com/path"); + }); + }); +}); + +describe("tokenizeLink() baseline behavior", () => { + describe("single valid links", () => { + it("tokenizes schemeless URL as single link token", () => { + const tokens = tokenizeToObjects("example.com"); + expect(tokens).toHaveLength(1); + expect(tokens[0]).toMatchObject({ + isLink: true, + value: "example.com", + href: "http://example.com", + start: 0, + end: 11, + }); + }); + + it("tokenizes https URL as single link token", () => { + const tokens = tokenizeToObjects("https://example.com"); + expect(tokens).toHaveLength(1); + expect(tokens[0]).toMatchObject({ + isLink: true, + value: "https://example.com", + href: "https://example.com", + }); + }); + + it("tokenizes URL with path as single link token", () => { + const tokens = tokenizeToObjects("example.com/path"); + expect(tokens).toHaveLength(1); + expect(tokens[0]).toMatchObject({ + isLink: true, + value: "example.com/path", + href: "http://example.com/path", + }); + }); + + it("tokenizes www URL as single link token", () => { + const tokens = tokenizeToObjects("www.example.com"); + expect(tokens).toHaveLength(1); + expect(tokens[0]).toMatchObject({ + isLink: true, + value: "www.example.com", + href: "http://www.example.com", + }); + }); + + it("tokenizes URL with https and path as single link token", () => { + const tokens = tokenizeToObjects("https://example.com/path"); + expect(tokens).toHaveLength(1); + expect(tokens[0]).toMatchObject({ + isLink: true, + value: "https://example.com/path", + href: "https://example.com/path", + }); + }); + + it("tokenizes short TLD (2 chars) as link", () => { + const tokens = tokenizeToObjects("test.co"); + expect(tokens).toHaveLength(1); + expect(tokens[0].isLink).toBe(true); + }); + }); + + describe("bracket-wrapped links", () => { + it("tokenizes (url) as 3 tokens with link in middle", () => { + const tokens = tokenizeToObjects("(example.com)"); + expect(tokens).toHaveLength(3); + expect(tokens[0]).toMatchObject({ + isLink: false, + value: "(", + start: 0, + end: 1, + }); + expect(tokens[1]).toMatchObject({ + isLink: true, + value: "example.com", + href: "http://example.com", + start: 1, + end: 12, + }); + expect(tokens[2]).toMatchObject({ + isLink: false, + value: ")", + start: 12, + end: 13, + }); + }); + + it("tokenizes [url] as 3 tokens with link in middle", () => { + const tokens = tokenizeToObjects("[example.com]"); + expect(tokens).toHaveLength(3); + expect(tokens[0]).toMatchObject({ isLink: false, value: "[" }); + expect(tokens[1]).toMatchObject({ + isLink: true, + value: "example.com", + }); + expect(tokens[2]).toMatchObject({ isLink: false, value: "]" }); + }); + + it("tokenizes (https://url) as 3 tokens", () => { + const tokens = tokenizeToObjects("(https://example.com)"); + expect(tokens).toHaveLength(3); + expect(tokens[0]).toMatchObject({ isLink: false, value: "(" }); + expect(tokens[1]).toMatchObject({ + isLink: true, + value: "https://example.com", + href: "https://example.com", + }); + expect(tokens[2]).toMatchObject({ isLink: false, value: ")" }); + }); + }); + + describe("non-links", () => { + it("tokenizes plain word as non-link", () => { + const tokens = tokenizeToObjects("notaurl"); + expect(tokens).toHaveLength(1); + expect(tokens[0].isLink).toBe(false); + }); + + it("tokenizes domain with trailing number as non-link", () => { + // This is a key behavior: example.com1 is NOT a valid link + // because the TLD is "com1" which is not valid + const tokens = tokenizeToObjects("example.com1"); + expect(tokens).toHaveLength(1); + expect(tokens[0].isLink).toBe(false); + }); + + it("tokenizes single-char TLD as non-link", () => { + const tokens = tokenizeToObjects("test.x"); + expect(tokens).toHaveLength(1); + expect(tokens[0].isLink).toBe(false); + }); + + it("tokenizes single-char hostname as non-link", () => { + const tokens = tokenizeToObjects("a.bc"); + expect(tokens).toHaveLength(1); + expect(tokens[0].isLink).toBe(false); + }); + }); + + describe("edge cases", () => { + it("tokenizes IP address as non-link", () => { + const tokens = tokenizeToObjects("192.168.1.1"); + expect(tokens).toHaveLength(1); + expect(tokens[0].isLink).toBe(false); + }); + + it("tokenizes localhost as link (filtered downstream by shouldAutoLink)", () => { + const tokens = tokenizeToObjects("localhost"); + expect(tokens).toHaveLength(1); + expect(tokens[0].isLink).toBe(true); + }); + + it("tokenizes url with trailing dot as url + dot tokens", () => { + const tokens = tokenizeToObjects("example.com."); + expect(tokens).toHaveLength(2); + expect(tokens[0]).toMatchObject({ + isLink: true, + value: "example.com", + }); + expect(tokens[1]).toMatchObject({ + isLink: false, + value: ".", + }); + }); + + it("tokenizes {url} as 3 tokens (curly braces)", () => { + const tokens = tokenizeToObjects("{example.com}"); + expect(tokens).toHaveLength(3); + expect(tokens[0]).toMatchObject({ isLink: false, value: "{" }); + expect(tokens[1]).toMatchObject({ isLink: true, value: "example.com" }); + expect(tokens[2]).toMatchObject({ isLink: false, value: "}" }); + }); + + it("respects defaultProtocol parameter", () => { + const tokens = tokenizeToObjects("example.com", "https"); + expect(tokens[0].href).toBe("https://example.com"); + }); + }); +}); + +describe("isValidLinkStructure baseline", () => { + it("accepts single link token", () => { + const tokens = tokenizeToObjects("example.com"); + expect(isValidLinkStructure(tokens)).toBe(true); + }); + + it("accepts link wrapped in parentheses", () => { + const tokens = tokenizeToObjects("(example.com)"); + expect(isValidLinkStructure(tokens)).toBe(true); + }); + + it("accepts link wrapped in square brackets", () => { + const tokens = tokenizeToObjects("[example.com]"); + expect(isValidLinkStructure(tokens)).toBe(true); + }); + + it("rejects link wrapped in curly braces", () => { + // {url} tokenizes to 3 tokens but {} is not in the accepted list + const tokens = tokenizeToObjects("{example.com}"); + expect(isValidLinkStructure(tokens)).toBe(false); + }); + + it("rejects non-link single token", () => { + const tokens = tokenizeToObjects("notaurl"); + expect(isValidLinkStructure(tokens)).toBe(false); + }); + + it("rejects url with trailing dot (2 tokens)", () => { + const tokens = tokenizeToObjects("example.com."); + expect(isValidLinkStructure(tokens)).toBe(false); + }); + + it("rejects example.com1 (invalid TLD)", () => { + const tokens = tokenizeToObjects("example.com1"); + expect(isValidLinkStructure(tokens)).toBe(false); + }); +}); + +// ============================================================================ +// Level 2: Integration tests through the editor +// ============================================================================ + +describe("Link extension autolink behavior", () => { + let editor: BlockNoteEditor; + + afterEach(() => { + if (editor) { + editor._tiptapEditor.destroy(); + } + }); + + function setupEditorWithBlock(content = "") { + editor = createEditor(); + editor.replaceBlocks(editor.document, [ + { + id: "test-block", + type: "paragraph", + content: content || undefined, + }, + ]); + return editor; + } + + describe("should autolink", () => { + it("autolinks https URL when followed by space", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace(editor, "test-block", "https://example.com"); + expect(links).toHaveLength(1); + expect(links[0].href).toBe("https://example.com"); + expect(links[0].text).toBe("https://example.com"); + }); + + it("autolinks http URL when followed by space", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace(editor, "test-block", "http://example.com"); + expect(links).toHaveLength(1); + expect(links[0].href).toBe("http://example.com"); + }); + + it("autolinks schemeless URL with default protocol (https in BlockNote)", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace(editor, "test-block", "example.com"); + expect(links).toHaveLength(1); + // BlockNote overrides the tiptap default to "https" via DEFAULT_LINK_PROTOCOL + expect(links[0].href).toBe("https://example.com"); + expect(links[0].text).toBe("example.com"); + }); + + it("autolinks www URL", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace(editor, "test-block", "www.example.com"); + expect(links).toHaveLength(1); + expect(links[0].href).toBe("https://www.example.com"); + }); + + it("autolinks URL with path and query", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace( + editor, + "test-block", + "https://example.com/path?q=1" + ); + expect(links).toHaveLength(1); + expect(links[0].href).toBe("https://example.com/path?q=1"); + }); + }); + + describe("should NOT autolink", () => { + it("does not autolink plain text", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace(editor, "test-block", "notaurl"); + expect(links).toHaveLength(0); + }); + + it("does not autolink single word", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace(editor, "test-block", "hello"); + expect(links).toHaveLength(0); + }); + + it("does not autolink IP address without protocol", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace(editor, "test-block", "192.168.1.1"); + expect(links).toHaveLength(0); + }); + + it("does not autolink localhost (single-word hostname)", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace(editor, "test-block", "localhost"); + expect(links).toHaveLength(0); + }); + + it("does not autolink domain with trailing number (invalid TLD)", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace(editor, "test-block", "example.com1"); + expect(links).toHaveLength(0); + }); + }); + + describe("bracket-wrapped URLs", () => { + it("autolinks URL in parentheses, linking only the URL", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace( + editor, + "test-block", + "(https://example.com)" + ); + expect(links).toHaveLength(1); + expect(links[0].href).toBe("https://example.com"); + expect(links[0].text).toBe("https://example.com"); + }); + + it("autolinks URL in square brackets, linking only the URL", () => { + setupEditorWithBlock(); + const links = typeTextThenSpace( + editor, + "test-block", + "[https://example.com]" + ); + expect(links).toHaveLength(1); + expect(links[0].href).toBe("https://example.com"); + expect(links[0].text).toBe("https://example.com"); + }); + }); +}); + +describe("Link extension paste handler behavior", () => { + let editor: BlockNoteEditor; + + afterEach(() => { + if (editor) { + editor._tiptapEditor.destroy(); + } + }); + + it("applies link mark when pasting URL over selected text", () => { + editor = createEditor(); + editor.replaceBlocks(editor.document, [ + { + id: "test-block", + type: "paragraph", + content: "click here", + }, + ]); + + // Select "click here" + editor.setTextCursorPosition("test-block", "start"); + const view = editor._tiptapEditor.view; + const doc = view.state.doc; + + // Find the text node position + let textStart = 0; + let textEnd = 0; + doc.descendants((node, pos) => { + if (node.isText && node.text === "click here") { + textStart = pos; + textEnd = pos + node.nodeSize; + } + }); + + // Create selection over the text + const tr = view.state.tr.setSelection( + TextSelection.create(view.state.doc, textStart, textEnd) + ); + view.dispatch(tr); + + // Create a minimal slice that looks like pasted URL text + const textNode = view.state.schema.text("https://example.com"); + const slice = new Slice(Fragment.from(textNode), 0, 0); + + // Dispatch paste through the editor view + const handled = view.someProp("handlePaste", (f) => + f(view, new ClipboardEvent("paste"), slice) + ); + + expect(handled).toBeTruthy(); + // Check that link mark was applied + const links = getLinksInDocument(editor); + expect(links).toHaveLength(1); + expect(links[0].href).toBe("https://example.com"); + expect(links[0].text).toBe("click here"); + }); + + it("does not apply link when pasting non-URL text over selection", () => { + editor = createEditor(); + editor.replaceBlocks(editor.document, [ + { + id: "test-block", + type: "paragraph", + content: "click here", + }, + ]); + + editor.setTextCursorPosition("test-block", "start"); + const view = editor._tiptapEditor.view; + const doc = view.state.doc; + + let textStart = 0; + let textEnd = 0; + doc.descendants((node, pos) => { + if (node.isText && node.text === "click here") { + textStart = pos; + textEnd = pos + node.nodeSize; + } + }); + + const tr = view.state.tr.setSelection( + TextSelection.create(view.state.doc, textStart, textEnd) + ); + view.dispatch(tr); + + const textNode = view.state.schema.text("not a url"); + const slice = new Slice(Fragment.from(textNode), 0, 0); + + const handled = view.someProp("handlePaste", (f) => + f(view, new ClipboardEvent("paste"), slice) + ); + + // Should not be handled (not a URL) + expect(handled).toBeFalsy(); + + // No links should exist + const links = getLinksInDocument(editor); + expect(links).toHaveLength(0); + }); + + it("does not apply link when pasting URL with empty selection", () => { + editor = createEditor(); + editor.replaceBlocks(editor.document, [ + { + id: "test-block", + type: "paragraph", + content: "some text", + }, + ]); + + // Place cursor without selection + editor.setTextCursorPosition("test-block", "end"); + const view = editor._tiptapEditor.view; + + const textNode = view.state.schema.text("https://example.com"); + const slice = new Slice(Fragment.from(textNode), 0, 0); + + const handled = view.someProp("handlePaste", (f) => + f(view, new ClipboardEvent("paste"), slice) + ); + + // Should not be handled because selection is empty + expect(handled).toBeFalsy(); + }); +}); + +describe("Link extension isValidLink option", () => { + let editor: BlockNoteEditor; + + afterEach(() => { + if (editor) { + editor._tiptapEditor.destroy(); + } + }); + + it("autolink: restrictive override blocks normally-valid URLs on typing", () => { + editor = createEditor({ isValidLink: () => false }); + editor.replaceBlocks(editor.document, [ + { + id: "test-block", + type: "paragraph", + content: "", + }, + ]); + + const links = typeTextThenSpace( + editor, + "test-block", + "https://example.com" + ); + expect(links).toHaveLength(0); + }); + + it("paste-rule: restrictive override blocks pasted URL text", () => { + editor = createEditor({ isValidLink: () => false }); + editor.replaceBlocks(editor.document, [ + { + id: "test-block", + type: "paragraph", + content: "some text here", + }, + ]); + + editor._tiptapEditor.commands.insertContent("https://example.com "); + + const links = getLinksInDocument(editor); + expect(links).toHaveLength(0); + }); + + it("paste-handler: restrictive override blocks URL pasted over selection", () => { + editor = createEditor({ isValidLink: () => false }); + editor.replaceBlocks(editor.document, [ + { + id: "test-block", + type: "paragraph", + content: "click here", + }, + ]); + + editor.setTextCursorPosition("test-block", "start"); + const view = editor._tiptapEditor.view; + const doc = view.state.doc; + + let textStart = 0; + let textEnd = 0; + doc.descendants((node, pos) => { + if (node.isText && node.text === "click here") { + textStart = pos; + textEnd = pos + node.nodeSize; + } + }); + + const tr = view.state.tr.setSelection( + TextSelection.create(view.state.doc, textStart, textEnd) + ); + view.dispatch(tr); + + const textNode = view.state.schema.text("https://example.com"); + const slice = new Slice(Fragment.from(textNode), 0, 0); + + const handled = view.someProp("handlePaste", (f) => + f(view, new ClipboardEvent("paste"), slice) + ); + + expect(handled).toBeFalsy(); + const links = getLinksInDocument(editor); + expect(links).toHaveLength(0); + }); + + it("parseHTML: permissive override accepts custom-scheme links", () => { + editor = createEditor({ + isValidLink: (href) => isAllowedUri(href) || href.startsWith("myapp:"), + }); + editor.pasteHTML(`

        click

        `); + + const links = getLinksInDocument(editor); + expect(links).toHaveLength(1); + expect(links[0].href).toBe("myapp://foo"); + }); + + it("parseHTML: default rejects unknown-scheme links", () => { + editor = createEditor(); + editor.pasteHTML(`

        click

        `); + + const links = getLinksInDocument(editor); + expect(links).toHaveLength(0); + }); + + it("renderHTML: permissive override preserves custom-scheme href on export", () => { + editor = createEditor({ + isValidLink: (href) => isAllowedUri(href) || href.startsWith("myapp:"), + }); + editor.pasteHTML(`

        click

        `); + + const html = editor.blocksToFullHTML(editor.document); + expect(html).toContain('href="myapp://foo"'); + }); +}); diff --git a/packages/core/src/extensions/tiptap-extensions/Link/link.ts b/packages/core/src/extensions/tiptap-extensions/Link/link.ts new file mode 100644 index 0000000000..1769dfa544 --- /dev/null +++ b/packages/core/src/extensions/tiptap-extensions/Link/link.ts @@ -0,0 +1,238 @@ +import type { PasteRuleMatch } from "@tiptap/core"; +import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core"; +import type { Plugin } from "@tiptap/pm/state"; +import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; +import { createExtension } from "../../../editor/BlockNoteExtension.js"; +import { autolink } from "./helpers/autolink.js"; +import { findLinks } from "./helpers/linkDetector.js"; +import { clickHandler } from "./helpers/clickHandler.js"; +import { pasteHandler } from "./helpers/pasteHandler.js"; +import { UNICODE_WHITESPACE_REGEX_GLOBAL } from "./helpers/whitespace.js"; + +const DEFAULT_PROTOCOL = "https"; + +// Pre-compiled regex for URI protocol validation. +// Allows: http, https, ftp, ftps, mailto, tel, callto, sms, cid, xmpp +const ALLOWED_URI_REGEX = + // eslint-disable-next-line no-useless-escape + /^(?:(?:http|https|ftp|ftps|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z0-9+.\-]+(?:[^a-z+.\-:]|$))/i; + +export function isAllowedUri(uri: string | undefined): boolean { + if (!uri) { + return true; + } + const cleaned = uri.replace(UNICODE_WHITESPACE_REGEX_GLOBAL, ""); + return ALLOWED_URI_REGEX.test(cleaned); +} + +/** + * Determine whether a detected URL should be auto-linked. + * URLs with explicit protocols are always auto-linked. + * Bare hostnames must have a TLD (no IP addresses or single words). + */ +function shouldAutoLink(url: string): boolean { + const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(url); + const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url); + + if (hasProtocol || (hasMaybeProtocol && !url.includes("@"))) { + return true; + } + // Strip userinfo (user:pass@) if present, then extract hostname + const urlWithoutUserinfo = url.includes("@") ? url.split("@").pop()! : url; + const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0]; + + // Don't auto-link IP addresses without protocol + if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) { + return false; + } + // Don't auto-link single-word hostnames without TLD (e.g., "localhost") + if (!/\./.test(hostname)) { + return false; + } + return true; +} + +export type LinkOptions = { + HTMLAttributes: Record; + editor?: BlockNoteEditor; + onClick?: ( + event: MouseEvent, + editor: BlockNoteEditor, + ) => boolean | void; + isValidLink: (href: string) => boolean; +}; + +/** + * BlockNote Link mark extension. + */ +export const Link = Mark.create({ + name: "link", + + keepOnSplit: false, + + exitable: true, + + inclusive: false, + + addOptions() { + return { + HTMLAttributes: { + target: "_blank", + rel: "noopener noreferrer nofollow", + className: "bn-inline-content-section", + "data-inline-content-type": "link", + }, + editor: undefined, + onClick: undefined, + isValidLink: isAllowedUri, + }; + }, + + addAttributes() { + return { + href: { + default: null, + parseHTML(element) { + return element.getAttribute("href"); + }, + }, + }; + }, + + parseHTML() { + const isValidLink = this.options.isValidLink; + return [ + { + tag: "a[href]", + getAttrs: (dom) => { + const href = (dom as HTMLElement).getAttribute("href"); + if (!href || !isValidLink(href)) { + return false; + } + return null; + }, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + if (!this.options.isValidLink(HTMLAttributes.href)) { + return [ + "a", + mergeAttributes( + { + ...HTMLAttributes, + href: "", + }, + this.options.HTMLAttributes, + ), + 0, + ]; + } + + return [ + "a", + mergeAttributes(HTMLAttributes, this.options.HTMLAttributes), + 0, + ]; + }, + + addPasteRules() { + const isValidLink = this.options.isValidLink; + return [ + markPasteRule({ + find: (text) => { + const foundLinks: PasteRuleMatch[] = []; + + if (text) { + const links = findLinks(text, { + defaultProtocol: DEFAULT_PROTOCOL, + }).filter((item) => item.isLink && isValidLink(item.value)); + + for (const link of links) { + if (!shouldAutoLink(link.value)) { + continue; + } + + foundLinks.push({ + text: link.value, + data: { href: link.href }, + index: link.start, + }); + } + } + + return foundLinks; + }, + type: this.type, + getAttributes: (match) => ({ + href: match.data?.href, + }), + }), + ]; + }, + + addProseMirrorPlugins() { + const plugins: Plugin[] = []; + + plugins.push( + autolink({ + type: this.type, + defaultProtocol: DEFAULT_PROTOCOL, + validate: this.options.isValidLink, + shouldAutoLink, + }), + ); + + plugins.push( + clickHandler({ + type: this.type, + tiptapEditor: this.editor, + editor: this.options.editor, + onClick: this.options.onClick, + }), + ); + + plugins.push( + pasteHandler({ + editor: this.editor, + defaultProtocol: DEFAULT_PROTOCOL, + type: this.type, + shouldAutoLink, + isValidLink: this.options.isValidLink, + }), + ); + + return plugins; + }, +}); + +type LinkExtensionOptions = { + HTMLAttributes?: Record; + onClick?: ( + event: MouseEvent, + editor: BlockNoteEditor, + ) => boolean | void; + isValidLink?: (href: string) => boolean; +}; + +/** + * BlockNote extension wrapping the {@link Link} TipTap mark. Wrapping the mark + * lets other extensions order their click handlers relative to the link click + * handler via `runsBefore: ["link"]`. + */ +export const LinkExtension = createExtension( + ({ editor, options }) => { + return { + key: "link", + tiptapExtensions: [ + Link.configure({ + HTMLAttributes: options.HTMLAttributes ?? {}, + editor, + onClick: options.onClick, + ...(options.isValidLink ? { isValidLink: options.isValidLink } : {}), + }), + ], + } as const; + }, +); diff --git a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts index 2f19981f89..a3ce6f3828 100644 --- a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts +++ b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts @@ -6,7 +6,7 @@ import { } from "@tiptap/core"; import { Fragment, Slice } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; -import { v4 } from "uuid"; +import { uuidv4 } from "lib0/random"; /** * Code from Tiptap UniqueID extension (https://tiptap.dev/api/extensions/unique-id) @@ -51,9 +51,7 @@ const UniqueID = Extension.create({ attributeName: "id", types: [], setIdAttribute: false, - isWithinEditor: undefined as - | ((element: Element) => boolean) - | undefined, + isWithinEditor: undefined as ((element: Element) => boolean) | undefined, generateID: () => { // Use mock ID if tests are running. if (typeof window !== "undefined" && (window as any).__TEST_OPTIONS) { @@ -67,7 +65,7 @@ const UniqueID = Extension.create({ return testOptions.mockID.toString() as string; } - return v4(); + return uuidv4(); }, filterTransaction: null, }; diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts index 00574debed..37abc3e30b 100644 --- a/packages/core/src/i18n/locales/ar.ts +++ b/packages/core/src/i18n/locales/ar.ts @@ -214,7 +214,7 @@ export const ar: Dictionary = { text_title: "نص", background_title: "خلفية", colors: { - default: "افتراضي", + default: "تلقائي", gray: "رمادي", brown: "بني", red: "أحمر", @@ -369,9 +369,11 @@ export const ar: Dictionary = { edited: "تم التحرير", save_button_text: "حفظ", cancel_button_text: "إلغاء", + deleted_reference_text: "تم حذف المحتوى الأصلي", actions: { add_reaction: "أضف تفاعلًا", resolve: "حل", + reopen: "إعادة فتح", edit_comment: "تحرير التعليق", delete_comment: "حذف التعليق", more_actions: "المزيد من الإجراءات", diff --git a/packages/core/src/i18n/locales/de.ts b/packages/core/src/i18n/locales/de.ts index cac48dd80a..40944212b3 100644 --- a/packages/core/src/i18n/locales/de.ts +++ b/packages/core/src/i18n/locales/de.ts @@ -249,7 +249,7 @@ export const de: Dictionary = { text_title: "Text", background_title: "Hintergrund", colors: { - default: "Standard", + default: "Automatisch", gray: "Grau", brown: "Braun", red: "Rot", @@ -403,9 +403,11 @@ export const de: Dictionary = { edited: "bearbeitet", save_button_text: "Speichern", cancel_button_text: "Abbrechen", + deleted_reference_text: "Originalinhalt gelöscht", actions: { add_reaction: "Reaktion hinzufügen", resolve: "Lösen", + reopen: "Wieder öffnen", edit_comment: "Kommentar bearbeiten", delete_comment: "Kommentar löschen", more_actions: "Weitere Aktionen", diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index af7927d207..784d130094 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -229,7 +229,7 @@ export const en = { text_title: "Text", background_title: "Background", colors: { - default: "Default", + default: "Auto", gray: "Gray", brown: "Brown", red: "Red", @@ -384,9 +384,11 @@ export const en = { edited: "edited", save_button_text: "Save", cancel_button_text: "Cancel", + deleted_reference_text: "Original content deleted", actions: { add_reaction: "Add reaction", resolve: "Resolve", + reopen: "Re-open", edit_comment: "Edit comment", delete_comment: "Delete comment", more_actions: "More actions", diff --git a/packages/core/src/i18n/locales/es.ts b/packages/core/src/i18n/locales/es.ts index 9e830b406b..4757d9784f 100644 --- a/packages/core/src/i18n/locales/es.ts +++ b/packages/core/src/i18n/locales/es.ts @@ -228,7 +228,7 @@ export const es: Dictionary = { text_title: "Texto", background_title: "Fondo", colors: { - default: "Por defecto", + default: "Automático", gray: "Gris", brown: "Marrón", red: "Rojo", @@ -382,9 +382,11 @@ export const es: Dictionary = { edited: "editado", save_button_text: "Guardar", cancel_button_text: "Cancelar", + deleted_reference_text: "Contenido original eliminado", actions: { add_reaction: "Agregar reacción", resolve: "Resolver", + reopen: "Reabrir", edit_comment: "Editar comentario", delete_comment: "Eliminar comentario", more_actions: "Más acciones", diff --git a/packages/core/src/i18n/locales/fa.ts b/packages/core/src/i18n/locales/fa.ts index f72270e04e..dff80beb81 100644 --- a/packages/core/src/i18n/locales/fa.ts +++ b/packages/core/src/i18n/locales/fa.ts @@ -197,7 +197,7 @@ export const fa = { text_title: "متن", background_title: "پس‌زمینه", colors: { - default: "پیش‌فرض", + default: "خودکار", gray: "خاکستری", brown: "قهوه‌ای", red: "قرمز", @@ -352,9 +352,11 @@ export const fa = { edited: "ویرایش شده", save_button_text: "ذخیره", cancel_button_text: "لغو", + deleted_reference_text: "محتوای اصلی حذف شد", actions: { add_reaction: "افزودن واکنش", resolve: "حل کردن", + reopen: "باز کردن مجدد", edit_comment: "ویرایش دیدگاه", delete_comment: "حذف دیدگاه", more_actions: "اقدامات بیشتر", diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index b56e6942f6..b05d346409 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -244,7 +244,7 @@ export const fr: Dictionary = { } as Record, }, toggle_blocks: { - add_block_button: "Toggle vide. Cliquez pour ajouter un bloc.", + add_block_button: "Liste repliable vide. Cliquez pour ajouter un bloc.", }, // from react package: side_menu: { @@ -275,7 +275,7 @@ export const fr: Dictionary = { text_title: "Texte", background_title: "Fond", colors: { - default: "Défaut", + default: "Auto", gray: "Gris", brown: "Marron", red: "Rouge", @@ -430,9 +430,11 @@ export const fr: Dictionary = { edited: "modifié", save_button_text: "Enregistrer", cancel_button_text: "Annuler", + deleted_reference_text: "Contenu d'origine supprimé", actions: { add_reaction: "Ajouter une réaction", resolve: "Résoudre", + reopen: "Rouvrir", edit_comment: "Modifier le commentaire", delete_comment: "Supprimer le commentaire", more_actions: "Plus d'actions", diff --git a/packages/core/src/i18n/locales/he.ts b/packages/core/src/i18n/locales/he.ts index 553fc42941..59cdc56414 100644 --- a/packages/core/src/i18n/locales/he.ts +++ b/packages/core/src/i18n/locales/he.ts @@ -230,7 +230,7 @@ export const he: Dictionary = { text_title: "טקסט", background_title: "רקע", colors: { - default: "ברירת מחדל", + default: "אוטומטי", gray: "אפור", brown: "חום", red: "אדום", @@ -384,9 +384,11 @@ export const he: Dictionary = { edited: "נערך", save_button_text: "שמור", cancel_button_text: "בטל", + deleted_reference_text: "התוכן המקורי נמחק", actions: { add_reaction: "הוסף תגובה", resolve: "סמן כפתור", + reopen: "פתח מחדש", edit_comment: "ערוך תגובה", delete_comment: "מחק תגובה", more_actions: "פעולות נוספות", diff --git a/packages/core/src/i18n/locales/hr.ts b/packages/core/src/i18n/locales/hr.ts index 31c0b71159..c2081599cc 100644 --- a/packages/core/src/i18n/locales/hr.ts +++ b/packages/core/src/i18n/locales/hr.ts @@ -242,7 +242,7 @@ export const hr: Dictionary = { text_title: "Tekst", background_title: "Pozadina", colors: { - default: "Zadano", + default: "Automatski", gray: "Siva", brown: "Smeđa", red: "Crvena", @@ -397,9 +397,11 @@ export const hr: Dictionary = { edited: "uredio", save_button_text: "Spremi", cancel_button_text: "Odustani", + deleted_reference_text: "Originalni sadržaj je obrisan", actions: { add_reaction: "Dodaj reakciju", resolve: "Riješi", + reopen: "Ponovno otvori", edit_comment: "Uredi komentar", delete_comment: "Obriši komentar", more_actions: "Više radnji", diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index 25060d651f..fcde471e56 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -242,7 +242,7 @@ export const is: Dictionary = { text_title: "Texti", background_title: "Bakgrunnur", colors: { - default: "Sjálfgefið", + default: "Sjálfvirkt", gray: "Grár", brown: "Brúnn", red: "Rauður", @@ -397,9 +397,11 @@ export const is: Dictionary = { edited: "breytt", save_button_text: "Vista", cancel_button_text: "Hætta", + deleted_reference_text: "Upprunalegu efni eytt", actions: { add_reaction: "Bæta við viðbrögðum", resolve: "Leysa", + reopen: "Opna aftur", edit_comment: "Breyta athugasemd", delete_comment: "Eyða athugasemd", more_actions: "Fleiri aðgerðir", diff --git a/packages/core/src/i18n/locales/it.ts b/packages/core/src/i18n/locales/it.ts index 45d9dcd277..4053581107 100644 --- a/packages/core/src/i18n/locales/it.ts +++ b/packages/core/src/i18n/locales/it.ts @@ -251,7 +251,7 @@ export const it: Dictionary = { text_title: "Testo", background_title: "Sfondo", colors: { - default: "Predefinito", + default: "Automatico", gray: "Grigio", brown: "Marrone", red: "Rosso", @@ -406,9 +406,11 @@ export const it: Dictionary = { edited: "modificato", save_button_text: "Salva", cancel_button_text: "Annulla", + deleted_reference_text: "Contenuto originale eliminato", actions: { add_reaction: "Aggiungi reazione", resolve: "Risolvi", + reopen: "Riapri", edit_comment: "Modifica commento", delete_comment: "Elimina commento", more_actions: "Altre azioni", diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index 236b834443..ce5ba87a77 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -269,7 +269,7 @@ export const ja: Dictionary = { text_title: "文字色", background_title: "背景色", colors: { - default: "デフォルト", + default: "自動", gray: "グレー", brown: "茶色", red: "赤", @@ -424,9 +424,11 @@ export const ja: Dictionary = { edited: "編集済み", save_button_text: "保存", cancel_button_text: "キャンセル", + deleted_reference_text: "元のコンテンツが削除されました", actions: { add_reaction: "リアクションを追加", resolve: "解決", + reopen: "再開", edit_comment: "コメントを編集", delete_comment: "コメントを削除", more_actions: "その他の操作", diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index cce4c8f7c6..53a5def39e 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -242,7 +242,7 @@ export const ko: Dictionary = { text_title: "텍스트", background_title: "배경", colors: { - default: "기본", + default: "자동", gray: "회색", brown: "갈색", red: "빨간색", @@ -397,9 +397,11 @@ export const ko: Dictionary = { edited: "수정됨", save_button_text: "저장", cancel_button_text: "취소", + deleted_reference_text: "원본 콘텐츠 삭제됨", actions: { add_reaction: "반응 추가", resolve: "해결", + reopen: "다시 열기", edit_comment: "댓글 수정", delete_comment: "댓글 삭제", more_actions: "더 많은 작업", diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index 6d1f48cde2..a1bff3fc6b 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -230,7 +230,7 @@ export const nl: Dictionary = { text_title: "Tekst", background_title: "Achtergrond", colors: { - default: "Standaard", + default: "Automatisch", gray: "Grijs", brown: "Bruin", red: "Rood", @@ -384,9 +384,11 @@ export const nl: Dictionary = { edited: "bewerkt", save_button_text: "Opslaan", cancel_button_text: "Annuleren", + deleted_reference_text: "Originele inhoud verwijderd", actions: { add_reaction: "Reactie toevoegen", resolve: "Oplossen", + reopen: "Heropenen", edit_comment: "Reactie bewerken", delete_comment: "Reactie verwijderen", more_actions: "Meer acties", diff --git a/packages/core/src/i18n/locales/no.ts b/packages/core/src/i18n/locales/no.ts index c28cac2b9f..5d518d116b 100644 --- a/packages/core/src/i18n/locales/no.ts +++ b/packages/core/src/i18n/locales/no.ts @@ -247,7 +247,7 @@ export const no: Dictionary = { text_title: "Tekst", background_title: "Bakgrunn", colors: { - default: "Standard", + default: "Automatisk", gray: "Grå", brown: "Brun", red: "Rød", @@ -401,9 +401,11 @@ export const no: Dictionary = { edited: "redigert", save_button_text: "Lagre", cancel_button_text: "Avbryt", + deleted_reference_text: "Originalt innhold slettet", actions: { add_reaction: "Legg til reaksjon", resolve: "Løs", + reopen: "Gjenåpne", edit_comment: "Rediger kommentar", delete_comment: "Slett kommentar", more_actions: "Flere handlinger", diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index 35bf1f255a..614f64e9f2 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -220,7 +220,7 @@ export const pl: Dictionary = { text_title: "Tekst", background_title: "Tło", colors: { - default: "Domyślny", + default: "Automatyczny", gray: "Szary", brown: "Brązowy", red: "Czerwony", @@ -375,9 +375,11 @@ export const pl: Dictionary = { edited: "edytowany", save_button_text: "Zapisz", cancel_button_text: "Anuluj", + deleted_reference_text: "Oryginalna treść usunięta", actions: { add_reaction: "Dodaj reakcję", resolve: "Rozwiąż", + reopen: "Otwórz ponownie", edit_comment: "Edytuj komentarz", delete_comment: "Usuń komentarz", more_actions: "Więcej akcji", diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index 7801cd2d36..c12c94012e 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -221,7 +221,7 @@ export const pt: Dictionary = { text_title: "Texto", background_title: "Fundo", colors: { - default: "Padrão", + default: "Automático", gray: "Cinza", brown: "Marrom", red: "Vermelho", @@ -376,9 +376,11 @@ export const pt: Dictionary = { edited: "editado", save_button_text: "Salvar", cancel_button_text: "Cancelar", + deleted_reference_text: "Conteúdo original excluído", actions: { add_reaction: "Adicionar reação", resolve: "Resolver", + reopen: "Reabrir", edit_comment: "Editar comentário", delete_comment: "Excluir comentário", more_actions: "Mais ações", diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts index 0c7537b1bc..2982c8f5f6 100644 --- a/packages/core/src/i18n/locales/ru.ts +++ b/packages/core/src/i18n/locales/ru.ts @@ -272,7 +272,7 @@ export const ru: Dictionary = { text_title: "Текст", background_title: "Задний фон", colors: { - default: "По умолчанию", + default: "Авто", gray: "Серый", brown: "Коричневый", red: "Красный", @@ -427,9 +427,11 @@ export const ru: Dictionary = { edited: "изменен", save_button_text: "Сохранить", cancel_button_text: "Отменить", + deleted_reference_text: "Исходный контент удалён", actions: { add_reaction: "Добавить реакцию", resolve: "Решить", + reopen: "Возобновить", edit_comment: "Редактировать комментарий", delete_comment: "Удалить комментарий", more_actions: "Другие действия", diff --git a/packages/core/src/i18n/locales/sk.ts b/packages/core/src/i18n/locales/sk.ts index cbdd0b706f..c1691e17e7 100644 --- a/packages/core/src/i18n/locales/sk.ts +++ b/packages/core/src/i18n/locales/sk.ts @@ -228,7 +228,7 @@ export const sk = { text_title: "Text", background_title: "Pozadie", colors: { - default: "Predvolená", + default: "Automaticky", gray: "Sivá", brown: "Hnedá", red: "Červená", @@ -382,9 +382,11 @@ export const sk = { edited: "upravený", save_button_text: "Uložiť", cancel_button_text: "Zrušiť", + deleted_reference_text: "Pôvodný obsah odstránený", actions: { add_reaction: "Pridať reakciu", resolve: "Vyriešiť", + reopen: "Znovu otvoriť", edit_comment: "Upraviť komentár", delete_comment: "Vymazať komentár", more_actions: "Ďalšie akcie", diff --git a/packages/core/src/i18n/locales/uk.ts b/packages/core/src/i18n/locales/uk.ts index a99a4259c6..a5d7d8f9af 100644 --- a/packages/core/src/i18n/locales/uk.ts +++ b/packages/core/src/i18n/locales/uk.ts @@ -254,7 +254,7 @@ export const uk: Dictionary = { text_title: "Текст", background_title: "Фон", colors: { - default: "За замовчуванням", + default: "Авто", gray: "Сірий", brown: "Коричневий", red: "Червоний", @@ -408,9 +408,11 @@ export const uk: Dictionary = { edited: "відредаговано", save_button_text: "Зберегти", cancel_button_text: "Скасувати", + deleted_reference_text: "Оригінальний вміст видалено", actions: { add_reaction: "Додати реакцію", resolve: "Вирішити", + reopen: "Відкрити знову", edit_comment: "Редагувати коментар", delete_comment: "Видалити коментар", more_actions: "Більше дій", diff --git a/packages/core/src/i18n/locales/uz.ts b/packages/core/src/i18n/locales/uz.ts index 8330db43b4..ffc8d04ac6 100644 --- a/packages/core/src/i18n/locales/uz.ts +++ b/packages/core/src/i18n/locales/uz.ts @@ -294,7 +294,7 @@ export const uz: Dictionary = { text_title: "Matn", background_title: "Fon", colors: { - default: "Standart", + default: "Avtomatik", gray: "Kulrang", brown: "Jigarrang", red: "Qizil", @@ -417,9 +417,11 @@ export const uz: Dictionary = { edited: "tahrirlangan", save_button_text: "Saqlash", cancel_button_text: "Bekor qilish", + deleted_reference_text: "Asl tarkib o‘chirildi", actions: { add_reaction: "Reaksiya qo‘shish", resolve: "Hal qilish", + reopen: "Qayta ochish", edit_comment: "Izohni tahrirlash", delete_comment: "Izohni o‘chirish", more_actions: "Boshqa amallar", diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index b300fdcfd0..cbe0e5e628 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -228,7 +228,7 @@ export const vi: Dictionary = { text_title: "Văn bản", background_title: "Nền", colors: { - default: "Mặc định", + default: "Tự động", gray: "Xám", brown: "Nâu", red: "Đỏ", @@ -383,9 +383,11 @@ export const vi: Dictionary = { edited: "đã chỉnh sửa", save_button_text: "Lưu", cancel_button_text: "Hủy", + deleted_reference_text: "Nội dung gốc đã bị xóa", actions: { add_reaction: "Thêm phản ứng", resolve: "Giải quyết", + reopen: "Mở lại", edit_comment: "Chỉnh sửa bình luận", delete_comment: "Xóa bình luận", more_actions: "Thêm hành động", diff --git a/packages/core/src/i18n/locales/zh-tw.ts b/packages/core/src/i18n/locales/zh-tw.ts index e9aa1e8ac6..b64912255f 100644 --- a/packages/core/src/i18n/locales/zh-tw.ts +++ b/packages/core/src/i18n/locales/zh-tw.ts @@ -270,7 +270,7 @@ export const zhTW: Dictionary = { text_title: "文字", background_title: "背景色", colors: { - default: "預設", + default: "自動", gray: "灰色", brown: "棕色", red: "紅色", @@ -425,9 +425,11 @@ export const zhTW: Dictionary = { edited: "已編輯", save_button_text: "儲存", cancel_button_text: "取消", + deleted_reference_text: "原始內容已刪除", actions: { add_reaction: "新增回應", resolve: "解決", + reopen: "重新開啟", edit_comment: "編輯評論", delete_comment: "刪除評論", more_actions: "更多操作", diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index b44c81aa36..ba5a2fe73b 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -270,7 +270,7 @@ export const zh: Dictionary = { text_title: "文本", background_title: "背景色", colors: { - default: "默认", + default: "自动", gray: "灰色", brown: "棕色", red: "红色", @@ -425,9 +425,11 @@ export const zh: Dictionary = { edited: "已编辑", save_button_text: "保存", cancel_button_text: "取消", + deleted_reference_text: "原始内容已删除", actions: { add_reaction: "添加反应", resolve: "解决", + reopen: "重新打开", edit_comment: "编辑评论", delete_comment: "删除评论", more_actions: "更多操作", diff --git a/packages/core/src/schema/inlineContent/types.ts b/packages/core/src/schema/inlineContent/types.ts index 1e5e2dbd63..b8e922502a 100644 --- a/packages/core/src/schema/inlineContent/types.ts +++ b/packages/core/src/schema/inlineContent/types.ts @@ -145,7 +145,7 @@ export type InlineContent< T extends StyleSchema, > = InlineContentFromConfig; -type PartialInlineContentElement< +export type PartialInlineContentElement< I extends InlineContentSchema, T extends StyleSchema, > = PartialInlineContentFromConfig; diff --git a/packages/dev-scripts/examples/template-react/package.json.template.tsx b/packages/dev-scripts/examples/template-react/package.json.template.tsx index a08cdc93ad..34701ed200 100644 --- a/packages/dev-scripts/examples/template-react/package.json.template.tsx +++ b/packages/dev-scripts/examples/template-react/package.json.template.tsx @@ -18,9 +18,8 @@ const template = (project: Project) => ({ "@blocknote/mantine": "latest", "@blocknote/react": "latest", "@blocknote/shadcn": "latest", - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^9.0.2", + "@mantine/hooks": "^9.0.2", react: "^19.2.3", "react-dom": "^19.2.3", ...(project.config.tailwind @@ -39,8 +38,8 @@ const template = (project: Project) => ({ : {}), "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^4.7.0", - vite: "^5.4.20", + "@vitejs/plugin-react": "^6.0.1", + vite: "^8.0.8", ...(project.config?.devDependencies || {}), }, }); diff --git a/packages/dev-scripts/package.json b/packages/dev-scripts/package.json index b2f591011a..9e073179c9 100644 --- a/packages/dev-scripts/package.json +++ b/packages/dev-scripts/package.json @@ -8,7 +8,7 @@ "directory": "packages/dev-scripts" }, "license": "MPL-2.0", - "version": "0.48.0", + "version": "0.51.0", "description": "", "type": "module", "scripts": { @@ -23,8 +23,8 @@ "@types/react-dom": "^19.2.3", "eslint": "^8.57.1", "glob": "^10.5.0", - "react": "^19.2.3", - "react-dom": "^19.2.3", + "react": "^19.2.5", + "react-dom": "^19.2.5", "tinyglobby": "0.2.12", "tsx": "^4.20.6", "typescript": "^5.9.3" diff --git a/packages/mantine/package.json b/packages/mantine/package.json index 7f2a09044a..65e2898965 100644 --- a/packages/mantine/package.json +++ b/packages/mantine/package.json @@ -11,7 +11,7 @@ "directory": "packages/mantine" }, "license": "MPL-2.0", - "version": "0.48.0", + "version": "0.51.0", "files": [ "dist", "types", @@ -61,8 +61,8 @@ "clean": "rimraf dist && rimraf types" }, "dependencies": { - "@blocknote/core": "0.48.0", - "@blocknote/react": "0.48.0", + "@blocknote/core": "0.51.0", + "@blocknote/react": "0.51.0", "react-icons": "^5.5.0" }, "devDependencies": { @@ -70,19 +70,18 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^8.57.1", - "react": "^19.2.3", - "react-dom": "^19.2.3", + "react": "^19.2.5", + "react-dom": "^19.2.5", "rimraf": "^5.0.10", "rollup-plugin-webpack-stats": "^0.2.6", "typescript": "^5.9.3", - "vite": "^8.0.3", + "vite": "^8.0.8", "vite-plugin-eslint": "^1.8.1", "vite-plugin-externalize-deps": "^0.10.0" }, "peerDependencies": { - "@mantine/core": "^8.3.11", - "@mantine/hooks": "^8.3.11", - "@mantine/utils": "^6.0.22", + "@mantine/core": "^8.3.11 || ^9.0.2", + "@mantine/hooks": "^8.3.11 || ^9.0.2", "react": "^18.0 || ^19.0 || >= 19.0.0-rc", "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" }, diff --git a/packages/mantine/src/suggestionMenu/SuggestionMenuLoader.tsx b/packages/mantine/src/suggestionMenu/SuggestionMenuLoader.tsx index 93255e0bd2..a742ccb19a 100644 --- a/packages/mantine/src/suggestionMenu/SuggestionMenuLoader.tsx +++ b/packages/mantine/src/suggestionMenu/SuggestionMenuLoader.tsx @@ -13,6 +13,8 @@ export const SuggestionMenuLoader = forwardRef< assertEmpty(rest); return ( - +
        + +
        ); }); diff --git a/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.tsx b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.tsx index 42dda5d583..d241354d3d 100644 --- a/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.tsx +++ b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.tsx @@ -18,11 +18,8 @@ export const GridSuggestionMenuLoader = forwardRef< assertEmpty(rest); return ( - +
        + +
        ); }); diff --git a/packages/react/package.json b/packages/react/package.json index 31f3d0fbfb..539eb8980e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -11,7 +11,7 @@ "directory": "packages/react" }, "license": "MPL-2.0", - "version": "0.48.0", + "version": "0.51.0", "files": [ "dist", "types", @@ -58,7 +58,7 @@ "clean": "rimraf dist && rimraf types" }, "dependencies": { - "@blocknote/core": "0.48.0", + "@blocknote/core": "0.51.0", "@emoji-mart/data": "^1.2.1", "@floating-ui/react": "^0.27.18", "@floating-ui/utils": "^0.2.10", @@ -74,7 +74,6 @@ "use-sync-external-store": "1.6.0" }, "devDependencies": { - "@types/emoji-mart": "^3.0.14", "@types/lodash.foreach": "^4.5.9", "@types/lodash.groupby": "^4.6.9", "@types/lodash.merge": "^4.6.9", @@ -82,12 +81,12 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^8.57.1", - "react": "^19.2.3", - "react-dom": "^19.2.3", + "react": "^19.2.5", + "react-dom": "^19.2.5", "rimraf": "^5.0.10", "rollup-plugin-webpack-stats": "^0.2.6", "typescript": "^5.9.3", - "vite": "^8.0.3", + "vite": "^8.0.8", "vite-plugin-eslint": "^1.8.1", "vite-plugin-externalize-deps": "^0.10.0", "vitest": "^4.1.2" diff --git a/packages/react/src/blocks/File/helpers/render/FileBlockWrapper.tsx b/packages/react/src/blocks/File/helpers/render/FileBlockWrapper.tsx index 6499b40ca0..f277db693f 100644 --- a/packages/react/src/blocks/File/helpers/render/FileBlockWrapper.tsx +++ b/packages/react/src/blocks/File/helpers/render/FileBlockWrapper.tsx @@ -29,8 +29,14 @@ export const FileBlockWrapper = ( ) => { const showLoader = useUploadLoading(props.block.id); + // Use a
        /
        when the block has a caption, so the caption + // is semantically associated with its content for assistive tech. + const useFigure = + props.block.props.url !== "" && !!props.block.props.caption && !showLoader; + const Wrapper = useFigure ? "figure" : "div"; + return ( -
        {props.block.props.caption}

        +
        + {props.block.props.caption} +
        )} )} -
        + ); }; diff --git a/packages/react/src/blocks/Image/block.tsx b/packages/react/src/blocks/Image/block.tsx index 92de17d8cc..870a16ec5c 100644 --- a/packages/react/src/blocks/Image/block.tsx +++ b/packages/react/src/blocks/Image/block.tsx @@ -18,6 +18,11 @@ export const ImagePreview = ( ) => { const resolved = useResolveUrl(props.block.props.url!); + // alt describes image content (per WCAG H86); figcaption (when present) + // is the contextual caption. Fall back to "" so unlabelled images are + // marked decorative rather than getting a noisy generic fallback. + const alt = props.block.props.name || ""; + return ( {props.block.props.caption @@ -43,12 +49,11 @@ export const ImageToExternalHTML = ( return

        Add image

        ; } + const alt = props.block.props.name || ""; const image = props.block.props.showPreview ? ( { ) : ( diff --git a/packages/react/src/blocks/Video/block.tsx b/packages/react/src/blocks/Video/block.tsx index 08a8f47d53..832794c095 100644 --- a/packages/react/src/blocks/Video/block.tsx +++ b/packages/react/src/blocks/Video/block.tsx @@ -27,6 +27,7 @@ export const VideoPreview = ( : resolved.downloadUrl } controls={true} + width={props.block.props.previewWidth || undefined} contentEditable={false} draggable={false} /> @@ -44,7 +45,7 @@ export const VideoToExternalHTML = ( } const video = props.block.props.showPreview ? ( -
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/codeBlock/empty.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/codeBlock/empty.html index c25f830ff4..ce97dbaaac 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/codeBlock/empty.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/codeBlock/empty.html @@ -9,9 +9,7 @@
-          
-            
-          
+          
         
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/complex/document.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/complex/document.html new file mode 100644 index 0000000000..4376ebf7f1 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/complex/document.html @@ -0,0 +1,90 @@ +
+
+
+
+

Document Title

+
+
+
+
+
+
+

Introduction paragraph.

+
+
+
+
+
+
+

Section 1

+
+
+
+
+
+
+

+ Text with + bold + and + a link + . +

+
+
+
+
+
+
+

First point

+
+
+
+
+
+
+

Second point

+
+
+
+
+
+
+
+
+
+
+
+
+
+
A notable quote
+
+
+
+
+
+
+
+ +
+
+          const x = 42;
+        
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/basic.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/basic.html index 9974d8d975..d7802da3e3 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/basic.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/basic.html @@ -9,7 +9,7 @@ data-caption="Caption" data-file-block="" > -
+
@@ -20,8 +20,8 @@

example

-

Caption

-
+
Caption
+
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/nested.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/nested.html index 6553a5c4a8..c62486d27d 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/nested.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/nested.html @@ -9,7 +9,7 @@ data-caption="Caption" data-file-block="" > -
+
@@ -20,8 +20,8 @@

example

-

Caption

-
+
Caption
+
@@ -34,7 +34,7 @@ data-caption="Caption" data-file-block="" > -
+
@@ -45,8 +45,8 @@

example

-

Caption

-
+
Caption
+
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/noName.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/noName.html index 47ae5b3bf9..1ec20af747 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/noName.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/file/noName.html @@ -8,7 +8,7 @@ data-caption="Caption" data-file-block="" > -
+
@@ -19,8 +19,8 @@

-

Caption

-
+
Caption
+ diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/hardbreak/between-links.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/hardbreak/between-links.html index 9e4b427c62..cbc42aa3f9 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/hardbreak/between-links.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/hardbreak/between-links.html @@ -4,15 +4,21 @@ diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/hardbreak/link.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/hardbreak/link.html index 4cae02d67b..a896507f92 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/hardbreak/link.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/hardbreak/link.html @@ -4,15 +4,21 @@ diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h1.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h1.html new file mode 100644 index 0000000000..3e3e513852 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h1.html @@ -0,0 +1,9 @@ +
+
+
+
+

Heading 1

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h2.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h2.html new file mode 100644 index 0000000000..7ffe42afa3 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h2.html @@ -0,0 +1,9 @@ +
+
+
+
+

Heading 2

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h3.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h3.html new file mode 100644 index 0000000000..437867e0d9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h3.html @@ -0,0 +1,9 @@ +
+
+
+
+

Heading 3

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h4.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h4.html new file mode 100644 index 0000000000..1ef4e627ff --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h4.html @@ -0,0 +1,9 @@ +
+
+
+
+

Heading 4

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h5.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h5.html new file mode 100644 index 0000000000..f44690aa57 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h5.html @@ -0,0 +1,9 @@ +
+
+
+
+
Heading 5
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h6.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h6.html new file mode 100644 index 0000000000..5daca5fbdf --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h6.html @@ -0,0 +1,9 @@ +
+
+
+
+
Heading 6
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/styled.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/styled.html new file mode 100644 index 0000000000..31df3416ba --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/styled.html @@ -0,0 +1,12 @@ +
+
+
+
+

+ Bold + Heading +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/toggleable.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/toggleable.html new file mode 100644 index 0000000000..2982ce3673 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/toggleable.html @@ -0,0 +1,38 @@ +
+
+
+
+
+
+ +

Toggle Heading

+
+
+
+
+
+
+
+

Child content

+
+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image.html index cf93dca684..a17b1d3982 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image.html @@ -15,7 +15,7 @@ BlockNote image diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/basic.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/basic.html index 5c7411f3ed..8700c93f57 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/basic.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/basic.html @@ -10,17 +10,23 @@ data-preview-width="256" data-file-block="" > -
- example + example
-

Caption

-
+
Caption
+ diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/nested.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/nested.html index 7eac772896..b7d2064ce9 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/nested.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/nested.html @@ -9,17 +9,23 @@ data-preview-width="256" data-file-block="" > -
- Caption +
-

Caption

-
+
Caption
+
@@ -32,17 +38,23 @@ data-preview-width="256" data-file-block="" > -
- Caption +
-

Caption

-
+
Caption
+
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noCaption.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noCaption.html index 67d4f962f0..44b706cebe 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noCaption.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noCaption.html @@ -14,7 +14,13 @@ style="position: relative; width: 256px;" >
- example + example
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noName.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noName.html index 315d8db293..2a2bbd7a8d 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noName.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noName.html @@ -9,17 +9,23 @@ data-preview-width="256" data-file-block="" > -
- Caption +
-

Caption

-
+
Caption
+ diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noPreview.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noPreview.html index 3e1f5a6264..59d65f82ae 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noPreview.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/noPreview.html @@ -11,7 +11,7 @@ data-preview-width="256" data-file-block="" > -
+
@@ -22,8 +22,8 @@

example

-

Caption

-
+
Caption
+ diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/urlOnly.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/urlOnly.html new file mode 100644 index 0000000000..6940729386 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/urlOnly.html @@ -0,0 +1,23 @@ +
+
+
+
+
+
+ + + +
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/withCaption.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/withCaption.html new file mode 100644 index 0000000000..5844c98607 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/withCaption.html @@ -0,0 +1,31 @@ +
+
+
+
+
+
+ Example Image + + +
+
This is a caption
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/adjacent.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/adjacent.html index 2408c611ac..9af317b375 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/adjacent.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/adjacent.html @@ -4,14 +4,20 @@ diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/basic.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/basic.html index 3daea90831..159dfce9ea 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/basic.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/basic.html @@ -4,9 +4,12 @@ diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/plainUrl.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/plainUrl.html new file mode 100644 index 0000000000..db540213b4 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/plainUrl.html @@ -0,0 +1,18 @@ +
+
+ +
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/styled.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/styled.html index 2b9d4cb574..766681b45a 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/styled.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/styled.html @@ -5,15 +5,21 @@

Web site

diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/urlWithParens.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/urlWithParens.html new file mode 100644 index 0000000000..ab632bd13f --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/urlWithParens.html @@ -0,0 +1,18 @@ +
+
+
+
+

+ Example +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/withCode.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/withCode.html new file mode 100644 index 0000000000..45427ff9ad --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/withCode.html @@ -0,0 +1,21 @@ +
+
+
+
+

+ See the + docs + for + config +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/lists/numberedListStart.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/lists/numberedListStart.html new file mode 100644 index 0000000000..62b0458466 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/lists/numberedListStart.html @@ -0,0 +1,25 @@ +
+
+
+
+

Item 5

+
+
+
+
+
+
+

Item 6

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/multiple.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/multiple.html new file mode 100644 index 0000000000..a5c65cdb68 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/multiple.html @@ -0,0 +1,23 @@ +
+
+
+
+

First paragraph

+
+
+
+
+
+
+

Second paragraph

+
+
+
+
+
+
+

Third paragraph

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/basic.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/basic.html new file mode 100644 index 0000000000..278aafa3ec --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/basic.html @@ -0,0 +1,9 @@ +
+
+
+
+
This is a quote
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/multiple.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/multiple.html new file mode 100644 index 0000000000..99c7aa530e --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/multiple.html @@ -0,0 +1,16 @@ +
+
+
+
+
First quote
+
+
+
+
+
+
+
Second quote
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/nested.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/nested.html new file mode 100644 index 0000000000..da3b3d1215 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/nested.html @@ -0,0 +1,18 @@ +
+
+
+
+
Parent quote
+
+
+
+
+
+

Nested paragraph

+
+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/styled.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/styled.html new file mode 100644 index 0000000000..244868c2c6 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/styled.html @@ -0,0 +1,14 @@ +
+
+
+
+
+ Bold + and + italic + quote +
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/withLink.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/withLink.html new file mode 100644 index 0000000000..5cbacbdec1 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/withLink.html @@ -0,0 +1,19 @@ +
+
+
+
+
+ Quote with + a link +
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/backgroundColor.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/backgroundColor.html new file mode 100644 index 0000000000..da1b939f67 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/backgroundColor.html @@ -0,0 +1,11 @@ +
+
+
+
+

+ Highlighted text +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/bold.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/bold.html new file mode 100644 index 0000000000..1834b5c3ca --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/bold.html @@ -0,0 +1,11 @@ +
+
+
+
+

+ Bold text +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/boldItalicStrike.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/boldItalicStrike.html new file mode 100644 index 0000000000..f8baaa4507 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/boldItalicStrike.html @@ -0,0 +1,15 @@ +
+
+
+
+

+ + + All styles + + +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/code.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/code.html new file mode 100644 index 0000000000..31aa7a6dba --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/code.html @@ -0,0 +1,11 @@ +
+
+
+
+

+ Inline code +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/combined.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/combined.html new file mode 100644 index 0000000000..3bcf4491b2 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/combined.html @@ -0,0 +1,13 @@ +
+
+
+
+

+ + Bold and italic + +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/italic.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/italic.html new file mode 100644 index 0000000000..265708f07d --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/italic.html @@ -0,0 +1,11 @@ +
+
+
+
+

+ Italic text +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/mixedInParagraph.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/mixedInParagraph.html new file mode 100644 index 0000000000..b8f217f641 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/mixedInParagraph.html @@ -0,0 +1,15 @@ +
+
+
+
+

+ Normal + bold + italic + code + strike +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/strike.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/strike.html new file mode 100644 index 0000000000..294425c21f --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/strike.html @@ -0,0 +1,11 @@ +
+
+
+
+

+ Strikethrough text +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/textColor.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/textColor.html new file mode 100644 index 0000000000..0e6799d766 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/textColor.html @@ -0,0 +1,11 @@ +
+
+
+
+

+ Colored text +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/underline.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/underline.html new file mode 100644 index 0000000000..29ab7e88cf --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/underline.html @@ -0,0 +1,11 @@ +
+
+
+
+

+ Underline text +

+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/advancedExample.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/advancedExample.html new file mode 100644 index 0000000000..1327b27445 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/advancedExample.html @@ -0,0 +1,82 @@ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+

This row has headers

+
+

+ This is + RED +

+
+

Text is Blue

+
+

+ This spans 2 columns +
+ and 2 rows +

+
+

Sooo many features

+
+

+
+

A cell

+
+

Another Cell

+
+

Aligned center

+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/cellTextAlignment.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/cellTextAlignment.html new file mode 100644 index 0000000000..8b1b1756b0 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/cellTextAlignment.html @@ -0,0 +1,33 @@ +
+
+
+
+
+
+ + + + + + + + + + + +
+

Left

+
+

Center

+
+

Right

+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/emptyCells.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/emptyCells.html new file mode 100644 index 0000000000..b3564081f0 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/emptyCells.html @@ -0,0 +1,37 @@ +
+
+
+
+
+
+ + + + + + + + + + + + + +
+

Has content

+
+

+
+

+
+

Also has content

+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/hardBreakInCell.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/hardBreakInCell.html new file mode 100644 index 0000000000..d0c5618ffc --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/hardBreakInCell.html @@ -0,0 +1,33 @@ +
+
+
+
+
+
+ + + + + + + + + +
+

+ Line 1 +
+ Line 2 +

+
+

Normal cell

+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/headerRowsAndCols.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/headerRowsAndCols.html new file mode 100644 index 0000000000..945741a785 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/headerRowsAndCols.html @@ -0,0 +1,44 @@ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + +
+

Corner

+
+

Column Header 1

+
+

Column Header 2

+
+

Row Header 1

+
+

Data 1

+
+

Data 2

+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/linksInCells.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/linksInCells.html new file mode 100644 index 0000000000..8aa07b4b98 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/linksInCells.html @@ -0,0 +1,56 @@ +
+
+
+
+
+
+ + + + + + + + + + + + + +
+

+ Visit + Example +

+
+

Plain cell

+
+

Data

+
+

+ Link +

+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/singleCell.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/singleCell.html new file mode 100644 index 0000000000..63c4da11a1 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/singleCell.html @@ -0,0 +1,25 @@ +
+
+
+
+
+
+ + + + + + + +
+

Only cell

+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/styledCellContent.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/styledCellContent.html new file mode 100644 index 0000000000..9d8035d5c5 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/styledCellContent.html @@ -0,0 +1,45 @@ +
+
+
+
+
+
+ + + + + + + + + + + + + +
+

+ Bold +

+
+

+ Italic +

+
+

+ Strike +

+
+

+ Code +

+
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video.html index 8bc01016c4..78babe7a23 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video.html @@ -17,7 +17,6 @@ src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm" controls="" draggable="false" - width="0" > diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video/withCaption.html b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video/withCaption.html new file mode 100644 index 0000000000..951b9d8b86 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video/withCaption.html @@ -0,0 +1,31 @@ +
+
+
+
+
+
+ + + +
+
Video caption
+
+
+
+
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/basic.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/basic.html new file mode 100644 index 0000000000..d2a69001ba --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/basic.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/button.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/button.html new file mode 100644 index 0000000000..915743c700 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/button.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/noName.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/noName.html new file mode 100644 index 0000000000..1699b58c73 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/noName.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/codeBlock/contains-newlines.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/codeBlock/contains-newlines.html index df88fa0937..a7db81b06b 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/codeBlock/contains-newlines.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/codeBlock/contains-newlines.html @@ -1,8 +1,5 @@
-  
-    const hello ='world';
-    
- console.log(hello); -
-
+ const hello = 'world'; +console.log(hello); +
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/document.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/document.html new file mode 100644 index 0000000000..421d420c08 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/document.html @@ -0,0 +1,30 @@ +

Document Title

+

Introduction paragraph.

+

Section 1

+

+ Text with + bold + and + a link + . +

+
    +
  • +

    First point

    +
  • +
  • +

    Second point

    +
  • +
+
+
A notable quote
+
+  const x = 42;
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/file/button.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/file/button.html index cc675c57a7..90ce06d701 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/file/button.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/file/button.html @@ -1 +1 @@ -

Add file

\ No newline at end of file + \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/hardbreak/between-links.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/hardbreak/between-links.html index 701b5d4213..54664cf2b1 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/hardbreak/between-links.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/hardbreak/between-links.html @@ -1,13 +1,19 @@

Link1
Link2

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/hardbreak/link.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/hardbreak/link.html index 2c762aedc5..930c29aaab 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/hardbreak/link.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/hardbreak/link.html @@ -1,13 +1,19 @@

Link1
Link1

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h1.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h1.html new file mode 100644 index 0000000000..ac06cdc123 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h1.html @@ -0,0 +1 @@ +

Heading 1

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h2.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h2.html new file mode 100644 index 0000000000..92e9734754 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h2.html @@ -0,0 +1 @@ +

Heading 2

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h3.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h3.html new file mode 100644 index 0000000000..df25998db1 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h3.html @@ -0,0 +1 @@ +

Heading 3

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h4.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h4.html new file mode 100644 index 0000000000..430144bc54 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h4.html @@ -0,0 +1 @@ +

Heading 4

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h5.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h5.html new file mode 100644 index 0000000000..02e7e8fda2 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h5.html @@ -0,0 +1 @@ +
Heading 5
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h6.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h6.html new file mode 100644 index 0000000000..6e76905810 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h6.html @@ -0,0 +1 @@ +
Heading 6
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/styled.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/styled.html new file mode 100644 index 0000000000..7f14fdb711 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/styled.html @@ -0,0 +1,4 @@ +

+ Bold + Heading +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/toggleable.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/toggleable.html new file mode 100644 index 0000000000..ecec05b566 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/toggleable.html @@ -0,0 +1,6 @@ +
+ +

Toggle Heading

+
+

Child content

+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image.html index ef7342fe92..a9efaefefc 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image.html @@ -1,5 +1,5 @@ BlockNote image \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/button.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/button.html index 8553433aff..df18852143 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/button.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/button.html @@ -1 +1 @@ -

Add image

\ No newline at end of file + \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/nested.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/nested.html index 04ccf17c56..667cef41ab 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/nested.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/nested.html @@ -1,5 +1,5 @@
- Caption +
Caption
- Caption +
Caption
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/noName.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/noName.html index 686fc7d4e5..47f0cbe255 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/noName.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/noName.html @@ -1,4 +1,4 @@
- Caption +
Caption
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/urlOnly.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/urlOnly.html new file mode 100644 index 0000000000..41960a99f8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/urlOnly.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/withCaption.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/withCaption.html new file mode 100644 index 0000000000..3ecba73103 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/image/withCaption.html @@ -0,0 +1,8 @@ +
+ Example Image +
This is a caption
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/adjacent.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/adjacent.html index db99691d33..057be7e7f6 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/adjacent.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/adjacent.html @@ -1,12 +1,18 @@

Website Website2

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/basic.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/basic.html index 4b61e8c582..610e86b2dd 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/basic.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/basic.html @@ -1,7 +1,10 @@

Website

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/plainUrl.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/plainUrl.html new file mode 100644 index 0000000000..d014edd8bb --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/plainUrl.html @@ -0,0 +1,10 @@ +

+ https://www.website.com +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/styled.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/styled.html index fb7737f7f8..189a36f5e8 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/styled.html +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/styled.html @@ -1,14 +1,20 @@

Web site

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/urlWithParens.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/urlWithParens.html new file mode 100644 index 0000000000..a783db1f73 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/urlWithParens.html @@ -0,0 +1,10 @@ +

+ Example +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/withCode.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/withCode.html new file mode 100644 index 0000000000..385c0be8c9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/link/withCode.html @@ -0,0 +1,13 @@ +

+ See the + docs + for + config +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/lists/numberedListStart.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/lists/numberedListStart.html new file mode 100644 index 0000000000..35535f7db4 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/lists/numberedListStart.html @@ -0,0 +1,8 @@ +
    +
  1. +

    Item 5

    +
  2. +
  3. +

    Item 6

    +
  4. +
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/multiple.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/multiple.html new file mode 100644 index 0000000000..a183a01cd8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/multiple.html @@ -0,0 +1,3 @@ +

First paragraph

+

Second paragraph

+

Third paragraph

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/basic.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/basic.html new file mode 100644 index 0000000000..53c51228f1 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/basic.html @@ -0,0 +1 @@ +
This is a quote
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/multiple.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/multiple.html new file mode 100644 index 0000000000..80b8a40ae5 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/multiple.html @@ -0,0 +1,2 @@ +
First quote
+
Second quote
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/nested.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/nested.html new file mode 100644 index 0000000000..3e74d08d92 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/nested.html @@ -0,0 +1,2 @@ +
Parent quote
+

Nested paragraph

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/styled.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/styled.html new file mode 100644 index 0000000000..7f80b7fc7c --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/styled.html @@ -0,0 +1,6 @@ +
+ Bold + and + italic + quote +
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/withLink.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/withLink.html new file mode 100644 index 0000000000..c893fa67ee --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/withLink.html @@ -0,0 +1,11 @@ +
+ Quote with + a link +
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/backgroundColor.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/backgroundColor.html new file mode 100644 index 0000000000..66f327e85d --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/backgroundColor.html @@ -0,0 +1,8 @@ +

+ Highlighted text +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/bold.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/bold.html new file mode 100644 index 0000000000..e57a879f6e --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/bold.html @@ -0,0 +1,3 @@ +

+ Bold text +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/boldItalicStrike.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/boldItalicStrike.html new file mode 100644 index 0000000000..d7506fe610 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/boldItalicStrike.html @@ -0,0 +1,7 @@ +

+ + + All styles + + +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/code.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/code.html new file mode 100644 index 0000000000..6fe865b744 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/code.html @@ -0,0 +1,3 @@ +

+ Inline code +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/combined.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/combined.html new file mode 100644 index 0000000000..920576e90a --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/combined.html @@ -0,0 +1,5 @@ +

+ + Bold and italic + +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/italic.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/italic.html new file mode 100644 index 0000000000..fcff5726e6 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/italic.html @@ -0,0 +1,3 @@ +

+ Italic text +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/mixedInParagraph.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/mixedInParagraph.html new file mode 100644 index 0000000000..369fd1b3bf --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/mixedInParagraph.html @@ -0,0 +1,7 @@ +

+ Normal + bold + italic + code + strike +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/strike.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/strike.html new file mode 100644 index 0000000000..abfabbe4e6 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/strike.html @@ -0,0 +1,3 @@ +

+ Strikethrough text +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/textColor.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/textColor.html new file mode 100644 index 0000000000..798166dcad --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/textColor.html @@ -0,0 +1,8 @@ +

+ Colored text +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/underline.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/underline.html new file mode 100644 index 0000000000..f861031c9e --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/style/underline.html @@ -0,0 +1,3 @@ +

+ Underline text +

\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/advancedExample.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/advancedExample.html new file mode 100644 index 0000000000..a4f5dfcb31 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/advancedExample.html @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+

This row has headers

+
+

+ This is + RED +

+
+

Text is Blue

+
+

+ This spans 2 columns +
+ and 2 rows +

+
+

Sooo many features

+
+

+
+

A cell

+
+

Another Cell

+
+

Aligned center

+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/cellTextAlignment.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/cellTextAlignment.html new file mode 100644 index 0000000000..21f5f0ab0c --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/cellTextAlignment.html @@ -0,0 +1,18 @@ + + + + + + + + + + + +
+

Left

+
+

Center

+
+

Right

+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/emptyCells.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/emptyCells.html new file mode 100644 index 0000000000..10a9fb5259 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/emptyCells.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + +
+

Has content

+
+

+
+

+
+

Also has content

+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/hardBreakInCell.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/hardBreakInCell.html new file mode 100644 index 0000000000..a313a5323d --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/hardBreakInCell.html @@ -0,0 +1,18 @@ + + + + + + + + + +
+

+ Line 1 +
+ Line 2 +

+
+

Normal cell

+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/headerRowsAndCols.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/headerRowsAndCols.html new file mode 100644 index 0000000000..64ee4183aa --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/headerRowsAndCols.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + +
+

Corner

+
+

Column Header 1

+
+

Column Header 2

+
+

Row Header 1

+
+

Data 1

+
+

Data 2

+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/linksInCells.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/linksInCells.html new file mode 100644 index 0000000000..0cafd0eda1 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/linksInCells.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + +
+

+ Visit + Example +

+
+

Plain cell

+
+

Data

+
+

+ Link +

+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/singleCell.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/singleCell.html new file mode 100644 index 0000000000..ce8bea5831 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/singleCell.html @@ -0,0 +1,10 @@ + + + + + + + +
+

Only cell

+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/styledCellContent.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/styledCellContent.html new file mode 100644 index 0000000000..91db4da0e4 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/table/styledCellContent.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + +
+

+ Bold +

+
+

+ Italic +

+
+

+ Strike +

+
+

+ Code +

+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/html/video/withCaption.html b/tests/src/unit/core/formatConversion/export/__snapshots__/html/video/withCaption.html new file mode 100644 index 0000000000..978dcc0448 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/html/video/withCaption.html @@ -0,0 +1,8 @@ +
+ +
Video caption
+
\ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/basic.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/basic.md new file mode 100644 index 0000000000..cdcf0dde54 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/basic.md @@ -0,0 +1 @@ + diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/button.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/button.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/button.md @@ -0,0 +1 @@ + diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/noName.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/noName.md new file mode 100644 index 0000000000..cdcf0dde54 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/noName.md @@ -0,0 +1 @@ + diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/document.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/document.md new file mode 100644 index 0000000000..47cf4739db --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/document.md @@ -0,0 +1,18 @@ +# Document Title + +Introduction paragraph. + +## Section 1 + +Text with **bold** and [a link](https://example.com). + +* First point +* Second point + +*** + +> A notable quote + +```javascript +const x = 42; +``` diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/misc.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/misc.md index 4a2de0a7fc..fca446bec3 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/misc.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/misc.md @@ -1,4 +1,4 @@ -## **Heading ***~~2~~* +## **Heading** *~~2~~* Paragraph diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/file/button.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/file/button.md index 8d3fa6a207..8b13789179 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/file/button.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/file/button.md @@ -1 +1 @@ -Add file + diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/file/noName.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/file/noName.md index c7fefc547f..4cca42f87d 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/file/noName.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/file/noName.md @@ -1,3 +1,3 @@ -[exampleURL](exampleURL) +exampleURL Caption diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/hardbreak/only.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/hardbreak/only.md index e69de29bb2..8b13789179 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/hardbreak/only.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/hardbreak/only.md @@ -0,0 +1 @@ + diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h1.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h1.md new file mode 100644 index 0000000000..bd706e91c4 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h1.md @@ -0,0 +1 @@ +# Heading 1 diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h2.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h2.md new file mode 100644 index 0000000000..cd760a44ba --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h2.md @@ -0,0 +1 @@ +## Heading 2 diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h3.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h3.md new file mode 100644 index 0000000000..607fcc43b6 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h3.md @@ -0,0 +1 @@ +### Heading 3 diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h4.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h4.md new file mode 100644 index 0000000000..9c7bd7c52e --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h4.md @@ -0,0 +1 @@ +#### Heading 4 diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h5.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h5.md new file mode 100644 index 0000000000..2410fdf2b0 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h5.md @@ -0,0 +1 @@ +##### Heading 5 diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h6.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h6.md new file mode 100644 index 0000000000..848d83e6dd --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h6.md @@ -0,0 +1 @@ +###### Heading 6 diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/styled.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/styled.md new file mode 100644 index 0000000000..90c78848fd --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/styled.md @@ -0,0 +1 @@ +# **Bold** Heading diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/toggleable.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/toggleable.md new file mode 100644 index 0000000000..cc8cbf3aa9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/toggleable.md @@ -0,0 +1,3 @@ +## Toggle Heading + +Child content diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image.md index 3219bb9f00..58d07ff1a4 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image.md @@ -1 +1 @@ -![BlockNote image](https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png) +![](https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png) diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/basic.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/basic.md index b350ae21e0..5a88869a1b 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/basic.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/basic.md @@ -1,3 +1 @@ -![example](exampleURL) - -Caption +
example
Caption
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/button.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/button.md index 02184caf8a..8b13789179 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/button.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/button.md @@ -1 +1 @@ -Add image + diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/nested.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/nested.md index 7a13551364..99ff1825d4 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/nested.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/nested.md @@ -1,7 +1,3 @@ -![Caption](exampleURL) +
Caption
-Caption - -![Caption](exampleURL) - -Caption +
Caption
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/noName.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/noName.md index c6b5864d90..ba1c350f13 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/noName.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/noName.md @@ -1,3 +1 @@ -![Caption](exampleURL) - -Caption +
Caption
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/urlOnly.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/urlOnly.md new file mode 100644 index 0000000000..f667f8e031 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/urlOnly.md @@ -0,0 +1 @@ +![](exampleURL) diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/withCaption.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/withCaption.md new file mode 100644 index 0000000000..fb8426fd87 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/withCaption.md @@ -0,0 +1 @@ +
Example Image
This is a caption
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/plainUrl.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/plainUrl.md new file mode 100644 index 0000000000..0ba1e54028 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/plainUrl.md @@ -0,0 +1 @@ +https://www.website.com diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/urlWithParens.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/urlWithParens.md new file mode 100644 index 0000000000..ebe94f4ffe --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/urlWithParens.md @@ -0,0 +1 @@ +[Example](https://en.wikipedia.org/wiki/Example_\(disambiguation\)) diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/withCode.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/withCode.md new file mode 100644 index 0000000000..090ae185e1 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/withCode.md @@ -0,0 +1 @@ +See the [docs](https://example.com) for `config` diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/basic.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/basic.md index f092d8bf95..a083f00804 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/basic.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/basic.md @@ -1,13 +1,9 @@ * Bullet List Item 1 - * Bullet List Item 2 1. Numbered List Item 1 - 2. Numbered List Item 2 * [ ] Check List Item 1 - * [x] Check List Item 2 - * Toggle List Item 1 diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/nested.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/nested.md index c43ddb13ef..a0388ef96d 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/nested.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/nested.md @@ -1,13 +1,7 @@ * Bullet List Item 1 - * Bullet List Item 2 - 1. Numbered List Item 1 - 2. Numbered List Item 2 - * [ ] Check List Item 1 - * [x] Check List Item 2 - * Toggle List Item 1 diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/numberedListStart.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/numberedListStart.md new file mode 100644 index 0000000000..bb4415d690 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/numberedListStart.md @@ -0,0 +1,2 @@ +5. Item 5 +6. Item 6 diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/pageBreak/basic.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/pageBreak/basic.md index e69de29bb2..8b13789179 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/pageBreak/basic.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/pageBreak/basic.md @@ -0,0 +1 @@ + diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/empty.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/empty.md index e69de29bb2..8b13789179 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/empty.md +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/empty.md @@ -0,0 +1 @@ + diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/multiple.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/multiple.md new file mode 100644 index 0000000000..8fadfa1d86 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/multiple.md @@ -0,0 +1,5 @@ +First paragraph + +Second paragraph + +Third paragraph diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/basic.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/basic.md new file mode 100644 index 0000000000..83d6a8096d --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/basic.md @@ -0,0 +1 @@ +> This is a quote diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/multiple.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/multiple.md new file mode 100644 index 0000000000..c2610d0ba7 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/multiple.md @@ -0,0 +1,3 @@ +> First quote + +> Second quote diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/nested.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/nested.md new file mode 100644 index 0000000000..41c50517f2 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/nested.md @@ -0,0 +1,3 @@ +> Parent quote + +Nested paragraph diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/styled.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/styled.md new file mode 100644 index 0000000000..71e0af0173 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/styled.md @@ -0,0 +1 @@ +> **Bold** and *italic* quote diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/withLink.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/withLink.md new file mode 100644 index 0000000000..8510d4defd --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/withLink.md @@ -0,0 +1 @@ +> Quote with [a link](https://www.example.com) diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/backgroundColor.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/backgroundColor.md new file mode 100644 index 0000000000..3ba8964656 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/backgroundColor.md @@ -0,0 +1 @@ +Highlighted text diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/bold.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/bold.md new file mode 100644 index 0000000000..df2474d633 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/bold.md @@ -0,0 +1 @@ +**Bold text** diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/boldItalicStrike.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/boldItalicStrike.md new file mode 100644 index 0000000000..1af450cf5e --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/boldItalicStrike.md @@ -0,0 +1 @@ +***~~All styles~~*** diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/code.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/code.md new file mode 100644 index 0000000000..aa4775ec76 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/code.md @@ -0,0 +1 @@ +`Inline code` diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/combined.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/combined.md new file mode 100644 index 0000000000..b011bd3c15 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/combined.md @@ -0,0 +1 @@ +***Bold and italic*** diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/italic.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/italic.md new file mode 100644 index 0000000000..c6c83dc114 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/italic.md @@ -0,0 +1 @@ +*Italic text* diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/mixedInParagraph.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/mixedInParagraph.md new file mode 100644 index 0000000000..76bd55f326 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/mixedInParagraph.md @@ -0,0 +1 @@ +Normal **bold** *italic* `code `~~strike~~ diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/strike.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/strike.md new file mode 100644 index 0000000000..afe555a038 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/strike.md @@ -0,0 +1 @@ +~~Strikethrough text~~ diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/textColor.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/textColor.md new file mode 100644 index 0000000000..28f332d788 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/textColor.md @@ -0,0 +1 @@ +Colored text diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/underline.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/underline.md new file mode 100644 index 0000000000..2ccc77398c --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/underline.md @@ -0,0 +1 @@ +Underline text diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/advancedExample.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/advancedExample.md new file mode 100644 index 0000000000..53599a4feb --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/advancedExample.md @@ -0,0 +1,6 @@ +| This row has headers | This is **RED** | Text is Blue | +| -------------------------------- | --------------- | ------------------ | +| This spans 2 columns\ +and 2 rows | | Sooo many features | +| | | | +| A cell | Another Cell | Aligned center | diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/cellTextAlignment.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/cellTextAlignment.md new file mode 100644 index 0000000000..d3d4211640 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/cellTextAlignment.md @@ -0,0 +1,3 @@ +| | | | +| ---------- | ---------- | ---------- | +| Left | Center | Right | diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/emptyCells.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/emptyCells.md new file mode 100644 index 0000000000..ba81ea2ca9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/emptyCells.md @@ -0,0 +1,4 @@ +| | | +| ----------- | ---------------- | +| Has content | | +| | Also has content | diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/hardBreakInCell.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/hardBreakInCell.md new file mode 100644 index 0000000000..d9ffaf65a3 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/hardBreakInCell.md @@ -0,0 +1,4 @@ +| | | +| -------------- | ----------- | +| Line 1\ +Line 2 | Normal cell | diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/headerRowsAndCols.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/headerRowsAndCols.md new file mode 100644 index 0000000000..1c29ad9f94 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/headerRowsAndCols.md @@ -0,0 +1,3 @@ +| Corner | Column Header 1 | Column Header 2 | +| ------------ | --------------- | --------------- | +| Row Header 1 | Data 1 | Data 2 | diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/linksInCells.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/linksInCells.md new file mode 100644 index 0000000000..7815cfe199 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/linksInCells.md @@ -0,0 +1,4 @@ +| | | +| ------------------------------------ | ---------------------------- | +| Visit [Example](https://example.com) | Plain cell | +| Data | [Link](https://example2.com) | diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/singleCell.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/singleCell.md new file mode 100644 index 0000000000..3be705c3e8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/singleCell.md @@ -0,0 +1,3 @@ +| | +| ---------- | +| Only cell | diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/styledCellContent.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/styledCellContent.md new file mode 100644 index 0000000000..ff5eff81a2 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/styledCellContent.md @@ -0,0 +1,4 @@ +| | | +| ---------- | ---------- | +| **Bold** | *Italic* | +| ~~Strike~~ | `Code` | diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/video/withCaption.md b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/video/withCaption.md new file mode 100644 index 0000000000..78cb313b4b --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/markdown/video/withCaption.md @@ -0,0 +1 @@ +
Video caption
diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/basic.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/basic.json new file mode 100644 index 0000000000..32e28f6803 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/basic.json @@ -0,0 +1,20 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "caption": "", + "name": "example", + "showPreview": true, + "url": "https://example.com/audio.mp3", + }, + "type": "audio", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/button.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/button.json new file mode 100644 index 0000000000..2149eef7c8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/button.json @@ -0,0 +1,20 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "caption": "", + "name": "", + "showPreview": true, + "url": "", + }, + "type": "audio", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/noName.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/noName.json new file mode 100644 index 0000000000..a9b3396ea4 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/noName.json @@ -0,0 +1,20 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "caption": "", + "name": "", + "showPreview": true, + "url": "https://example.com/audio.mp3", + }, + "type": "audio", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/complex/document.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/complex/document.json new file mode 100644 index 0000000000..0eb89bbcda --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/complex/document.json @@ -0,0 +1,219 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Document Title", + "type": "text", + }, + ], + "type": "heading", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "2", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Introduction paragraph.", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "3", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "isToggleable": false, + "level": 2, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Section 1", + "type": "text", + }, + ], + "type": "heading", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "4", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Text with ", + "type": "text", + }, + { + "marks": [ + { + "type": "bold", + }, + ], + "text": "bold", + "type": "text", + }, + { + "text": " and ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "href": "https://example.com", + }, + "type": "link", + }, + ], + "text": "a link", + "type": "text", + }, + { + "text": ".", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "5", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "First point", + "type": "text", + }, + ], + "type": "bulletListItem", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "6", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Second point", + "type": "text", + }, + ], + "type": "bulletListItem", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "7", + }, + "content": [ + { + "type": "divider", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "8", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textColor": "default", + }, + "content": [ + { + "text": "A notable quote", + "type": "text", + }, + ], + "type": "quote", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "9", + }, + "content": [ + { + "attrs": { + "language": "javascript", + }, + "content": [ + { + "text": "const x = 42;", + "type": "text", + }, + ], + "type": "codeBlock", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/hardbreak/between-links.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/hardbreak/between-links.json index 174eeecd5b..9db2d101f6 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/hardbreak/between-links.json +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/hardbreak/between-links.json @@ -15,10 +15,7 @@ "marks": [ { "attrs": { - "class": null, "href": "https://www.website.com", - "rel": "noopener noreferrer nofollow", - "target": "_blank", }, "type": "link", }, @@ -33,10 +30,7 @@ "marks": [ { "attrs": { - "class": null, "href": "https://www.website2.com", - "rel": "noopener noreferrer nofollow", - "target": "_blank", }, "type": "link", }, diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/hardbreak/link.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/hardbreak/link.json index 4ae3cc342b..7a3369fd10 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/hardbreak/link.json +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/hardbreak/link.json @@ -15,10 +15,7 @@ "marks": [ { "attrs": { - "class": null, "href": "https://www.website.com", - "rel": "noopener noreferrer nofollow", - "target": "_blank", }, "type": "link", }, @@ -33,10 +30,7 @@ "marks": [ { "attrs": { - "class": null, "href": "https://www.website.com", - "rel": "noopener noreferrer nofollow", - "target": "_blank", }, "type": "link", }, diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h1.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h1.json new file mode 100644 index 0000000000..d147b23ade --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h1.json @@ -0,0 +1,26 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Heading 1", + "type": "text", + }, + ], + "type": "heading", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h2.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h2.json new file mode 100644 index 0000000000..f9f92e7081 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h2.json @@ -0,0 +1,26 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "isToggleable": false, + "level": 2, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Heading 2", + "type": "text", + }, + ], + "type": "heading", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h3.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h3.json new file mode 100644 index 0000000000..6399a58563 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h3.json @@ -0,0 +1,26 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "isToggleable": false, + "level": 3, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Heading 3", + "type": "text", + }, + ], + "type": "heading", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h4.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h4.json new file mode 100644 index 0000000000..c23a0c4809 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h4.json @@ -0,0 +1,26 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "isToggleable": false, + "level": 4, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Heading 4", + "type": "text", + }, + ], + "type": "heading", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h5.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h5.json new file mode 100644 index 0000000000..0867b7796f --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h5.json @@ -0,0 +1,26 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "isToggleable": false, + "level": 5, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Heading 5", + "type": "text", + }, + ], + "type": "heading", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h6.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h6.json new file mode 100644 index 0000000000..b5eddefddc --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h6.json @@ -0,0 +1,26 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "isToggleable": false, + "level": 6, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Heading 6", + "type": "text", + }, + ], + "type": "heading", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/styled.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/styled.json new file mode 100644 index 0000000000..ff107bc15f --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/styled.json @@ -0,0 +1,35 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "type": "bold", + }, + ], + "text": "Bold ", + "type": "text", + }, + { + "text": "Heading", + "type": "text", + }, + ], + "type": "heading", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/toggleable.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/toggleable.json new file mode 100644 index 0000000000..7bb4c57f86 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/toggleable.json @@ -0,0 +1,53 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "isToggleable": true, + "level": 2, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Toggle Heading", + "type": "text", + }, + ], + "type": "heading", + }, + { + "content": [ + { + "attrs": { + "id": "2", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Child content", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "blockGroup", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image/urlOnly.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image/urlOnly.json new file mode 100644 index 0000000000..3d56a77d23 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image/urlOnly.json @@ -0,0 +1,22 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "caption": "", + "name": "", + "previewWidth": undefined, + "showPreview": true, + "textAlignment": "left", + "url": "exampleURL", + }, + "type": "image", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image/withCaption.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image/withCaption.json new file mode 100644 index 0000000000..5016f16a10 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image/withCaption.json @@ -0,0 +1,22 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "caption": "This is a caption", + "name": "Example Image", + "previewWidth": undefined, + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/image.png", + }, + "type": "image", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/adjacent.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/adjacent.json index d546271743..f3020cdf7f 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/adjacent.json +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/adjacent.json @@ -15,10 +15,7 @@ "marks": [ { "attrs": { - "class": null, "href": "https://www.website.com", - "rel": "noopener noreferrer nofollow", - "target": "_blank", }, "type": "link", }, @@ -30,10 +27,7 @@ "marks": [ { "attrs": { - "class": null, "href": "https://www.website2.com", - "rel": "noopener noreferrer nofollow", - "target": "_blank", }, "type": "link", }, diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/basic.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/basic.json index 3964520c13..b85306efc8 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/basic.json +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/basic.json @@ -15,10 +15,7 @@ "marks": [ { "attrs": { - "class": null, "href": "https://www.website.com", - "rel": "noopener noreferrer nofollow", - "target": "_blank", }, "type": "link", }, diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/plainUrl.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/plainUrl.json new file mode 100644 index 0000000000..dc5aa26397 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/plainUrl.json @@ -0,0 +1,32 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "attrs": { + "href": "https://www.website.com", + }, + "type": "link", + }, + ], + "text": "https://www.website.com", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/styled.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/styled.json index 84c3a57c95..2c01e6c1a1 100644 --- a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/styled.json +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/styled.json @@ -18,10 +18,7 @@ }, { "attrs": { - "class": null, "href": "https://www.website.com", - "rel": "noopener noreferrer nofollow", - "target": "_blank", }, "type": "link", }, @@ -33,10 +30,7 @@ "marks": [ { "attrs": { - "class": null, "href": "https://www.website.com", - "rel": "noopener noreferrer nofollow", - "target": "_blank", }, "type": "link", }, diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/urlWithParens.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/urlWithParens.json new file mode 100644 index 0000000000..4de7926c89 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/urlWithParens.json @@ -0,0 +1,32 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "attrs": { + "href": "https://en.wikipedia.org/wiki/Example_(disambiguation)", + }, + "type": "link", + }, + ], + "text": "Example", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/withCode.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/withCode.json new file mode 100644 index 0000000000..bf1f1da31f --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/withCode.json @@ -0,0 +1,49 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "See the ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "href": "https://example.com", + }, + "type": "link", + }, + ], + "text": "docs", + "type": "text", + }, + { + "text": " for ", + "type": "text", + }, + { + "marks": [ + { + "type": "code", + }, + ], + "text": "config", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/lists/numberedListStart.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/lists/numberedListStart.json new file mode 100644 index 0000000000..387b4fa073 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/lists/numberedListStart.json @@ -0,0 +1,48 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "start": 5, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Item 5", + "type": "text", + }, + ], + "type": "numberedListItem", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "2", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "start": undefined, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Item 6", + "type": "text", + }, + ], + "type": "numberedListItem", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/paragraph/multiple.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/paragraph/multiple.json new file mode 100644 index 0000000000..affba0772c --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/paragraph/multiple.json @@ -0,0 +1,68 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "First paragraph", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "2", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Second paragraph", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "3", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Third paragraph", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/basic.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/basic.json new file mode 100644 index 0000000000..9234a8f05f --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/basic.json @@ -0,0 +1,23 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textColor": "default", + }, + "content": [ + { + "text": "This is a quote", + "type": "text", + }, + ], + "type": "quote", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/multiple.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/multiple.json new file mode 100644 index 0000000000..458e24879a --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/multiple.json @@ -0,0 +1,44 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textColor": "default", + }, + "content": [ + { + "text": "First quote", + "type": "text", + }, + ], + "type": "quote", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "id": "2", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textColor": "default", + }, + "content": [ + { + "text": "Second quote", + "type": "text", + }, + ], + "type": "quote", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/nested.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/nested.json new file mode 100644 index 0000000000..b3c46ed220 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/nested.json @@ -0,0 +1,50 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textColor": "default", + }, + "content": [ + { + "text": "Parent quote", + "type": "text", + }, + ], + "type": "quote", + }, + { + "content": [ + { + "attrs": { + "id": "2", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Nested paragraph", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "blockGroup", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/styled.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/styled.json new file mode 100644 index 0000000000..a482c2ad58 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/styled.json @@ -0,0 +1,45 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "type": "bold", + }, + ], + "text": "Bold ", + "type": "text", + }, + { + "text": "and ", + "type": "text", + }, + { + "marks": [ + { + "type": "italic", + }, + ], + "text": "italic", + "type": "text", + }, + { + "text": " quote", + "type": "text", + }, + ], + "type": "quote", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/withLink.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/withLink.json new file mode 100644 index 0000000000..c0e2857679 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/withLink.json @@ -0,0 +1,35 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textColor": "default", + }, + "content": [ + { + "text": "Quote with ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "href": "https://www.example.com", + }, + "type": "link", + }, + ], + "text": "a link", + "type": "text", + }, + ], + "type": "quote", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/backgroundColor.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/backgroundColor.json new file mode 100644 index 0000000000..e07954b219 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/backgroundColor.json @@ -0,0 +1,32 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "attrs": { + "stringValue": "blue", + }, + "type": "backgroundColor", + }, + ], + "text": "Highlighted text", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/bold.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/bold.json new file mode 100644 index 0000000000..b6d5b7a208 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/bold.json @@ -0,0 +1,29 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "type": "bold", + }, + ], + "text": "Bold text", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/boldItalicStrike.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/boldItalicStrike.json new file mode 100644 index 0000000000..7c5f05d763 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/boldItalicStrike.json @@ -0,0 +1,35 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "type": "bold", + }, + { + "type": "italic", + }, + { + "type": "strike", + }, + ], + "text": "All styles", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/code.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/code.json new file mode 100644 index 0000000000..2cf0463e03 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/code.json @@ -0,0 +1,29 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "type": "code", + }, + ], + "text": "Inline code", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/combined.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/combined.json new file mode 100644 index 0000000000..5b91fb8abb --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/combined.json @@ -0,0 +1,32 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "type": "bold", + }, + { + "type": "italic", + }, + ], + "text": "Bold and italic", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/italic.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/italic.json new file mode 100644 index 0000000000..7d22809e81 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/italic.json @@ -0,0 +1,29 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "type": "italic", + }, + ], + "text": "Italic text", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/mixedInParagraph.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/mixedInParagraph.json new file mode 100644 index 0000000000..d72aa1d3bb --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/mixedInParagraph.json @@ -0,0 +1,60 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Normal ", + "type": "text", + }, + { + "marks": [ + { + "type": "bold", + }, + ], + "text": "bold ", + "type": "text", + }, + { + "marks": [ + { + "type": "italic", + }, + ], + "text": "italic ", + "type": "text", + }, + { + "marks": [ + { + "type": "code", + }, + ], + "text": "code ", + "type": "text", + }, + { + "marks": [ + { + "type": "strike", + }, + ], + "text": "strike", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/strike.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/strike.json new file mode 100644 index 0000000000..756569ade9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/strike.json @@ -0,0 +1,29 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "type": "strike", + }, + ], + "text": "Strikethrough text", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/textColor.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/textColor.json new file mode 100644 index 0000000000..7bca812fd7 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/textColor.json @@ -0,0 +1,32 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "attrs": { + "stringValue": "red", + }, + "type": "textColor", + }, + ], + "text": "Colored text", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/underline.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/underline.json new file mode 100644 index 0000000000..348c2ae742 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/underline.json @@ -0,0 +1,29 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "marks": [ + { + "type": "underline", + }, + ], + "text": "Underline text", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/advancedExample.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/advancedExample.json new file mode 100644 index 0000000000..1fdde23a6b --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/advancedExample.json @@ -0,0 +1,265 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": [ + 199, + ], + "rowspan": 1, + "textAlignment": "center", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "This row has headers", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableHeader", + }, + { + "attrs": { + "backgroundColor": "red", + "colspan": 1, + "colwidth": [ + 148, + ], + "rowspan": 1, + "textAlignment": "center", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "This is ", + "type": "text", + }, + { + "marks": [ + { + "type": "bold", + }, + ], + "text": "RED", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableHeader", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": [ + 201, + ], + "rowspan": 1, + "textAlignment": "center", + "textColor": "blue", + }, + "content": [ + { + "content": [ + { + "text": "Text is Blue", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableHeader", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "backgroundColor": "yellow", + "colspan": 2, + "colwidth": [ + 199, + 148, + ], + "rowspan": 2, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "This spans 2 columns", + "type": "text", + }, + { + "type": "hardBreak", + }, + { + "text": "and 2 rows", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "gray", + "colspan": 1, + "colwidth": [ + 201, + ], + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Sooo many features", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "backgroundColor": "gray", + "colspan": 1, + "colwidth": [ + 201, + ], + "rowspan": 1, + "textAlignment": "left", + "textColor": "purple", + }, + "content": [ + { + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": [ + 199, + ], + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "A cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": [ + 148, + ], + "rowspan": 1, + "textAlignment": "right", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Another Cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": [ + 201, + ], + "rowspan": 1, + "textAlignment": "center", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Aligned center", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/cellTextAlignment.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/cellTextAlignment.json new file mode 100644 index 0000000000..c7ded0cfc6 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/cellTextAlignment.json @@ -0,0 +1,89 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Left", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "center", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Center", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "right", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Right", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/emptyCells.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/emptyCells.json new file mode 100644 index 0000000000..05bb8aa0af --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/emptyCells.json @@ -0,0 +1,104 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Has content", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Also has content", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/hardBreakInCell.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/hardBreakInCell.json new file mode 100644 index 0000000000..121834448d --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/hardBreakInCell.json @@ -0,0 +1,74 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Line 1", + "type": "text", + }, + { + "type": "hardBreak", + }, + { + "text": "Line 2", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Normal cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/headerRowsAndCols.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/headerRowsAndCols.json new file mode 100644 index 0000000000..606ec05c49 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/headerRowsAndCols.json @@ -0,0 +1,160 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Corner", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableHeader", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Column Header 1", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableHeader", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Column Header 2", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableHeader", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Row Header 1", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableHeader", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Data 1", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Data 2", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/linksInCells.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/linksInCells.json new file mode 100644 index 0000000000..6f491b4f48 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/linksInCells.json @@ -0,0 +1,136 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Visit ", + "type": "text", + }, + { + "marks": [ + { + "attrs": { + "href": "https://example.com", + }, + "type": "link", + }, + ], + "text": "Example", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Plain cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Data", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "marks": [ + { + "attrs": { + "href": "https://example2.com", + }, + "type": "link", + }, + ], + "text": "Link", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/singleCell.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/singleCell.json new file mode 100644 index 0000000000..dd4628d177 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/singleCell.json @@ -0,0 +1,45 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "text": "Only cell", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/styledCellContent.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/styledCellContent.json new file mode 100644 index 0000000000..3727fa1cf0 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/styledCellContent.json @@ -0,0 +1,136 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "marks": [ + { + "type": "bold", + }, + ], + "text": "Bold", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "marks": [ + { + "type": "italic", + }, + ], + "text": "Italic", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "marks": [ + { + "type": "strike", + }, + ], + "text": "Strike", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + { + "attrs": { + "backgroundColor": "default", + "colspan": 1, + "colwidth": null, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "content": [ + { + "marks": [ + { + "type": "code", + }, + ], + "text": "Code", + "type": "text", + }, + ], + "type": "tableParagraph", + }, + ], + "type": "tableCell", + }, + ], + "type": "tableRow", + }, + ], + "type": "table", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/video/withCaption.json b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/video/withCaption.json new file mode 100644 index 0000000000..6d6c134eb0 --- /dev/null +++ b/tests/src/unit/core/formatConversion/export/__snapshots__/nodes/video/withCaption.json @@ -0,0 +1,22 @@ +[ + { + "attrs": { + "id": "1", + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "caption": "Video caption", + "name": "Example Video", + "previewWidth": undefined, + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/video.mp4", + }, + "type": "video", + }, + ], + "type": "blockContainer", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/export/exportTestInstances.ts b/tests/src/unit/core/formatConversion/export/exportTestInstances.ts index de46704117..bd8cb77493 100644 --- a/tests/src/unit/core/formatConversion/export/exportTestInstances.ts +++ b/tests/src/unit/core/formatConversion/export/exportTestInstances.ts @@ -289,6 +289,653 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, executeTest: testExportBlockNoteHTML, }, + // Heading levels + { + testCase: { + name: "heading/h1", + content: [ + { + type: "heading", + props: { level: 1 }, + content: "Heading 1", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "heading/h2", + content: [ + { + type: "heading", + props: { level: 2 }, + content: "Heading 2", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "heading/h3", + content: [ + { + type: "heading", + props: { level: 3 }, + content: "Heading 3", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "heading/h4", + content: [ + { + type: "heading", + props: { level: 4 }, + content: "Heading 4", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "heading/h5", + content: [ + { + type: "heading", + props: { level: 5 }, + content: "Heading 5", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "heading/h6", + content: [ + { + type: "heading", + props: { level: 6 }, + content: "Heading 6", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "heading/styled", + content: [ + { + type: "heading", + props: { level: 1 }, + content: [ + { + type: "text", + text: "Bold ", + styles: { bold: true }, + }, + { + type: "text", + text: "Heading", + styles: {}, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "heading/toggleable", + content: [ + { + type: "heading", + props: { level: 2, isToggleable: true }, + content: "Toggle Heading", + children: [ + { + type: "paragraph", + content: "Child content", + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Quote / Blockquote + { + testCase: { + name: "quote/basic", + content: [ + { + type: "quote", + content: "This is a quote", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "quote/styled", + content: [ + { + type: "quote", + content: [ + { + type: "text", + text: "Bold ", + styles: { bold: true }, + }, + { + type: "text", + text: "and ", + styles: {}, + }, + { + type: "text", + text: "italic", + styles: { italic: true }, + }, + { + type: "text", + text: " quote", + styles: {}, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "quote/withLink", + content: [ + { + type: "quote", + content: [ + { + type: "text", + text: "Quote with ", + styles: {}, + }, + { + type: "link", + href: "https://www.example.com", + content: "a link", + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "quote/nested", + content: [ + { + type: "quote", + content: "Parent quote", + children: [ + { + type: "paragraph", + content: "Nested paragraph", + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "quote/multiple", + content: [ + { + type: "quote", + content: "First quote", + }, + { + type: "quote", + content: "Second quote", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Audio + { + testCase: { + name: "audio/basic", + content: [ + { + type: "audio", + props: { + url: "https://example.com/audio.mp3", + name: "example", + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "audio/button", + content: [ + { + type: "audio", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "audio/noName", + content: [ + { + type: "audio", + props: { + url: "https://example.com/audio.mp3", + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Individual styles + { + testCase: { + name: "style/bold", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Bold text", + styles: { bold: true }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "style/italic", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Italic text", + styles: { italic: true }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "style/underline", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Underline text", + styles: { underline: true }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "style/strike", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Strikethrough text", + styles: { strike: true }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "style/code", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Inline code", + styles: { code: true }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "style/textColor", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Colored text", + styles: { textColor: "red" }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "style/backgroundColor", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Highlighted text", + styles: { backgroundColor: "blue" }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "style/combined", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Bold and italic", + styles: { bold: true, italic: true }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "style/boldItalicStrike", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "All styles", + styles: { bold: true, italic: true, strike: true }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "style/mixedInParagraph", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Normal ", + styles: {}, + }, + { + type: "text", + text: "bold ", + styles: { bold: true }, + }, + { + type: "text", + text: "italic ", + styles: { italic: true }, + }, + { + type: "text", + text: "code ", + styles: { code: true }, + }, + { + type: "text", + text: "strike", + styles: { strike: true }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Numbered list with custom start + { + testCase: { + name: "lists/numberedListStart", + content: [ + { + type: "numberedListItem", + props: { start: 5 }, + content: "Item 5", + }, + { + type: "numberedListItem", + content: "Item 6", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Multiple paragraphs + { + testCase: { + name: "paragraph/multiple", + content: [ + { + type: "paragraph", + content: "First paragraph", + }, + { + type: "paragraph", + content: "Second paragraph", + }, + { + type: "paragraph", + content: "Third paragraph", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Mixed block types document + { + testCase: { + name: "complex/document", + content: [ + { + type: "heading", + props: { level: 1 }, + content: "Document Title", + }, + { + type: "paragraph", + content: "Introduction paragraph.", + }, + { + type: "heading", + props: { level: 2 }, + content: "Section 1", + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Text with ", + styles: {}, + }, + { + type: "text", + text: "bold", + styles: { bold: true }, + }, + { + type: "text", + text: " and ", + styles: {}, + }, + { + type: "link", + href: "https://example.com", + content: "a link", + }, + { + type: "text", + text: ".", + styles: {}, + }, + ], + }, + { + type: "bulletListItem", + content: "First point", + }, + { + type: "bulletListItem", + content: "Second point", + }, + { + type: "divider", + }, + { + type: "quote", + content: "A notable quote", + }, + { + type: "codeBlock", + props: { language: "javascript" }, + content: "const x = 42;", + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Link with inline code + { + testCase: { + name: "link/withCode", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "See the ", + styles: {}, + }, + { + type: "link", + href: "https://example.com", + content: "docs", + }, + { + type: "text", + text: " for ", + styles: {}, + }, + { + type: "text", + text: "config", + styles: { code: true }, + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Image with caption + { + testCase: { + name: "image/withCaption", + content: [ + { + type: "image", + props: { + url: "https://example.com/image.png", + name: "Example Image", + caption: "This is a caption", + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Video with caption + { + testCase: { + name: "video/withCaption", + content: [ + { + type: "video", + props: { + url: "https://example.com/video.mp4", + name: "Example Video", + caption: "Video caption", + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, { testCase: { name: "divider/basic", @@ -454,6 +1101,23 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, executeTest: testExportBlockNoteHTML, }, + { + // Image with only a URL — no name, no caption. Confirms markdown export + // stays as plain `![](url)` without wrapping in a `
` (the figure + // form is only used to carry caption text through the round-trip). + testCase: { + name: "image/urlOnly", + content: [ + { + type: "image", + props: { + url: "exampleURL", + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, { testCase: { name: "image/noPreview", @@ -491,8 +1155,263 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< caption: "Caption", previewWidth: 256, }, - }, - ], + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "table/basic", + content: [ + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: [ + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + ], + }, + ], + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "table/allColWidths", + content: [ + { + type: "table", + content: { + type: "tableContent", + columnWidths: [100, 200, 300], + rows: [ + { + cells: [ + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + ], + }, + ], + }, }, ], }, @@ -500,12 +1419,13 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { testCase: { - name: "table/basic", + name: "table/mixedColWidths", content: [ { type: "table", content: { type: "tableContent", + columnWidths: [100, undefined, 300], rows: [ { cells: [ @@ -627,14 +1547,51 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { testCase: { - name: "table/allColWidths", + name: "table/mixedCellColors", content: [ { type: "table", content: { type: "tableContent", - columnWidths: [100, 200, 300], + columnWidths: [100, undefined, 300], rows: [ + { + cells: [ + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "red", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "blue", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "blue", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "yellow", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "red", + }, + }, + ], + }, { cells: [ { @@ -709,6 +1666,49 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, ], }, + ], + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "table/mixedRowspansAndColspans", + content: [ + { + type: "table", + content: { + type: "tableContent", + columnWidths: [100, 200, 300], + rows: [ + { + cells: [ + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "red", + colspan: 2, + rowspan: 1, + textAlignment: "left", + textColor: "blue", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "yellow", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "red", + }, + }, + ], + }, { cells: [ { @@ -717,11 +1717,26 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< props: { backgroundColor: "default", colspan: 1, + rowspan: 2, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 2, rowspan: 1, textAlignment: "left", textColor: "default", }, }, + ], + }, + { + cells: [ { type: "tableCell", content: ["Table Cell"], @@ -755,13 +1770,13 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { testCase: { - name: "table/mixedColWidths", + name: "table/headerRows", content: [ { type: "table", content: { + headerRows: 1, type: "tableContent", - columnWidths: [100, undefined, 300], rows: [ { cells: [ @@ -883,13 +1898,13 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { testCase: { - name: "table/mixedCellColors", + name: "table/headerCols", content: [ { type: "table", content: { + headerCols: 1, type: "tableContent", - columnWidths: [100, undefined, 300], rows: [ { cells: [ @@ -897,18 +1912,18 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< type: "tableCell", content: ["Table Cell"], props: { - backgroundColor: "red", + backgroundColor: "default", colspan: 1, rowspan: 1, textAlignment: "left", - textColor: "blue", + textColor: "default", }, }, { type: "tableCell", content: ["Table Cell"], props: { - backgroundColor: "blue", + backgroundColor: "default", colspan: 1, rowspan: 1, textAlignment: "left", @@ -919,20 +1934,113 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< type: "tableCell", content: ["Table Cell"], props: { - backgroundColor: "yellow", + backgroundColor: "default", colspan: 1, rowspan: 1, textAlignment: "left", - textColor: "red", + textColor: "default", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + { + type: "tableCell", + content: ["Table Cell"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", }, }, ], }, + ], + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Advanced table: header rows + header cols together + { + testCase: { + name: "table/headerRowsAndCols", + content: [ + { + type: "table", + content: { + type: "tableContent", + headerRows: 1, + headerCols: 1, + rows: [ { cells: [ { type: "tableCell", - content: ["Table Cell"], + content: ["Corner"], props: { backgroundColor: "default", colspan: 1, @@ -943,7 +2051,7 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { type: "tableCell", - content: ["Table Cell"], + content: ["Column Header 1"], props: { backgroundColor: "default", colspan: 1, @@ -954,7 +2062,7 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { type: "tableCell", - content: ["Table Cell"], + content: ["Column Header 2"], props: { backgroundColor: "default", colspan: 1, @@ -969,7 +2077,7 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< cells: [ { type: "tableCell", - content: ["Table Cell"], + content: ["Row Header 1"], props: { backgroundColor: "default", colspan: 1, @@ -980,7 +2088,7 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { type: "tableCell", - content: ["Table Cell"], + content: ["Data 1"], props: { backgroundColor: "default", colspan: 1, @@ -991,7 +2099,7 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { type: "tableCell", - content: ["Table Cell"], + content: ["Data 2"], props: { backgroundColor: "default", colspan: 1, @@ -1009,61 +2117,47 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, executeTest: testExportBlockNoteHTML, }, + // Advanced table: styled content in cells { testCase: { - name: "table/mixedRowspansAndColspans", + name: "table/styledCellContent", content: [ { type: "table", content: { type: "tableContent", - columnWidths: [100, 200, 300], rows: [ { cells: [ { type: "tableCell", - content: ["Table Cell"], - props: { - backgroundColor: "red", - colspan: 2, - rowspan: 1, - textAlignment: "left", - textColor: "blue", - }, - }, - { - type: "tableCell", - content: ["Table Cell"], - props: { - backgroundColor: "yellow", - colspan: 1, - rowspan: 1, - textAlignment: "left", - textColor: "red", - }, - }, - ], - }, - { - cells: [ - { - type: "tableCell", - content: ["Table Cell"], + content: [ + { + type: "text", + text: "Bold", + styles: { bold: true }, + }, + ], props: { backgroundColor: "default", colspan: 1, - rowspan: 2, + rowspan: 1, textAlignment: "left", textColor: "default", }, }, { type: "tableCell", - content: ["Table Cell"], + content: [ + { + type: "text", + text: "Italic", + styles: { italic: true }, + }, + ], props: { backgroundColor: "default", - colspan: 2, + colspan: 1, rowspan: 1, textAlignment: "left", textColor: "default", @@ -1075,7 +2169,13 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< cells: [ { type: "tableCell", - content: ["Table Cell"], + content: [ + { + type: "text", + text: "Strike", + styles: { strike: true }, + }, + ], props: { backgroundColor: "default", colspan: 1, @@ -1086,7 +2186,13 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { type: "tableCell", - content: ["Table Cell"], + content: [ + { + type: "text", + text: "Code", + styles: { code: true }, + }, + ], props: { backgroundColor: "default", colspan: 1, @@ -1104,21 +2210,32 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, executeTest: testExportBlockNoteHTML, }, + // Advanced table: links in cells { testCase: { - name: "table/headerRows", + name: "table/linksInCells", content: [ { type: "table", content: { - headerRows: 1, type: "tableContent", rows: [ { cells: [ { type: "tableCell", - content: ["Table Cell"], + content: [ + { + type: "text", + text: "Visit ", + styles: {}, + }, + { + type: "link", + href: "https://example.com", + content: "Example", + }, + ], props: { backgroundColor: "default", colspan: 1, @@ -1129,7 +2246,7 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { type: "tableCell", - content: ["Table Cell"], + content: ["Plain cell"], props: { backgroundColor: "default", colspan: 1, @@ -1138,9 +2255,13 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< textColor: "default", }, }, + ], + }, + { + cells: [ { type: "tableCell", - content: ["Table Cell"], + content: ["Data"], props: { backgroundColor: "default", colspan: 1, @@ -1149,13 +2270,15 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< textColor: "default", }, }, - ], - }, - { - cells: [ { type: "tableCell", - content: ["Table Cell"], + content: [ + { + type: "link", + href: "https://example2.com", + content: "Link", + }, + ], props: { backgroundColor: "default", colspan: 1, @@ -1164,9 +2287,30 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< textColor: "default", }, }, + ], + }, + ], + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Advanced table: empty cells + { + testCase: { + name: "table/emptyCells", + content: [ + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: [ { type: "tableCell", - content: ["Table Cell"], + content: ["Has content"], props: { backgroundColor: "default", colspan: 1, @@ -1177,7 +2321,7 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { type: "tableCell", - content: ["Table Cell"], + content: [], props: { backgroundColor: "default", colspan: 1, @@ -1192,7 +2336,7 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< cells: [ { type: "tableCell", - content: ["Table Cell"], + content: [], props: { backgroundColor: "default", colspan: 1, @@ -1203,7 +2347,7 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { type: "tableCell", - content: ["Table Cell"], + content: ["Also has content"], props: { backgroundColor: "default", colspan: 1, @@ -1212,9 +2356,30 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< textColor: "default", }, }, + ], + }, + ], + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Advanced table: single cell + { + testCase: { + name: "table/singleCell", + content: [ + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: [ { type: "tableCell", - content: ["Table Cell"], + content: ["Only cell"], props: { backgroundColor: "default", colspan: 1, @@ -1232,49 +2397,103 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, executeTest: testExportBlockNoteHTML, }, + // Advanced table: from the advanced-tables example (large merged cells) { testCase: { - name: "table/headerCols", + name: "table/advancedExample", content: [ { type: "table", content: { - headerCols: 1, type: "tableContent", + columnWidths: [199, 148, 201], + headerRows: 1, rows: [ { cells: [ { type: "tableCell", - content: ["Table Cell"], + content: ["This row has headers"], props: { + colspan: 1, + rowspan: 1, backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + }, + { + type: "tableCell", + content: [ + { + type: "text", + text: "This is ", + styles: {}, + }, + { + type: "text", + text: "RED", + styles: { bold: true }, + }, + ], + props: { colspan: 1, rowspan: 1, - textAlignment: "left", + backgroundColor: "red", textColor: "default", + textAlignment: "center", }, }, { type: "tableCell", - content: ["Table Cell"], + content: ["Text is Blue"], props: { - backgroundColor: "default", colspan: 1, rowspan: 1, + backgroundColor: "default", + textColor: "blue", + textAlignment: "center", + }, + }, + ], + }, + { + cells: [ + { + type: "tableCell", + content: ["This spans 2 columns\nand 2 rows"], + props: { + colspan: 2, + rowspan: 2, + backgroundColor: "yellow", + textColor: "default", textAlignment: "left", + }, + }, + { + type: "tableCell", + content: ["Sooo many features"], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "gray", textColor: "default", + textAlignment: "left", }, }, + ], + }, + { + cells: [ { type: "tableCell", - content: ["Table Cell"], + content: [], props: { - backgroundColor: "default", colspan: 1, rowspan: 1, + backgroundColor: "gray", + textColor: "purple", textAlignment: "left", - textColor: "default", }, }, ], @@ -1283,18 +2502,67 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< cells: [ { type: "tableCell", - content: ["Table Cell"], + content: ["A cell"], props: { - backgroundColor: "default", colspan: 1, rowspan: 1, + backgroundColor: "default", + textColor: "default", textAlignment: "left", + }, + }, + { + type: "tableCell", + content: ["Another Cell"], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", textColor: "default", + textAlignment: "right", }, }, { type: "tableCell", - content: ["Table Cell"], + content: ["Aligned center"], + props: { + colspan: 1, + rowspan: 1, + backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + }, + ], + }, + ], + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Advanced table: hard breaks in cells + { + testCase: { + name: "table/hardBreakInCell", + content: [ + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: [ + { + type: "tableCell", + content: [ + { + type: "text", + text: "Line 1\nLine 2", + styles: {}, + }, + ], props: { backgroundColor: "default", colspan: 1, @@ -1305,7 +2573,7 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { type: "tableCell", - content: ["Table Cell"], + content: ["Normal cell"], props: { backgroundColor: "default", colspan: 1, @@ -1316,11 +2584,28 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, ], }, + ], + }, + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + // Advanced table: mixed text alignment per cell + { + testCase: { + name: "table/cellTextAlignment", + content: [ + { + type: "table", + content: { + type: "tableContent", + rows: [ { cells: [ { type: "tableCell", - content: ["Table Cell"], + content: ["Left"], props: { backgroundColor: "default", colspan: 1, @@ -1331,23 +2616,23 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, { type: "tableCell", - content: ["Table Cell"], + content: ["Center"], props: { backgroundColor: "default", colspan: 1, rowspan: 1, - textAlignment: "left", + textAlignment: "center", textColor: "default", }, }, { type: "tableCell", - content: ["Table Cell"], + content: ["Right"], props: { backgroundColor: "default", colspan: 1, rowspan: 1, - textAlignment: "left", + textAlignment: "right", textColor: "default", }, }, @@ -1435,6 +2720,44 @@ export const exportTestInstancesBlockNoteHTML: TestInstance< }, executeTest: testExportBlockNoteHTML, }, + { + testCase: { + name: "link/plainUrl", + content: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "link", + href: "https://www.website.com", + content: "https://www.website.com", + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, + { + testCase: { + name: "link/urlWithParens", + content: [ + { + // id: UniqueID.options.generateID(), + type: "paragraph", + content: [ + { + type: "link", + href: "https://en.wikipedia.org/wiki/Example_(disambiguation)", + content: "Example", + }, + ], + }, + ], + }, + executeTest: testExportBlockNoteHTML, + }, { testCase: { name: "hardbreak/basic", diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/bold.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/bold.json new file mode 100644 index 0000000000..0569abc8a9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/bold.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Bold text", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/boldItalic.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/boldItalic.json new file mode 100644 index 0000000000..f6b0ca8045 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/boldItalic.json @@ -0,0 +1,22 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + "italic": true, + }, + "text": "Bold and italic", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/bulletList.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/bulletList.json new file mode 100644 index 0000000000..b5999d3ec9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/bulletList.json @@ -0,0 +1,36 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Item 1", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Item 2", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/checkList.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/checkList.json new file mode 100644 index 0000000000..3f344bf122 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/checkList.json @@ -0,0 +1,38 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Unchecked", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "checked": false, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Checked", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "checked": true, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/codeBlock.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/codeBlock.json new file mode 100644 index 0000000000..63ebc503a5 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/codeBlock.json @@ -0,0 +1,17 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "const x = 42;", + "type": "text", + }, + ], + "id": "1", + "props": { + "language": "javascript", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/complexDocument.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/complexDocument.json new file mode 100644 index 0000000000..570c7e98d5 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/complexDocument.json @@ -0,0 +1,106 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Title", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph with ", + "type": "text", + }, + { + "styles": { + "bold": true, + }, + "text": "bold", + "type": "text", + }, + { + "styles": {}, + "text": " text.", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Bullet 1", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Bullet 2", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": undefined, + "id": "5", + "props": {}, + "type": "divider", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "print('hello')", + "type": "text", + }, + ], + "id": "6", + "props": { + "language": "python", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/deeplyNestedLists.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/deeplyNestedLists.json new file mode 100644 index 0000000000..f81fde4a33 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/deeplyNestedLists.json @@ -0,0 +1,144 @@ +[ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Level 4 numbered", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Level 3 bullet", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Level 2 numbered", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Level 2 sibling", + "type": "text", + }, + ], + "id": "5", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Level 1 bullet", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Deep checklist item", + "type": "text", + }, + ], + "id": "8", + "props": { + "backgroundColor": "default", + "checked": false, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Child of second bullet", + "type": "text", + }, + ], + "id": "7", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Another top-level bullet", + "type": "text", + }, + ], + "id": "6", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/defaultBlocks.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/defaultBlocks.json new file mode 100644 index 0000000000..69bef1f3f3 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/defaultBlocks.json @@ -0,0 +1,474 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Welcome to this demo!", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Blocks:", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Toggle Heading", + "type": "text", + }, + ], + "id": "5", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Quote", + "type": "text", + }, + ], + "id": "6", + "props": { + "backgroundColor": "default", + "textColor": "default", + }, + "type": "quote", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Bullet List Item", + "type": "text", + }, + ], + "id": "7", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Numbered List Item", + "type": "text", + }, + ], + "id": "8", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Check List Item", + "type": "text", + }, + ], + "id": "9", + "props": { + "backgroundColor": "default", + "checked": false, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Toggle List Item", + "type": "text", + }, + ], + "id": "10", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "console.log('Hello, world!');", + "type": "text", + }, + ], + "id": "11", + "props": { + "language": "javascript", + }, + "type": "codeBlock", + }, + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Table Cell", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Table Cell", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Table Cell", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Table Cell", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Table Cell", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Table Cell", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Table Cell", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Table Cell", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Table Cell", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "12", + "props": { + "textColor": "default", + }, + "type": "table", + }, + { + "children": [], + "content": undefined, + "id": "13", + "props": { + "backgroundColor": "default", + "caption": "From https://placehold.co/332x322.jpg", + "name": "", + "showPreview": true, + "textAlignment": "left", + "url": "https://placehold.co/332x322.jpg", + }, + "type": "image", + }, + { + "children": [], + "content": undefined, + "id": "14", + "props": { + "backgroundColor": "default", + "caption": "From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", + "name": "", + "showPreview": true, + "textAlignment": "left", + "url": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", + }, + "type": "video", + }, + { + "children": [], + "content": undefined, + "id": "15", + "props": { + "backgroundColor": "default", + "caption": "From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", + "name": "", + "showPreview": true, + "url": "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", + }, + "type": "audio", + }, + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Inline Content:", + "type": "text", + }, + ], + "id": "16", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + "italic": true, + }, + "text": "Styled Text", + "type": "text", + }, + { + "styles": {}, + "text": " ", + "type": "text", + }, + { + "content": [ + { + "styles": {}, + "text": "Link", + "type": "text", + }, + ], + "href": "https://www.blocknotejs.org", + "type": "link", + }, + ], + "id": "17", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/divider.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/divider.json new file mode 100644 index 0000000000..e944763aea --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/divider.json @@ -0,0 +1,43 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Before", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": undefined, + "id": "2", + "props": {}, + "type": "divider", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "After", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/hardBreak.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/hardBreak.json new file mode 100644 index 0000000000..5626739e88 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/hardBreak.json @@ -0,0 +1,20 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Line 1 + Line 2", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/headingLevels.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/headingLevels.json new file mode 100644 index 0000000000..f1ce124a58 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/headingLevels.json @@ -0,0 +1,59 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 1", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 2", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 2, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 3", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 3, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/image.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/image.json new file mode 100644 index 0000000000..c9a6bddd61 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/image.json @@ -0,0 +1,16 @@ +[ + { + "children": [], + "content": undefined, + "id": "1", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "Example", + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/image.png", + }, + "type": "image", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/inlineCode.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/inlineCode.json new file mode 100644 index 0000000000..6ddd0e59ee --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/inlineCode.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "code": true, + }, + "text": "Code text", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/italic.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/italic.json new file mode 100644 index 0000000000..01ec89cd69 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/italic.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "italic": true, + }, + "text": "Italic text", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/link.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/link.json new file mode 100644 index 0000000000..fabbb2daa3 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/link.json @@ -0,0 +1,35 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Text ", + "type": "text", + }, + { + "content": [ + { + "styles": {}, + "text": "Link", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + { + "styles": {}, + "text": " more text", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/mixedStyles.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/mixedStyles.json new file mode 100644 index 0000000000..7085f4c8b8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/mixedStyles.json @@ -0,0 +1,50 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Normal ", + "type": "text", + }, + { + "styles": { + "bold": true, + }, + "text": "bold", + "type": "text", + }, + { + "styles": {}, + "text": " ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "italic", + "type": "text", + }, + { + "styles": {}, + "text": " ", + "type": "text", + }, + { + "styles": { + "strike": true, + }, + "text": "strike", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/multipleParagraphs.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/multipleParagraphs.json new file mode 100644 index 0000000000..fc70b307a0 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/multipleParagraphs.json @@ -0,0 +1,36 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "First paragraph", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Second paragraph", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/nestedLists.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/nestedLists.json new file mode 100644 index 0000000000..90bb306cb8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/nestedLists.json @@ -0,0 +1,54 @@ +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Child 1", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Child 2", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Parent", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/numberedList.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/numberedList.json new file mode 100644 index 0000000000..f2dfd7912e --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/numberedList.json @@ -0,0 +1,36 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Item 1", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Item 2", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/paragraph.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/paragraph.json new file mode 100644 index 0000000000..575bc9876a --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/paragraph.json @@ -0,0 +1,19 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Simple paragraph", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/quote.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/quote.json new file mode 100644 index 0000000000..7a2b3b4601 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/quote.json @@ -0,0 +1,18 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "A quote", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textColor": "default", + }, + "type": "quote", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/specialCharEscaping.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/specialCharEscaping.json new file mode 100644 index 0000000000..0ede1c2000 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/specialCharEscaping.json @@ -0,0 +1,124 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Literal ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "asterisks", + "type": "text", + }, + { + "styles": {}, + "text": " and ", + "type": "text", + }, + { + "styles": { + "bold": true, + }, + "text": "double asterisks", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Backticks ` in plain text and ", + "type": "text", + }, + { + "styles": { + "code": true, + }, + "text": "double", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Underscores ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "here", + "type": "text", + }, + { + "styles": {}, + "text": " and ~tildes~ and [brackets]", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Pipes | and backslash \ and #hash at start", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "const x = `template ${literal}`; +const y = '```triple backticks```';", + "type": "text", + }, + ], + "id": "5", + "props": { + "language": "text", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/strike.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/strike.json new file mode 100644 index 0000000000..7f504a4b3f --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/strike.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "strike": true, + }, + "text": "Strikethrough text", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/table.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/table.json new file mode 100644 index 0000000000..7fb20ea004 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/table.json @@ -0,0 +1,97 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Header 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Header 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/tableWithHeaderRow.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/tableWithHeaderRow.json new file mode 100644 index 0000000000..7944f66a18 --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/tableWithHeaderRow.json @@ -0,0 +1,97 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": 1, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Header 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Header 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/video.json b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/video.json new file mode 100644 index 0000000000..57607adfdf --- /dev/null +++ b/tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/video.json @@ -0,0 +1,16 @@ +[ + { + "children": [], + "content": undefined, + "id": "1", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "Example", + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/video.mp4", + }, + "type": "video", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts b/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts index 57120eb518..b9dcba4828 100644 --- a/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts +++ b/tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts @@ -2,6 +2,7 @@ import { ExportParseEqualityTestCase } from "../../../shared/formatConversion/ex import { testExportParseEqualityBlockNoteHTML, testExportParseEqualityHTML, + testExportParseEqualityMarkdown, } from "../../../shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.js"; import { TestInstance } from "../../../types.js"; import { @@ -371,3 +372,723 @@ export const exportParseEqualityTestInstancesHTML: TestInstance< executeTest: testExportParseEqualityHTML, }, ]; + +// Markdown round-trip tests: blocks → markdown → blocks +// Markdown is a lossy format (no colors, underline, alignment), so these tests +// use snapshot matching to capture the expected round-trip result rather than +// strict equality with the input. This is critical for verifying that the +// custom markdown parser/serializer produces the same round-trip results. +export const exportParseEqualityTestInstancesMarkdown: TestInstance< + ExportParseEqualityTestCase< + TestBlockSchema, + TestInlineContentSchema, + TestStyleSchema + >, + TestBlockSchema, + TestInlineContentSchema, + TestStyleSchema +>[] = [ + { + testCase: { + name: "markdown/paragraph", + content: [ + { + type: "paragraph", + content: "Simple paragraph", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/multipleParagraphs", + content: [ + { + type: "paragraph", + content: "First paragraph", + }, + { + type: "paragraph", + content: "Second paragraph", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/headingLevels", + content: [ + { + type: "heading", + props: { level: 1 }, + content: "Heading 1", + }, + { + type: "heading", + props: { level: 2 }, + content: "Heading 2", + }, + { + type: "heading", + props: { level: 3 }, + content: "Heading 3", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/bulletList", + content: [ + { + type: "bulletListItem", + content: "Item 1", + }, + { + type: "bulletListItem", + content: "Item 2", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/numberedList", + content: [ + { + type: "numberedListItem", + content: "Item 1", + }, + { + type: "numberedListItem", + content: "Item 2", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/checkList", + content: [ + { + type: "checkListItem", + content: "Unchecked", + }, + { + type: "checkListItem", + props: { checked: true }, + content: "Checked", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/nestedLists", + content: [ + { + type: "bulletListItem", + content: "Parent", + children: [ + { + type: "numberedListItem", + content: "Child 1", + }, + { + type: "numberedListItem", + content: "Child 2", + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/codeBlock", + content: [ + { + type: "codeBlock", + props: { language: "javascript" }, + content: "const x = 42;", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/divider", + content: [ + { + type: "paragraph", + content: "Before", + }, + { + type: "divider", + }, + { + type: "paragraph", + content: "After", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/bold", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Bold text", + styles: { bold: true }, + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/italic", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Italic text", + styles: { italic: true }, + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/strike", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Strikethrough text", + styles: { strike: true }, + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/inlineCode", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Code text", + styles: { code: true }, + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/boldItalic", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Bold and italic", + styles: { bold: true, italic: true }, + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/link", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Text ", + styles: {}, + }, + { + type: "link", + content: "Link", + href: "https://example.com", + }, + { + type: "text", + text: " more text", + styles: {}, + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/image", + content: [ + { + type: "image", + props: { + url: "https://example.com/image.png", + name: "Example", + }, + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/video", + content: [ + { + type: "video", + props: { + url: "https://example.com/video.mp4", + name: "Example", + }, + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/table", + content: [ + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: ["Header 1", "Header 2"], + }, + { + cells: ["Cell 1", "Cell 2"], + }, + ], + }, + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + // Complementary check for https://github.com/TypeCellOS/BlockNote/issues/739: + // a table WITH a real header row must round-trip with the header preserved + // (i.e. non-empty headers must not be treated as the empty-header case). + testCase: { + name: "markdown/tableWithHeaderRow", + content: [ + { + type: "table", + content: { + type: "tableContent", + headerRows: 1, + rows: [ + { + cells: ["Header 1", "Header 2"], + }, + { + cells: ["Cell 1", "Cell 2"], + }, + ], + }, + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/quote", + content: [ + { + type: "quote", + content: "A quote", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/hardBreak", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Line 1\nLine 2", + styles: {}, + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/mixedStyles", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Normal ", + styles: {}, + }, + { + type: "text", + text: "bold ", + styles: { bold: true }, + }, + { + type: "text", + text: "italic ", + styles: { italic: true }, + }, + { + type: "text", + text: "strike", + styles: { strike: true }, + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/complexDocument", + content: [ + { + type: "heading", + props: { level: 1 }, + content: "Title", + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Paragraph with ", + styles: {}, + }, + { + type: "text", + text: "bold", + styles: { bold: true }, + }, + { + type: "text", + text: " text.", + styles: {}, + }, + ], + }, + { + type: "bulletListItem", + content: "Bullet 1", + }, + { + type: "bulletListItem", + content: "Bullet 2", + }, + { + type: "divider", + }, + { + type: "codeBlock", + props: { language: "python" }, + content: "print('hello')", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/deeplyNestedLists", + content: [ + { + type: "bulletListItem", + content: "Level 1 bullet", + children: [ + { + type: "numberedListItem", + content: "Level 2 numbered", + children: [ + { + type: "bulletListItem", + content: "Level 3 bullet", + children: [ + { + type: "numberedListItem", + content: "Level 4 numbered", + }, + ], + }, + ], + }, + { + type: "numberedListItem", + content: "Level 2 sibling", + }, + ], + }, + { + type: "bulletListItem", + content: "Another top-level bullet", + children: [ + { + type: "bulletListItem", + content: "Child of second bullet", + children: [ + { + type: "checkListItem", + content: "Deep checklist item", + }, + ], + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + testCase: { + name: "markdown/specialCharEscaping", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Literal *asterisks* and **double asterisks**", + styles: {}, + }, + ], + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Backticks ` in plain text and `` double ``", + styles: {}, + }, + ], + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Underscores _here_ and ~tildes~ and [brackets]", + styles: {}, + }, + ], + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Pipes | and backslash \\ and #hash at start", + styles: {}, + }, + ], + }, + { + type: "codeBlock", + props: { language: "" }, + // eslint-disable-next-line no-template-curly-in-string + content: "const x = `template ${literal}`;\nconst y = '```triple backticks```';", + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, + { + // Mirrors the default-blocks demo (examples/01-basic/04-default-blocks) + // so we get a single round-trip snapshot covering every default block + // type. Markdown is lossy (colors/alignment/file blocks/toggle + // affordances are dropped), so the snapshot documents what survives. + // Image/video/audio captions are preserved via raw `
` HTML. + testCase: { + name: "markdown/defaultBlocks", + content: [ + { + type: "paragraph", + content: "Welcome to this demo!", + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Blocks:", + styles: { bold: true }, + }, + ], + }, + { + type: "paragraph", + content: "Paragraph", + }, + { + type: "heading", + content: "Heading", + }, + { + type: "heading", + props: { isToggleable: true }, + content: "Toggle Heading", + }, + { + type: "quote", + content: "Quote", + }, + { + type: "bulletListItem", + content: "Bullet List Item", + }, + { + type: "numberedListItem", + content: "Numbered List Item", + }, + { + type: "checkListItem", + content: "Check List Item", + }, + { + type: "toggleListItem", + content: "Toggle List Item", + }, + { + type: "codeBlock", + props: { language: "javascript" }, + content: "console.log('Hello, world!');", + }, + { + type: "table", + content: { + type: "tableContent", + rows: [ + { cells: ["Table Cell", "Table Cell", "Table Cell"] }, + { cells: ["Table Cell", "Table Cell", "Table Cell"] }, + { cells: ["Table Cell", "Table Cell", "Table Cell"] }, + ], + }, + }, + { + type: "file", + }, + { + type: "image", + props: { + url: "https://placehold.co/332x322.jpg", + caption: "From https://placehold.co/332x322.jpg", + }, + }, + { + type: "video", + props: { + url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", + caption: + "From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", + }, + }, + { + type: "audio", + props: { + url: "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", + caption: + "From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", + }, + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Inline Content:", + styles: { bold: true }, + }, + ], + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Styled Text", + styles: { + bold: true, + italic: true, + textColor: "red", + backgroundColor: "blue", + }, + }, + { + type: "text", + text: " ", + styles: {}, + }, + { + type: "link", + content: "Link", + href: "https://www.blocknotejs.org", + }, + ], + }, + ], + }, + executeTest: testExportParseEqualityMarkdown, + }, +]; diff --git a/tests/src/unit/core/formatConversion/exportParseEquality/runTests.test.ts b/tests/src/unit/core/formatConversion/exportParseEquality/runTests.test.ts index fc7f33f3c8..bcbad6fea9 100644 --- a/tests/src/unit/core/formatConversion/exportParseEquality/runTests.test.ts +++ b/tests/src/unit/core/formatConversion/exportParseEquality/runTests.test.ts @@ -5,6 +5,7 @@ import { testSchema } from "../../testSchema.js"; import { exportParseEqualityTestInstancesBlockNoteHTML, exportParseEqualityTestInstancesHTML, + exportParseEqualityTestInstancesMarkdown, } from "./exportParseEqualityTestInstances.js"; // Tests for verifying that exporting blocks to another format, then importing @@ -36,3 +37,16 @@ describe("Export/parse equality tests (HTML)", () => { }); } }); + +describe("Export/parse equality tests (Markdown)", () => { + const getEditor = createTestEditor(testSchema); + + for (const { + testCase, + executeTest, + } of exportParseEqualityTestInstancesMarkdown) { + it(`${testCase.name}`, async () => { + await executeTest(getEditor(), testCase); + }); + } +}); diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/adjacentFormattedRuns.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/adjacentFormattedRuns.json new file mode 100644 index 0000000000..24ea75b41d --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/adjacentFormattedRuns.json @@ -0,0 +1,35 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "bold", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "italic", + "type": "text", + }, + { + "styles": { + "strike": true, + }, + "text": "strike", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/adjacentLinks.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/adjacentLinks.json new file mode 100644 index 0000000000..bdaf169421 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/adjacentLinks.json @@ -0,0 +1,36 @@ +[ + { + "children": [], + "content": [ + { + "content": [ + { + "styles": {}, + "text": "Link1", + "type": "text", + }, + ], + "href": "https://example1.com", + "type": "link", + }, + { + "content": [ + { + "styles": {}, + "text": "Link2", + "type": "text", + }, + ], + "href": "https://example2.com", + "type": "link", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/backslashEscapes.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/backslashEscapes.json new file mode 100644 index 0000000000..fbdb14c852 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/backslashEscapes.json @@ -0,0 +1,19 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "*not bold* [not a link] ~not strike~", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/bareAngleBrackets.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/bareAngleBrackets.json new file mode 100644 index 0000000000..b50ace7d9e --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/bareAngleBrackets.json @@ -0,0 +1,19 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "1 < 2 and 3 > 0", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockHtmlComment.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockHtmlComment.json new file mode 100644 index 0000000000..dde187fb49 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockHtmlComment.json @@ -0,0 +1,19 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Next paragraph.", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockHtmlDiv.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockHtmlDiv.json new file mode 100644 index 0000000000..76c9d076e7 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockHtmlDiv.json @@ -0,0 +1,19 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "A warning block.", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockHtmlInterruptsParagraph.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockHtmlInterruptsParagraph.json new file mode 100644 index 0000000000..f0b3a0131d --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockHtmlInterruptsParagraph.json @@ -0,0 +1,36 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Some text before.", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "raw block Some text after.", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteLazyContinuation.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteLazyContinuation.json new file mode 100644 index 0000000000..146a54b49b --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteLazyContinuation.json @@ -0,0 +1,20 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "This is a quote + that continues here + and here too", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textColor": "default", + }, + "type": "quote", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteMultiline.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteMultiline.json new file mode 100644 index 0000000000..669e44db40 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteMultiline.json @@ -0,0 +1,20 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Line one + Line two + Line three", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textColor": "default", + }, + "type": "quote", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteWithCode.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteWithCode.json new file mode 100644 index 0000000000..b4a209b413 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteWithCode.json @@ -0,0 +1,30 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Quote with ", + "type": "text", + }, + { + "styles": { + "code": true, + }, + "text": "inline code", + "type": "text", + }, + { + "styles": {}, + "text": " inside", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textColor": "default", + }, + "type": "quote", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteWithLink.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteWithLink.json new file mode 100644 index 0000000000..8cd0e17218 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteWithLink.json @@ -0,0 +1,34 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Quote with ", + "type": "text", + }, + { + "content": [ + { + "styles": {}, + "text": "a link", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + { + "styles": {}, + "text": " inside", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textColor": "default", + }, + "type": "quote", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/boldOnly.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/boldOnly.json new file mode 100644 index 0000000000..0569abc8a9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/boldOnly.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Bold text", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/boldUnderscore.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/boldUnderscore.json new file mode 100644 index 0000000000..fc81db25f4 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/boldUnderscore.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Bold with underscores", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListBasic.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListBasic.json new file mode 100644 index 0000000000..62136bf1c9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListBasic.json @@ -0,0 +1,56 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Unchecked item", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "checked": false, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Checked item", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "checked": true, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Another unchecked", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "checked": false, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListMixed.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListMixed.json new file mode 100644 index 0000000000..4084a42458 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListMixed.json @@ -0,0 +1,55 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Regular bullet", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Check item", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "checked": false, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Checked item", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "checked": true, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListNested.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListNested.json new file mode 100644 index 0000000000..ff5c84bf29 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListNested.json @@ -0,0 +1,57 @@ +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Child checked", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "checked": true, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Child unchecked", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "checked": false, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Parent item", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "checked": false, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockBasic.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockBasic.json new file mode 100644 index 0000000000..cf59869a6f --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockBasic.json @@ -0,0 +1,17 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "console.log('Hello');", + "type": "text", + }, + ], + "id": "1", + "props": { + "language": "text", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockIndented.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockIndented.json new file mode 100644 index 0000000000..475525d84e --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockIndented.json @@ -0,0 +1,17 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "const x = 1;", + "type": "text", + }, + ], + "id": "1", + "props": { + "language": "ts", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockPython.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockPython.json new file mode 100644 index 0000000000..78cd6179bf --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockPython.json @@ -0,0 +1,18 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "def hello(): + print("Hello, world!")", + "type": "text", + }, + ], + "id": "1", + "props": { + "language": "python", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockTildes.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockTildes.json new file mode 100644 index 0000000000..1a656bd726 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockTildes.json @@ -0,0 +1,17 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "code with tildes", + "type": "text", + }, + ], + "id": "1", + "props": { + "language": "text", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockWithLanguage.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockWithLanguage.json new file mode 100644 index 0000000000..90eb554680 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockWithLanguage.json @@ -0,0 +1,18 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "const x = 42; +console.log(x);", + "type": "text", + }, + ], + "id": "1", + "props": { + "language": "javascript", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockWithSpecialChars.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockWithSpecialChars.json new file mode 100644 index 0000000000..bdf7b585f8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockWithSpecialChars.json @@ -0,0 +1,19 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "
+

Hello **not bold**

+
", + "type": "text", + }, + ], + "id": "1", + "props": { + "language": "html", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeSpanWithNewline.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeSpanWithNewline.json new file mode 100644 index 0000000000..b9b3e84746 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeSpanWithNewline.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "code": true, + }, + "text": "foo bar baz", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/complexDocument.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/complexDocument.json new file mode 100644 index 0000000000..972d1d02df --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/complexDocument.json @@ -0,0 +1,442 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Main Title", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "An introduction paragraph with ", + "type": "text", + }, + { + "styles": { + "bold": true, + }, + "text": "bold", + "type": "text", + }, + { + "styles": {}, + "text": " and ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "italic", + "type": "text", + }, + { + "styles": {}, + "text": " text.", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Section 1", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 2, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "First bullet point", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Nested point", + "type": "text", + }, + ], + "id": "6", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Second bullet point", + "type": "text", + }, + ], + "id": "5", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "A notable quote", + "type": "text", + }, + ], + "id": "7", + "props": { + "backgroundColor": "default", + "textColor": "default", + }, + "type": "quote", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Code Example", + "type": "text", + }, + ], + "id": "8", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 3, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "function hello() { + return "world"; +}", + "type": "text", + }, + ], + "id": "9", + "props": { + "language": "javascript", + }, + "type": "codeBlock", + }, + { + "children": [], + "content": undefined, + "id": "10", + "props": {}, + "type": "divider", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Section 2", + "type": "text", + }, + ], + "id": "11", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 2, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Step one", + "type": "text", + }, + ], + "id": "12", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Step two", + "type": "text", + }, + ], + "id": "13", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Step three", + "type": "text", + }, + ], + "id": "14", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": 1, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Feature", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Status", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Bold", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Done", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Italic", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Done", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "15", + "props": { + "textColor": "default", + }, + "type": "table", + }, + { + "children": [], + "content": undefined, + "id": "16", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "Image", + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/image.png", + }, + "type": "image", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Final paragraph with ", + "type": "text", + }, + { + "content": [ + { + "styles": {}, + "text": "a link", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + { + "styles": {}, + "text": ".", + "type": "text", + }, + ], + "id": "17", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/deeplyNestedLists.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/deeplyNestedLists.json new file mode 100644 index 0000000000..535c584553 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/deeplyNestedLists.json @@ -0,0 +1,73 @@ +[ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Level 4", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Level 3", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Level 2", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Level 1", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/emptyString.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/emptyString.json new file mode 100644 index 0000000000..45f0949abe --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/emptyString.json @@ -0,0 +1,13 @@ +[ + { + "children": [], + "content": [], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/escapedDelimiterInEmphasis.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/escapedDelimiterInEmphasis.json new file mode 100644 index 0000000000..219c8e869a --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/escapedDelimiterInEmphasis.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "italic": true, + }, + "text": "*", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakBackslash.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakBackslash.json new file mode 100644 index 0000000000..e4ed206bcf --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakBackslash.json @@ -0,0 +1,20 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Line one + Line two", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakMultiple.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakMultiple.json new file mode 100644 index 0000000000..359a3b1cbc --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakMultiple.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Line one + Line two + Line three", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakTwoSpaces.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakTwoSpaces.json new file mode 100644 index 0000000000..e4ed206bcf --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakTwoSpaces.json @@ -0,0 +1,20 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Line one + Line two", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH1.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH1.json new file mode 100644 index 0000000000..27be2c1d52 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH1.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 1", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH2.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH2.json new file mode 100644 index 0000000000..2cb042a21b --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH2.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 2", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 2, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH3.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH3.json new file mode 100644 index 0000000000..3e79e93fa0 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH3.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 3", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 3, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH4.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH4.json new file mode 100644 index 0000000000..b5dffeaff8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH4.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 4", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 4, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH5.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH5.json new file mode 100644 index 0000000000..9f69aff1a8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH5.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 5", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 5, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH6.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH6.json new file mode 100644 index 0000000000..c01c6b3437 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH6.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 6", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 6, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingInternalPadding.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingInternalPadding.json new file mode 100644 index 0000000000..c61af59e1d --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingInternalPadding.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "foo", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingThenCode.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingThenCode.json new file mode 100644 index 0000000000..d32797e0b3 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingThenCode.json @@ -0,0 +1,36 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Code Section", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 2, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "x = 42", + "type": "text", + }, + ], + "id": "2", + "props": { + "language": "python", + }, + "type": "codeBlock", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingTrailingWhitespace.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingTrailingWhitespace.json new file mode 100644 index 0000000000..4a72b6b759 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingTrailingWhitespace.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "foo", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 3, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingWithInlineStyles.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingWithInlineStyles.json new file mode 100644 index 0000000000..61518fb6d7 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingWithInlineStyles.json @@ -0,0 +1,52 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Bold", + "type": "text", + }, + { + "styles": {}, + "text": " ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "Italic", + "type": "text", + }, + { + "styles": {}, + "text": " ", + "type": "text", + }, + { + "styles": { + "strike": true, + }, + "text": "Strike", + "type": "text", + }, + { + "styles": {}, + "text": " Heading", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleAsterisks.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleAsterisks.json new file mode 100644 index 0000000000..6d2e457fd8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleAsterisks.json @@ -0,0 +1,43 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph above", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": undefined, + "id": "2", + "props": {}, + "type": "divider", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph below", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleDashes.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleDashes.json new file mode 100644 index 0000000000..6d2e457fd8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleDashes.json @@ -0,0 +1,43 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph above", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": undefined, + "id": "2", + "props": {}, + "type": "divider", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph below", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleUnderscores.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleUnderscores.json new file mode 100644 index 0000000000..6d2e457fd8 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleUnderscores.json @@ -0,0 +1,43 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph above", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": undefined, + "id": "2", + "props": {}, + "type": "divider", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph below", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageAngleBracketUrl.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageAngleBracketUrl.json new file mode 100644 index 0000000000..8ecc4b0a3b --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageAngleBracketUrl.json @@ -0,0 +1,16 @@ +[ + { + "children": [], + "content": undefined, + "id": "1", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "alt", + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/image.png", + }, + "type": "image", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageNestedBracketsAlt.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageNestedBracketsAlt.json new file mode 100644 index 0000000000..bd2ec4d1a0 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageNestedBracketsAlt.json @@ -0,0 +1,16 @@ +[ + { + "children": [], + "content": undefined, + "id": "1", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "alt [with] brackets", + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/image.png", + }, + "type": "image", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageWithAlt.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageWithAlt.json new file mode 100644 index 0000000000..7f7ff95459 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageWithAlt.json @@ -0,0 +1,16 @@ +[ + { + "children": [], + "content": undefined, + "id": "1", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "Alt text for image", + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/photo.jpg", + }, + "type": "image", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageWithTitle.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageWithTitle.json new file mode 100644 index 0000000000..a6279cc8a6 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageWithTitle.json @@ -0,0 +1,16 @@ +[ + { + "children": [], + "content": undefined, + "id": "1", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "alt text", + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/image.png", + }, + "type": "image", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineCode.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineCode.json new file mode 100644 index 0000000000..7e02a1406d --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineCode.json @@ -0,0 +1,31 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "This has ", + "type": "text", + }, + { + "styles": { + "code": true, + }, + "text": "inline code", + "type": "text", + }, + { + "styles": {}, + "text": " in it", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineCodeWithSpecialChars.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineCodeWithSpecialChars.json new file mode 100644 index 0000000000..8b77e45fc7 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineCodeWithSpecialChars.json @@ -0,0 +1,31 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Use ", + "type": "text", + }, + { + "styles": { + "code": true, + }, + "text": "const x = 42;", + "type": "text", + }, + { + "styles": {}, + "text": " to declare", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineHtmlTag.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineHtmlTag.json new file mode 100644 index 0000000000..775c811913 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineHtmlTag.json @@ -0,0 +1,31 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Hello ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "world", + "type": "text", + }, + { + "styles": {}, + "text": "!", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineHtmlVoidTag.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineHtmlVoidTag.json new file mode 100644 index 0000000000..a617cae33f --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineHtmlVoidTag.json @@ -0,0 +1,20 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Line one +line two.", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineHtmlWithAttributes.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineHtmlWithAttributes.json new file mode 100644 index 0000000000..839e11fbb7 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineHtmlWithAttributes.json @@ -0,0 +1,47 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Text with ", + "type": "text", + }, + { + "styles": { + "bold": true, + }, + "text": "bold", + "type": "text", + }, + { + "styles": {}, + "text": " and ", + "type": "text", + }, + { + "content": [ + { + "styles": {}, + "text": "link", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + { + "styles": {}, + "text": ".", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineImage.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineImage.json new file mode 100644 index 0000000000..35a0bc23d1 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineImage.json @@ -0,0 +1,19 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Text before text after", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/italicOnly.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/italicOnly.json new file mode 100644 index 0000000000..01ec89cd69 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/italicOnly.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "italic": true, + }, + "text": "Italic text", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/italicUnderscore.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/italicUnderscore.json new file mode 100644 index 0000000000..3e39b28872 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/italicUnderscore.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "italic": true, + }, + "text": "Italic with underscores", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/lineBreaks.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/lineBreaks.json new file mode 100644 index 0000000000..359a3b1cbc --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/lineBreaks.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Line one + Line two + Line three", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkAndText.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkAndText.json new file mode 100644 index 0000000000..4e6bd86d51 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkAndText.json @@ -0,0 +1,35 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Before ", + "type": "text", + }, + { + "content": [ + { + "styles": {}, + "text": "Link", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + { + "styles": {}, + "text": " after", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkBasic.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkBasic.json new file mode 100644 index 0000000000..2d0b7cff77 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkBasic.json @@ -0,0 +1,25 @@ +[ + { + "children": [], + "content": [ + { + "content": [ + { + "styles": {}, + "text": "Example", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkInParagraph.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkInParagraph.json new file mode 100644 index 0000000000..0509ca27e9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkInParagraph.json @@ -0,0 +1,35 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Check out ", + "type": "text", + }, + { + "content": [ + { + "styles": {}, + "text": "this link", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + { + "styles": {}, + "text": " for more info.", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkWithStyledContent.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkWithStyledContent.json new file mode 100644 index 0000000000..17d11b4dde --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkWithStyledContent.json @@ -0,0 +1,27 @@ +[ + { + "children": [], + "content": [ + { + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Bold link", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkWithTitle.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkWithTitle.json new file mode 100644 index 0000000000..aac8fd51d6 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkWithTitle.json @@ -0,0 +1,25 @@ +[ + { + "children": [], + "content": [ + { + "content": [ + { + "styles": {}, + "text": "example", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/listWithStyledItems.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/listWithStyledItems.json new file mode 100644 index 0000000000..95ff31e2d2 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/listWithStyledItems.json @@ -0,0 +1,83 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Bold item", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": { + "italic": true, + }, + "text": "Italic item", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": { + "strike": true, + }, + "text": "Strikethrough item", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Item with ", + "type": "text", + }, + { + "styles": { + "code": true, + }, + "text": "code", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/mixedInlineContent.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/mixedInlineContent.json new file mode 100644 index 0000000000..71ff4f8a21 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/mixedInlineContent.json @@ -0,0 +1,78 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Normal ", + "type": "text", + }, + { + "styles": { + "bold": true, + }, + "text": "bold", + "type": "text", + }, + { + "styles": {}, + "text": " ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "italic", + "type": "text", + }, + { + "styles": {}, + "text": " ", + "type": "text", + }, + { + "styles": { + "strike": true, + }, + "text": "strike", + "type": "text", + }, + { + "styles": {}, + "text": " ", + "type": "text", + }, + { + "styles": { + "code": true, + }, + "text": "code", + "type": "text", + }, + { + "styles": {}, + "text": " ", + "type": "text", + }, + { + "content": [ + { + "styles": {}, + "text": "link", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/mixedListTypes.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/mixedListTypes.json new file mode 100644 index 0000000000..a877f90459 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/mixedListTypes.json @@ -0,0 +1,90 @@ +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Numbered child", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Another numbered", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Bullet item", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Check child", + "type": "text", + }, + ], + "id": "5", + "props": { + "backgroundColor": "default", + "checked": false, + "textAlignment": "left", + "textColor": "default", + }, + "type": "checkListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Another bullet", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/multipleImages.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/multipleImages.json new file mode 100644 index 0000000000..11f15e765d --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/multipleImages.json @@ -0,0 +1,30 @@ +[ + { + "children": [], + "content": undefined, + "id": "1", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "First", + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/first.png", + }, + "type": "image", + }, + { + "children": [], + "content": undefined, + "id": "2", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "Second", + "showPreview": true, + "textAlignment": "left", + "url": "https://example.com/second.png", + }, + "type": "image", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/multipleParagraphs.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/multipleParagraphs.json new file mode 100644 index 0000000000..de4db270fe --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/multipleParagraphs.json @@ -0,0 +1,53 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "First paragraph", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Second paragraph", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Third paragraph", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedBulletLists.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedBulletLists.json new file mode 100644 index 0000000000..d2d96963e9 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedBulletLists.json @@ -0,0 +1,89 @@ +[ + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Deep nested", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested 1", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Nested 2", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "Item 1", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Item 2", + "type": "text", + }, + ], + "id": "5", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "bulletListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedEmphasis.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedEmphasis.json new file mode 100644 index 0000000000..2e5c63d508 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedEmphasis.json @@ -0,0 +1,22 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + "italic": true, + }, + "text": "bold and italic", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedEmphasisComplex.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedEmphasisComplex.json new file mode 100644 index 0000000000..717e251a84 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedEmphasisComplex.json @@ -0,0 +1,36 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "bold ", + "type": "text", + }, + { + "styles": { + "bold": true, + "italic": true, + }, + "text": "bold and italic", + "type": "text", + }, + { + "styles": { + "bold": true, + }, + "text": " bold", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedOrderedLists.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedOrderedLists.json new file mode 100644 index 0000000000..301593b562 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedOrderedLists.json @@ -0,0 +1,71 @@ +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Sub first", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Sub second", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + ], + "content": [ + { + "styles": {}, + "text": "First", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Second", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/onlyWhitespace.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/onlyWhitespace.json new file mode 100644 index 0000000000..45f0949abe --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/onlyWhitespace.json @@ -0,0 +1,13 @@ +[ + { + "children": [], + "content": [], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/orderedListStart.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/orderedListStart.json new file mode 100644 index 0000000000..35d2df68a3 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/orderedListStart.json @@ -0,0 +1,54 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Third item", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "start": 3, + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Fourth item", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Fifth item", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "numberedListItem", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/paragraphLeadingIndent.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/paragraphLeadingIndent.json new file mode 100644 index 0000000000..613cf56ae2 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/paragraphLeadingIndent.json @@ -0,0 +1,20 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "aaa + bbb", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/setextH1.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/setextH1.json new file mode 100644 index 0000000000..27be2c1d52 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/setextH1.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 1", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/setextH2.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/setextH2.json new file mode 100644 index 0000000000..2cb042a21b --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/setextH2.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 2", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "isToggleable": false, + "level": 2, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/singleNewLines.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/singleNewLines.json new file mode 100644 index 0000000000..3cd78e1f12 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/singleNewLines.json @@ -0,0 +1,22 @@ +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Line 1 + Line 2 + Line 3 + Line 4", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/strikethroughOnly.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/strikethroughOnly.json new file mode 100644 index 0000000000..7f504a4b3f --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/strikethroughOnly.json @@ -0,0 +1,21 @@ +[ + { + "children": [], + "content": [ + { + "styles": { + "strike": true, + }, + "text": "Strikethrough text", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableAlignment.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableAlignment.json new file mode 100644 index 0000000000..35d1354f9f --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableAlignment.json @@ -0,0 +1,132 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": 1, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Left", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Center", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Right", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "L", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "C", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "R", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableBasic.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableBasic.json new file mode 100644 index 0000000000..7a5d236441 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableBasic.json @@ -0,0 +1,135 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": 1, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Header 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Header 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 3", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 4", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableFollowedByParagraph.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableFollowedByParagraph.json new file mode 100644 index 0000000000..36896f03df --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableFollowedByParagraph.json @@ -0,0 +1,114 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": 1, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Col 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Col 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "A", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "B", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph after table", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tablePipeless.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tablePipeless.json new file mode 100644 index 0000000000..4052304874 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tablePipeless.json @@ -0,0 +1,97 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": 1, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Col 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Col 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "A", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "B", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableThreeColumns.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableThreeColumns.json new file mode 100644 index 0000000000..e235519185 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableThreeColumns.json @@ -0,0 +1,132 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": 1, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "A", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "B", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "C", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "3", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableWithInlineFormatting.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableWithInlineFormatting.json new file mode 100644 index 0000000000..7d0cb2bfe3 --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableWithInlineFormatting.json @@ -0,0 +1,141 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": 1, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Header", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Styled", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Normal", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Bold", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": { + "italic": true, + }, + "text": "Italic", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": { + "strike": true, + }, + "text": "Strike", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableWithLinks.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableWithLinks.json new file mode 100644 index 0000000000..7ac6eab9fb --- /dev/null +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableWithLinks.json @@ -0,0 +1,103 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": 1, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Name", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Link", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Example", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "content": [ + { + "styles": {}, + "text": "Click", + "type": "text", + }, + ], + "href": "https://example.com", + "type": "link", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/video.json b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/video.json index 5070e1873e..55635684d9 100644 --- a/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/video.json +++ b/tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/video.json @@ -6,7 +6,7 @@ "props": { "backgroundColor": "default", "caption": "", - "name": "", + "name": "Video", "showPreview": true, "textAlignment": "left", "url": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", diff --git a/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts b/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts index 266d87a68a..5bb72fec41 100644 --- a/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts +++ b/tests/src/unit/core/formatConversion/parse/parseTestInstances.ts @@ -1279,4 +1279,771 @@ Regular paragraph`, }, executeTest: testParseMarkdown, }, + // Individual heading levels + { + testCase: { + name: "headingH1", + content: `# Heading 1`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "headingH2", + content: `## Heading 2`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "headingH3", + content: `### Heading 3`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "headingH4", + content: `#### Heading 4`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "headingH5", + content: `##### Heading 5`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "headingH6", + content: `###### Heading 6`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "headingWithInlineStyles", + content: `# **Bold** *Italic* ~~Strike~~ Heading`, + }, + executeTest: testParseMarkdown, + }, + // Setext headings + { + testCase: { + name: "setextH1", + content: `Heading 1 +===`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "setextH2", + content: `Heading 2 +---`, + }, + executeTest: testParseMarkdown, + }, + // Code blocks + { + testCase: { + name: "codeBlockBasic", + content: `\`\`\` +console.log('Hello'); +\`\`\``, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "codeBlockWithLanguage", + content: `\`\`\`javascript +const x = 42; +console.log(x); +\`\`\``, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "codeBlockPython", + content: `\`\`\`python +def hello(): + print("Hello, world!") +\`\`\``, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "codeBlockWithSpecialChars", + content: `\`\`\`html +
+

Hello **not bold**

+
+\`\`\``, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "codeBlockTildes", + content: `~~~ +code with tildes +~~~`, + }, + executeTest: testParseMarkdown, + }, + // Horizontal rules + { + testCase: { + name: "horizontalRuleDashes", + content: `Paragraph above + +--- + +Paragraph below`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "horizontalRuleAsterisks", + content: `Paragraph above + +*** + +Paragraph below`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "horizontalRuleUnderscores", + content: `Paragraph above + +___ + +Paragraph below`, + }, + executeTest: testParseMarkdown, + }, + // Inline code + { + testCase: { + name: "inlineCode", + content: `This has \`inline code\` in it`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "inlineCodeWithSpecialChars", + content: `Use \`const x = 42;\` to declare`, + }, + executeTest: testParseMarkdown, + }, + // Links + { + testCase: { + name: "linkBasic", + content: `[Example](https://example.com)`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "linkInParagraph", + content: `Check out [this link](https://example.com) for more info.`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "linkWithStyledContent", + content: `[**Bold link**](https://example.com)`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "adjacentLinks", + content: `[Link1](https://example1.com)[Link2](https://example2.com)`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "linkAndText", + content: `Before [Link](https://example.com) after`, + }, + executeTest: testParseMarkdown, + }, + // Tables + { + testCase: { + name: "tableBasic", + content: `| Header 1 | Header 2 | +| -------- | -------- | +| Cell 1 | Cell 2 | +| Cell 3 | Cell 4 |`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "tableThreeColumns", + content: `| A | B | C | +| - | - | - | +| 1 | 2 | 3 |`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "tableWithInlineFormatting", + content: `| Header | Styled | +| ------ | ------ | +| Normal | **Bold** | +| *Italic* | ~~Strike~~ |`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "tableWithLinks", + content: `| Name | Link | +| ---- | ---- | +| Example | [Click](https://example.com) |`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "tableAlignment", + content: `| Left | Center | Right | +| :--- | :----: | ----: | +| L | C | R |`, + }, + executeTest: testParseMarkdown, + }, + // Task lists / check lists + { + testCase: { + name: "checkListBasic", + content: `- [ ] Unchecked item +- [x] Checked item +- [ ] Another unchecked`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "checkListMixed", + content: `- Regular bullet +- [ ] Check item +- [x] Checked item`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "checkListNested", + content: `- [ ] Parent item + - [x] Child checked + - [ ] Child unchecked`, + }, + executeTest: testParseMarkdown, + }, + // Ordered list with start number + { + testCase: { + name: "orderedListStart", + content: `3. Third item +4. Fourth item +5. Fifth item`, + }, + executeTest: testParseMarkdown, + }, + // Hard breaks + { + testCase: { + name: "hardBreakBackslash", + content: `Line one\\ +Line two`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "hardBreakMultiple", + content: `Line one\\ +Line two\\ +Line three`, + }, + executeTest: testParseMarkdown, + }, + // Backslash escapes + { + testCase: { + name: "backslashEscapes", + content: `\\*not bold\\* \\[not a link\\] \\~not strike\\~`, + }, + executeTest: testParseMarkdown, + }, + // Escaped delimiter inside emphasis + { + testCase: { + name: "escapedDelimiterInEmphasis", + content: `*\\**`, + }, + executeTest: testParseMarkdown, + }, + // Nested emphasis + { + testCase: { + name: "nestedEmphasis", + content: `***bold and italic***`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "nestedEmphasisComplex", + content: `**bold *bold and italic* bold**`, + }, + executeTest: testParseMarkdown, + }, + // Individual styles + { + testCase: { + name: "boldOnly", + content: `**Bold text**`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "italicOnly", + content: `*Italic text*`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "strikethroughOnly", + content: `~~Strikethrough text~~`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "boldUnderscore", + content: `__Bold with underscores__`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "italicUnderscore", + content: `_Italic with underscores_`, + }, + executeTest: testParseMarkdown, + }, + // Mixed inline content + { + testCase: { + name: "mixedInlineContent", + content: `Normal **bold** *italic* ~~strike~~ \`code\` [link](https://example.com)`, + }, + executeTest: testParseMarkdown, + }, + // Multiple paragraphs + { + testCase: { + name: "multipleParagraphs", + content: `First paragraph + +Second paragraph + +Third paragraph`, + }, + executeTest: testParseMarkdown, + }, + // Empty content + { + testCase: { + name: "emptyString", + content: ``, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "onlyWhitespace", + content: ` + + `, + }, + executeTest: testParseMarkdown, + }, + // Line breaks + { + testCase: { + name: "lineBreaks", + content: `Line one +Line two +Line three`, + }, + executeTest: testParseMarkdown, + }, + // Nested lists - complex + { + testCase: { + name: "nestedBulletLists", + content: `- Item 1 + - Nested 1 + - Deep nested + - Nested 2 +- Item 2`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "nestedOrderedLists", + content: `1. First + 1. Sub first + 2. Sub second +2. Second`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "mixedListTypes", + content: `- Bullet item + 1. Numbered child + 2. Another numbered +- Another bullet + - [ ] Check child`, + }, + executeTest: testParseMarkdown, + }, + // Blockquote with multiple blocks + { + testCase: { + name: "blockquoteMultiline", + content: `> Line one +> Line two +> Line three`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "blockquoteWithCode", + content: `> Quote with \`inline code\` inside`, + }, + executeTest: testParseMarkdown, + }, + { + testCase: { + name: "blockquoteWithLink", + content: `> Quote with [a link](https://example.com) inside`, + }, + executeTest: testParseMarkdown, + }, + // Blockquote with lazy continuation (no > on continuation lines) + { + testCase: { + name: "blockquoteLazyContinuation", + content: `> This is a quote +that continues here +and here too`, + }, + executeTest: testParseMarkdown, + }, + // Complex document + { + testCase: { + name: "complexDocument", + content: `# Main Title + +An introduction paragraph with **bold** and *italic* text. + +## Section 1 + +- First bullet point +- Second bullet point + - Nested point + +> A notable quote + +### Code Example + +\`\`\`javascript +function hello() { + return "world"; +} +\`\`\` + +--- + +## Section 2 + +1. Step one +2. Step two +3. Step three + +| Feature | Status | +| ------- | ------ | +| Bold | Done | +| Italic | Done | + +![Image](https://example.com/image.png) + +Final paragraph with [a link](https://example.com).`, + }, + executeTest: testParseMarkdown, + }, + // Image with alt text + { + testCase: { + name: "imageWithAlt", + content: `![Alt text for image](https://example.com/photo.jpg)`, + }, + executeTest: testParseMarkdown, + }, + // Multiple images + { + testCase: { + name: "multipleImages", + content: `![First](https://example.com/first.png) + +![Second](https://example.com/second.png)`, + }, + executeTest: testParseMarkdown, + }, + // Inline image within text (should be handled) + { + testCase: { + name: "inlineImage", + content: `Text before ![inline](https://example.com/img.png) text after`, + }, + executeTest: testParseMarkdown, + }, + // Code block immediately after heading + { + testCase: { + name: "headingThenCode", + content: `## Code Section + +\`\`\`python +x = 42 +\`\`\``, + }, + executeTest: testParseMarkdown, + }, + // List with styled items + { + testCase: { + name: "listWithStyledItems", + content: `- **Bold item** +- *Italic item* +- ~~Strikethrough item~~ +- Item with \`code\``, + }, + executeTest: testParseMarkdown, + }, + // Deeply nested lists + { + testCase: { + name: "deeplyNestedLists", + content: `- Level 1 + - Level 2 + - Level 3 + - Level 4`, + }, + executeTest: testParseMarkdown, + }, + // Table followed by paragraph + { + testCase: { + name: "tableFollowedByParagraph", + content: `| Col 1 | Col 2 | +| ----- | ----- | +| A | B | + +Paragraph after table`, + }, + executeTest: testParseMarkdown, + }, + // Paragraphs with various inline formatting + { + testCase: { + name: "adjacentFormattedRuns", + content: `**bold***italic*~~strike~~`, + }, + executeTest: testParseMarkdown, + }, + // Table without outer pipes (GFM allows optional outer pipes) + { + testCase: { + name: "tablePipeless", + content: `Col 1 | Col 2 +----- | ----- +A | B`, + }, + executeTest: testParseMarkdown, + }, + // Indented fenced code block (up to 3 leading spaces per CommonMark) + { + testCase: { + name: "codeBlockIndented", + content: ` \`\`\`ts +const x = 1; + \`\`\``, + }, + executeTest: testParseMarkdown, + }, + // Link with title (title should not appear in href) + { + testCase: { + name: "linkWithTitle", + content: `[example](https://example.com "Example Site")`, + }, + executeTest: testParseMarkdown, + }, + // Image with nested brackets in alt text + { + testCase: { + name: "imageNestedBracketsAlt", + content: `![alt [with] brackets](https://example.com/image.png)`, + }, + executeTest: testParseMarkdown, + }, + // Inline raw HTML tag inside a paragraph passes through verbatim + { + testCase: { + name: "inlineHtmlTag", + content: `Hello world!`, + }, + executeTest: testParseMarkdown, + }, + // Multiple inline HTML tags with attributes + { + testCase: { + name: "inlineHtmlWithAttributes", + content: `Text with bold and link.`, + }, + executeTest: testParseMarkdown, + }, + // A self-closing-style void HTML tag inside a paragraph + { + testCase: { + name: "inlineHtmlVoidTag", + content: `Line one
line two.`, + }, + executeTest: testParseMarkdown, + }, + // Block-level raw HTML is emitted verbatim — not wrapped in

+ { + testCase: { + name: "blockHtmlDiv", + content: `

+A warning block. +
`, + }, + executeTest: testParseMarkdown, + }, + // Block-level HTML comment + { + testCase: { + name: "blockHtmlComment", + content: ` + +Next paragraph.`, + }, + executeTest: testParseMarkdown, + }, + // Bare angle brackets that don't form a valid tag must still be escaped + { + testCase: { + name: "bareAngleBrackets", + content: `1 < 2 and 3 > 0`, + }, + executeTest: testParseMarkdown, + }, + // Block HTML interrupting a paragraph above it + { + testCase: { + name: "blockHtmlInterruptsParagraph", + content: `Some text before. +
raw block
+Some text after.`, + }, + executeTest: testParseMarkdown, + }, + // Hard line break via two trailing spaces (CommonMark ex. 633) + { + testCase: { + name: "hardBreakTwoSpaces", + content: `Line one \nLine two`, + }, + executeTest: testParseMarkdown, + }, + // ATX heading: closing #'s and trailing whitespace are stripped (ex. 73) + { + testCase: { + name: "headingTrailingWhitespace", + content: `### foo ### `, + }, + executeTest: testParseMarkdown, + }, + // ATX heading: lots of internal padding still produces a clean heading (ex. 67) + { + testCase: { + name: "headingInternalPadding", + content: `# foo `, + }, + executeTest: testParseMarkdown, + }, + // Code span with internal newline collapses to space (CommonMark ex. 337) + { + testCase: { + name: "codeSpanWithNewline", + content: "`foo bar \nbaz`", + }, + executeTest: testParseMarkdown, + }, + // Image with title attribute (CommonMark ex. 572). The title is parsed + // even if the BlockNote image block doesn't surface it as a prop — + // the point is to not leak `"title"` into the alt or src. + { + testCase: { + name: "imageWithTitle", + content: `![alt text](https://example.com/image.png "An image title")`, + }, + executeTest: testParseMarkdown, + }, + // Angle-bracket-wrapped image URL — brackets are stripped (ex. 580) + { + testCase: { + name: "imageAngleBracketUrl", + content: `![alt]()`, + }, + executeTest: testParseMarkdown, + }, + // Paragraph lines with up to 3 leading spaces of indent are still a + // paragraph; the indent is stripped (CommonMark ex. 222) + { + testCase: { + name: "paragraphLeadingIndent", + content: ` aaa\n bbb`, + }, + executeTest: testParseMarkdown, + }, ]; diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/basic.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/basic.html index 9974d8d975..d7802da3e3 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/basic.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/basic.html @@ -9,7 +9,7 @@ data-caption="Caption" data-file-block="" > -
+
@@ -20,8 +20,8 @@

example

-

Caption

-
+
Caption
+
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/nested.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/nested.html index 6553a5c4a8..c62486d27d 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/nested.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/nested.html @@ -9,7 +9,7 @@ data-caption="Caption" data-file-block="" > -
+
@@ -20,8 +20,8 @@

example

-

Caption

-
+
Caption
+
@@ -34,7 +34,7 @@ data-caption="Caption" data-file-block="" > -
+
@@ -45,8 +45,8 @@

example

-

Caption

-
+
Caption
+
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/noName.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/noName.html index 47ae5b3bf9..1ec20af747 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/noName.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactFile/noName.html @@ -8,7 +8,7 @@ data-caption="Caption" data-file-block="" > -
+
@@ -19,8 +19,8 @@

-

Caption

-
+
Caption
+ diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/basic.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/basic.html index 5c7411f3ed..8700c93f57 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/basic.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/basic.html @@ -10,17 +10,23 @@ data-preview-width="256" data-file-block="" > -
- example + example
-

Caption

-
+
Caption
+ diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/nested.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/nested.html index 3a0d0d50b0..bed56f5a3d 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/nested.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/nested.html @@ -10,17 +10,23 @@ data-preview-width="256" data-file-block="" > -
- example + example
-

Caption

-
+
Caption
+
@@ -34,17 +40,23 @@ data-preview-width="256" data-file-block="" > -
- example + example
-

Caption

-
+
Caption
+
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noCaption.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noCaption.html index 67d4f962f0..44b706cebe 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noCaption.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noCaption.html @@ -14,7 +14,13 @@ style="position: relative; width: 256px;" >
- example + example
diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noName.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noName.html index 315d8db293..2a2bbd7a8d 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noName.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noName.html @@ -9,17 +9,23 @@ data-preview-width="256" data-file-block="" > -
- Caption +
-

Caption

-
+
Caption
+ diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noPreview.html b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noPreview.html index 3e1f5a6264..59d65f82ae 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noPreview.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/reactImage/noPreview.html @@ -11,7 +11,7 @@ data-preview-width="256" data-file-block="" > -
+
@@ -22,8 +22,8 @@

example

-

Caption

-
+
Caption
+ diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactFile/button.html b/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactFile/button.html index cc675c57a7..90ce06d701 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactFile/button.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactFile/button.html @@ -1 +1 @@ -

Add file

\ No newline at end of file + \ No newline at end of file diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactImage/button.html b/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactImage/button.html index 8553433aff..df18852143 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactImage/button.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactImage/button.html @@ -1 +1 @@ -

Add image

\ No newline at end of file + \ No newline at end of file diff --git a/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactImage/noName.html b/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactImage/noName.html index 686fc7d4e5..47f0cbe255 100644 --- a/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactImage/noName.html +++ b/tests/src/unit/react/formatConversion/export/__snapshots__/html/reactImage/noName.html @@ -1,4 +1,4 @@
- Caption +
Caption
\ No newline at end of file diff --git a/tests/src/unit/shared/clipboard/copyPasteEquality/copyPasteEqualityTestExecutors.ts b/tests/src/unit/shared/clipboard/copyPasteEquality/copyPasteEqualityTestExecutors.ts index 1fdfb61013..5a6a22f322 100644 --- a/tests/src/unit/shared/clipboard/copyPasteEquality/copyPasteEqualityTestExecutors.ts +++ b/tests/src/unit/shared/clipboard/copyPasteEquality/copyPasteEqualityTestExecutors.ts @@ -21,7 +21,7 @@ export const testCopyPasteEquality = async < ) => { initTestEditor(editor, testCase.document, testCase.getCopyAndPasteSelection); - const { clipboardHTML } = selectedFragmentToHTML( + const { clipboardHTML, markdown } = selectedFragmentToHTML( editor.prosemirrorView, editor, ); @@ -29,7 +29,7 @@ export const testCopyPasteEquality = async < const originalDocument = editor.document; doPaste( editor.prosemirrorView, - "text", + markdown, clipboardHTML, false, new ClipboardEvent("paste"), diff --git a/tests/src/unit/shared/formatConversion/export/exportTestExecutors.ts b/tests/src/unit/shared/formatConversion/export/exportTestExecutors.ts index e7ccef6e5b..1a8893b2b8 100644 --- a/tests/src/unit/shared/formatConversion/export/exportTestExecutors.ts +++ b/tests/src/unit/shared/formatConversion/export/exportTestExecutors.ts @@ -11,6 +11,10 @@ import { expect } from "vitest"; import { ExportTestCase } from "./exportTestCase.js"; +// Preserve `` whitespace so code-block snapshots show actual newlines +// instead of having them collapsed by the prettifier. +const PRETTIFY_OPTIONS = { tag_wrap: true, ignore: ["code"] }; + export const testExportBlockNoteHTML = async < B extends BlockSchema, I extends InlineContentSchema, @@ -24,9 +28,7 @@ export const testExportBlockNoteHTML = async < addIdsToBlocks(testCase.content); await expect( - prettify(await editor.blocksToFullHTML(testCase.content), { - tag_wrap: true, - }), + prettify(await editor.blocksToFullHTML(testCase.content), PRETTIFY_OPTIONS), ).toMatchFileSnapshot(`./__snapshots__/blocknoteHTML/${testCase.name}.html`); }; @@ -43,9 +45,7 @@ export const testExportHTML = async < addIdsToBlocks(testCase.content); await expect( - prettify(await editor.blocksToHTMLLossy(testCase.content), { - tag_wrap: true, - }), + prettify(await editor.blocksToHTMLLossy(testCase.content), PRETTIFY_OPTIONS), ).toMatchFileSnapshot(`./__snapshots__/html/${testCase.name}.html`); }; diff --git a/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts b/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts index a42f7c7c4b..0606d7dd85 100644 --- a/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts +++ b/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts @@ -63,6 +63,34 @@ export const testExportParseEqualityHTML = async < ); }; +export const testExportParseEqualityMarkdown = async < + B extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema, +>( + editor: BlockNoteEditor, + testCase: ExportParseEqualityTestCase, +) => { + (window as any).__TEST_OPTIONS.mockID = 0; + + addIdsToBlocks(testCase.content); + + const exported = await editor.blocksToMarkdownLossy(testCase.content); + + // Reset mock ID as we don't expect block IDs to be preserved in this + // conversion. + (window as any).__TEST_OPTIONS.mockID = 0; + + // Markdown is lossy (colors, underline, alignment are dropped), so we use + // snapshot matching to capture the expected round-trip result rather than + // strict equality with the input. + await expect( + await editor.tryParseMarkdownToBlocks(exported), + ).toMatchFileSnapshot( + `./__snapshots__/markdown/${testCase.name}.json`, + ); +}; + export const testExportParseEqualityNodes = async < B extends BlockSchema, I extends InlineContentSchema, diff --git a/tests/src/utils/const.ts b/tests/src/utils/const.ts index 61cedc194d..6220709aa7 100644 --- a/tests/src/utils/const.ts +++ b/tests/src/utils/const.ts @@ -43,6 +43,22 @@ export const ALERT_BLOCK_URL = !process.env.RUN_IN_DOCKER ? `http://localhost:${PORT}/custom-schema/alert-block?hideMenu` : `http://host.docker.internal:${PORT}/custom-schema/alert-block?hideMenu`; +export const NON_EDITABLE_BLOCK_URL = !process.env.RUN_IN_DOCKER + ? `http://localhost:${PORT}/custom-schema/non-editable-block?hideMenu` + : `http://host.docker.internal:${PORT}/custom-schema/non-editable-block?hideMenu`; + +export const PDF_FILE_BLOCK_URL = !process.env.RUN_IN_DOCKER + ? `http://localhost:${PORT}/custom-schema/pdf-file-block?hideMenu` + : `http://host.docker.internal:${PORT}/custom-schema/pdf-file-block?hideMenu`; + +export const COMMENTS_URL = !process.env.RUN_IN_DOCKER + ? `http://localhost:${PORT}/collaboration/comments-testing?hideMenu` + : `http://host.docker.internal:${PORT}/collaboration/comments-testing?hideMenu`; + +export const NO_TRAILING_BLOCK_URL = !process.env.RUN_IN_DOCKER + ? `http://localhost:${PORT}/basic/no-trailing-block?hideMenu` + : `http://host.docker.internal:${PORT}/basic/no-trailing-block?hideMenu`; + export const PASTE_ZONE_SELECTOR = "#pasteZone"; export const EDITOR_SELECTOR = `.bn-editor`; @@ -56,6 +72,7 @@ export const NUMBERED_LIST_SELECTOR = `[data-content-type="numberedListItem"]`; export const BULLET_LIST_SELECTOR = `[data-content-type="bulletListItem"]`; export const PARAGRAPH_SELECTOR = `[data-content-type="paragraph"]`; export const IMAGE_SELECTOR = `[data-content-type="image"]`; +export const PDF_SELECTOR = `[data-content-type="pdf"]`; export const TABLE_SELECTOR = `[data-content-type="table"]`; export const DRAG_HANDLE_SELECTOR = `[data-test="dragHandle"]`; diff --git a/tests/src/utils/copypaste.ts b/tests/src/utils/copypaste.ts index eb95f811dc..cabfc72db9 100644 --- a/tests/src/utils/copypaste.ts +++ b/tests/src/utils/copypaste.ts @@ -1,11 +1,12 @@ import { Page } from "@playwright/test"; -import { PASTE_ZONE_SELECTOR, TYPE_DELAY } from "./const.js"; +import { PASTE_ZONE_SELECTOR } from "./const.js"; import { focusOnEditor } from "./editor.js"; export async function copyPaste(page: Page) { await page.keyboard.press(`ControlOrMeta+C`); - await page.keyboard.press("ArrowDown", { delay: TYPE_DELAY }); - await page.keyboard.press("Enter"); + // Exit out of any menus/toolbars which may block the trailing block. + await page.keyboard.press(`Escape`); + await page.locator(".bn-trailing-block").click(); await page.keyboard.press(`ControlOrMeta+V`); } @@ -40,7 +41,6 @@ export function removeMetaFromHTML(html: string) { export async function insertParagraph(page: Page) { await page.keyboard.type("Paragraph"); - await page.keyboard.press("ArrowDown", { delay: TYPE_DELAY }); } export async function insertHeading(page: Page, headingLevel: number) { @@ -50,7 +50,6 @@ export async function insertHeading(page: Page, headingLevel: number) { await page.keyboard.press(" "); await page.keyboard.type("Heading"); - await page.keyboard.press("ArrowDown", { delay: TYPE_DELAY }); } export async function startList(page: Page, ordered: boolean) { @@ -70,7 +69,6 @@ export async function insertListItems(page: Page) { await page.keyboard.type("List Item 2"); await page.keyboard.press("Enter"); await page.keyboard.type("List Item 3"); - await page.keyboard.press("ArrowDown", { delay: TYPE_DELAY }); } export async function insertNestedListItems(page: Page) { @@ -81,5 +79,4 @@ export async function insertNestedListItems(page: Page) { await page.keyboard.press("Enter"); await page.keyboard.press("Tab"); await page.keyboard.type("List Item 3"); - await page.keyboard.press("ArrowDown", { delay: TYPE_DELAY }); } diff --git a/tests/src/utils/editor.ts b/tests/src/utils/editor.ts index 24bdb3c53b..c8859f3f8e 100644 --- a/tests/src/utils/editor.ts +++ b/tests/src/utils/editor.ts @@ -46,3 +46,32 @@ export async function compareDocToSnapshot(page: Page, name: string) { const doc = JSON.stringify(await getDoc(page), null, 2); expect(doc).toMatchSnapshot(`${name}.json`); } + +/** + * Programmatically move cursor to end of the current block content. + * This avoids relying on keyboard navigation (ArrowUp/End) which can + * position the cursor incorrectly in WebKit when crossing blocks with + * different indentation levels. + */ +export async function moveCursorToBlockEnd(page: Page) { + await page.evaluate(() => { + const tiptap = (window as any).ProseMirror; + const bnEditor = tiptap.schema.cached.blockNoteEditor; + const block = bnEditor.getTextCursorPosition().block; + bnEditor.setTextCursorPosition(block, "end"); + }); +} + +/** + * Programmatically move cursor to start of the current block content. + * This avoids relying on keyboard navigation which can be unreliable + * in WebKit. + */ +export async function moveCursorToBlockStart(page: Page) { + await page.evaluate(() => { + const tiptap = (window as any).ProseMirror; + const bnEditor = tiptap.schema.cached.blockNoteEditor; + const block = bnEditor.getTextCursorPosition().block; + bnEditor.setTextCursorPosition(block, "start"); + }); +}