diff --git a/demos/src/Examples/JSX/React/Paragraph.tsx b/demos/src/Examples/JSX/React/Paragraph.tsx
new file mode 100644
index 000000000..8084b28e7
--- /dev/null
+++ b/demos/src/Examples/JSX/React/Paragraph.tsx
@@ -0,0 +1,13 @@
+/** @jsxImportSource @tiptap/core */
+import { mergeAttributes } from '@tiptap/core'
+import { Paragraph as BaseParagraph } from '@tiptap/extension-paragraph'
+
+export const Paragraph = BaseParagraph.extend({
+ renderHTML({ HTMLAttributes }) {
+ return (
+
+
+
+ )
+ },
+})
diff --git a/demos/src/Examples/JSX/React/index.html b/demos/src/Examples/JSX/React/index.html
new file mode 100644
index 000000000..e69de29bb
diff --git a/demos/src/Examples/JSX/React/index.spec.js b/demos/src/Examples/JSX/React/index.spec.js
new file mode 100644
index 000000000..e26a0a564
--- /dev/null
+++ b/demos/src/Examples/JSX/React/index.spec.js
@@ -0,0 +1,16 @@
+context('/src/Examples/JSX/React/', () => {
+ beforeEach(() => {
+ cy.visit('/src/Examples/JSX/React/')
+ })
+
+ it('should have a working tiptap instance', () => {
+ cy.get('.tiptap').then(([{ editor }]) => {
+ // eslint-disable-next-line
+ expect(editor).to.not.be.null
+ })
+ })
+
+ it('should have paragraphs colored as red', () => {
+ cy.get('.tiptap p').should('have.css', 'color', 'rgb(255, 0, 0)')
+ })
+})
diff --git a/demos/src/Examples/JSX/React/index.tsx b/demos/src/Examples/JSX/React/index.tsx
new file mode 100644
index 000000000..562da836e
--- /dev/null
+++ b/demos/src/Examples/JSX/React/index.tsx
@@ -0,0 +1,25 @@
+import './styles.scss'
+
+import { EditorContent, useEditor } from '@tiptap/react'
+import StarterKit from '@tiptap/starter-kit'
+import React from 'react'
+
+import { Paragraph } from './Paragraph.jsx'
+
+export default () => {
+ const editor = useEditor({
+ extensions: [
+ StarterKit.configure({
+ paragraph: false,
+ }),
+ Paragraph,
+ ],
+ content: `
+
+ Each paragraph will be red
+
+ `,
+ })
+
+ return
+}
diff --git a/demos/src/Examples/JSX/React/styles.scss b/demos/src/Examples/JSX/React/styles.scss
new file mode 100644
index 000000000..15283b15a
--- /dev/null
+++ b/demos/src/Examples/JSX/React/styles.scss
@@ -0,0 +1,91 @@
+/* Basic editor styles */
+.tiptap {
+ :first-child {
+ margin-top: 0;
+ }
+
+ /* List styles */
+ ul,
+ ol {
+ padding: 0 1rem;
+ margin: 1.25rem 1rem 1.25rem 0.4rem;
+
+ li p {
+ margin-top: 0.25em;
+ margin-bottom: 0.25em;
+ }
+ }
+
+ /* Heading styles */
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ line-height: 1.1;
+ margin-top: 2.5rem;
+ text-wrap: pretty;
+ }
+
+ h1,
+ h2 {
+ margin-top: 3.5rem;
+ margin-bottom: 1.5rem;
+ }
+
+ h1 {
+ font-size: 1.4rem;
+ }
+
+ h2 {
+ font-size: 1.2rem;
+ }
+
+ h3 {
+ font-size: 1.1rem;
+ }
+
+ h4,
+ h5,
+ h6 {
+ font-size: 1rem;
+ }
+
+ /* Code and preformatted text styles */
+ code {
+ background-color: var(--purple-light);
+ border-radius: 0.4rem;
+ color: var(--black);
+ font-size: 0.85rem;
+ padding: 0.25em 0.3em;
+ }
+
+ pre {
+ background: var(--black);
+ border-radius: 0.5rem;
+ color: var(--white);
+ font-family: 'JetBrainsMono', monospace;
+ margin: 1.5rem 0;
+ padding: 0.75rem 1rem;
+
+ code {
+ background: none;
+ color: inherit;
+ font-size: 0.8rem;
+ padding: 0;
+ }
+ }
+
+ blockquote {
+ border-left: 3px solid var(--gray-3);
+ margin: 1.5rem 0;
+ padding-left: 1rem;
+ }
+
+ hr {
+ border: none;
+ border-top: 1px solid var(--gray-2);
+ margin: 2rem 0;
+ }
+}
diff --git a/demos/src/Examples/JSX/Vue/Paragraph.tsx b/demos/src/Examples/JSX/Vue/Paragraph.tsx
new file mode 100644
index 000000000..8084b28e7
--- /dev/null
+++ b/demos/src/Examples/JSX/Vue/Paragraph.tsx
@@ -0,0 +1,13 @@
+/** @jsxImportSource @tiptap/core */
+import { mergeAttributes } from '@tiptap/core'
+import { Paragraph as BaseParagraph } from '@tiptap/extension-paragraph'
+
+export const Paragraph = BaseParagraph.extend({
+ renderHTML({ HTMLAttributes }) {
+ return (
+
+
+
+ )
+ },
+})
diff --git a/demos/src/Examples/JSX/Vue/index.html b/demos/src/Examples/JSX/Vue/index.html
new file mode 100644
index 000000000..e69de29bb
diff --git a/demos/src/Examples/JSX/Vue/index.spec.js b/demos/src/Examples/JSX/Vue/index.spec.js
new file mode 100644
index 000000000..b0360d5b4
--- /dev/null
+++ b/demos/src/Examples/JSX/Vue/index.spec.js
@@ -0,0 +1,16 @@
+context('/src/Examples/JSX/Vue/', () => {
+ beforeEach(() => {
+ cy.visit('/src/Examples/JSX/Vue/')
+ })
+
+ it('should have a working tiptap instance', () => {
+ cy.get('.tiptap').then(([{ editor }]) => {
+ // eslint-disable-next-line
+ expect(editor).to.not.be.null
+ })
+ })
+
+ it('should have paragraphs colored as red', () => {
+ cy.get('.tiptap p').should('have.css', 'color', 'rgb(255, 0, 0)')
+ })
+})
diff --git a/demos/src/Examples/JSX/Vue/index.vue b/demos/src/Examples/JSX/Vue/index.vue
new file mode 100644
index 000000000..492e13819
--- /dev/null
+++ b/demos/src/Examples/JSX/Vue/index.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+n
diff --git a/demos/vite.config.ts b/demos/vite.config.ts
index 223a22320..72992b925 100644
--- a/demos/vite.config.ts
+++ b/demos/vite.config.ts
@@ -41,6 +41,10 @@ const getPackageDependencies = () => {
}
})
+ // Handle the JSX runtime alias
+ paths.unshift({ find: '@tiptap/core/jsx-runtime', replacement: resolve('../packages/core/src/jsx-runtime.ts') })
+ paths.unshift({ find: '@tiptap/core/jsx-dev-runtime', replacement: resolve('../packages/core/src/jsx-runtime.ts') })
+
return paths
}
diff --git a/packages/core/jsx-dev-runtime/index.cjs b/packages/core/jsx-dev-runtime/index.cjs
new file mode 100644
index 000000000..6f1356f15
--- /dev/null
+++ b/packages/core/jsx-dev-runtime/index.cjs
@@ -0,0 +1 @@
+export * from '../dist/jsx-runtime/jsx-runtime.cjs'
diff --git a/packages/core/jsx-dev-runtime/index.d.cts b/packages/core/jsx-dev-runtime/index.d.cts
new file mode 100644
index 000000000..75d913623
--- /dev/null
+++ b/packages/core/jsx-dev-runtime/index.d.cts
@@ -0,0 +1 @@
+export * from '../src/jsx-runtime.ts'
diff --git a/packages/core/jsx-dev-runtime/index.d.ts b/packages/core/jsx-dev-runtime/index.d.ts
new file mode 100644
index 000000000..644513c17
--- /dev/null
+++ b/packages/core/jsx-dev-runtime/index.d.ts
@@ -0,0 +1 @@
+export type * from '../src/jsx-runtime.js'
diff --git a/packages/core/jsx-dev-runtime/index.js b/packages/core/jsx-dev-runtime/index.js
new file mode 100644
index 000000000..856e5edff
--- /dev/null
+++ b/packages/core/jsx-dev-runtime/index.js
@@ -0,0 +1 @@
+export * from '../dist/jsx-runtime/jsx-runtime.js'
diff --git a/packages/core/jsx-runtime/index.cjs b/packages/core/jsx-runtime/index.cjs
new file mode 100644
index 000000000..6f1356f15
--- /dev/null
+++ b/packages/core/jsx-runtime/index.cjs
@@ -0,0 +1 @@
+export * from '../dist/jsx-runtime/jsx-runtime.cjs'
diff --git a/packages/core/jsx-runtime/index.d.cts b/packages/core/jsx-runtime/index.d.cts
new file mode 100644
index 000000000..75d913623
--- /dev/null
+++ b/packages/core/jsx-runtime/index.d.cts
@@ -0,0 +1 @@
+export * from '../src/jsx-runtime.ts'
diff --git a/packages/core/jsx-runtime/index.d.ts b/packages/core/jsx-runtime/index.d.ts
new file mode 100644
index 000000000..5a1c2c9d3
--- /dev/null
+++ b/packages/core/jsx-runtime/index.d.ts
@@ -0,0 +1 @@
+export type * from '../src/jsx-runtime.ts'
diff --git a/packages/core/jsx-runtime/index.js b/packages/core/jsx-runtime/index.js
new file mode 100644
index 000000000..856e5edff
--- /dev/null
+++ b/packages/core/jsx-runtime/index.js
@@ -0,0 +1 @@
+export * from '../dist/jsx-runtime/jsx-runtime.js'
diff --git a/packages/core/package.json b/packages/core/package.json
index c1892178a..99423aee4 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -24,6 +24,22 @@
},
"import": "./dist/index.js",
"require": "./dist/index.cjs"
+ },
+ "./jsx-runtime": {
+ "types": {
+ "import": "./jsx-runtime/index.d.ts",
+ "require": "./jsx-runtime/index.d.cts"
+ },
+ "import": "./jsx-runtime/index.js",
+ "require": "./jsx-runtime/index.cjs"
+ },
+ "./jsx-dev-runtime": {
+ "types": {
+ "import": "./jsx-dev-runtime/index.d.ts",
+ "require": "./jsx-dev-runtime/index.d.cts"
+ },
+ "import": "./jsx-dev-runtime/index.js",
+ "require": "./jsx-dev-runtime/index.cjs"
}
},
"main": "dist/index.cjs",
@@ -31,7 +47,8 @@
"types": "dist/index.d.ts",
"files": [
"src",
- "dist"
+ "dist",
+ "jsx-runtime"
],
"devDependencies": {
"@tiptap/pm": "^3.0.0-next.4"
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index ccfa70925..5ccf34851 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -5,6 +5,7 @@ export * as extensions from './extensions/index.js'
export * from './helpers/index.js'
export * from './InputRule.js'
export * from './inputRules/index.js'
+export { createElement, Fragment, createElement as h } from './jsx-runtime.js'
export * from './Mark.js'
export * from './Node.js'
export * from './NodePos.js'
diff --git a/packages/core/src/jsx-runtime.ts b/packages/core/src/jsx-runtime.ts
new file mode 100644
index 000000000..9b9278faa
--- /dev/null
+++ b/packages/core/src/jsx-runtime.ts
@@ -0,0 +1,64 @@
+export type Attributes = Record
+
+export type DOMOutputSpecElement = 0 | Attributes | DOMOutputSpecArray
+/**
+ * Better describes the output of a `renderHTML` function in prosemirror
+ * @see https://prosemirror.net/docs/ref/#model.DOMOutputSpec
+ */
+export type DOMOutputSpecArray =
+ | [string]
+ | [string, Attributes]
+ | [string, 0]
+ | [string, Attributes, 0]
+ | [string, Attributes, DOMOutputSpecArray | 0]
+ | [string, DOMOutputSpecArray]
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace JSX {
+ // @ts-ignore - conflict with React typings
+ type Element = [string, ...any[]]
+ // @ts-ignore - conflict with React typings
+ interface IntrinsicElements {
+ // @ts-ignore - conflict with React typings
+ [key: string]: any
+ }
+ }
+}
+
+export type JSXRenderer = (
+ tag: 'slot' | string | ((props?: Attributes) => DOMOutputSpecArray | DOMOutputSpecElement),
+ props?: Attributes,
+ ...children: JSXRenderer[]
+) => DOMOutputSpecArray | DOMOutputSpecElement
+
+export function Fragment(props: { children: JSXRenderer[] }) {
+ return props.children
+}
+
+export const h: JSXRenderer = (tag, attributes) => {
+ // Treat the slot tag as the Prosemirror hole to render content into
+ if (tag === 'slot') {
+ return 0
+ }
+
+ // If the tag is a function, call it with the props
+ if (tag instanceof Function) {
+ return tag(attributes)
+ }
+
+ const { children, ...rest } = attributes ?? {}
+
+ if (tag === 'svg') {
+ throw new Error('SVG elements are not supported in the JSX syntax, use the array syntax instead')
+ }
+
+ // Otherwise, return the tag, attributes, and children
+ return [tag, rest, children]
+}
+
+// See
+// https://esbuild.github.io/api/#jsx-import-source
+// https://www.typescriptlang.org/tsconfig/#jsxImportSource
+
+export { h as createElement, h as jsx, h as jsxDEV, h as jsxs }
diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts
index 9ad38f6d6..820659d52 100644
--- a/packages/core/tsup.config.ts
+++ b/packages/core/tsup.config.ts
@@ -1,11 +1,22 @@
import { defineConfig } from 'tsup'
-export default defineConfig({
- entry: ['src/index.ts'],
- // purposefully not using the build tsconfig, so @tiptap/core's types can be resolved correctly
- outDir: 'dist',
- dts: true,
- clean: true,
- sourcemap: true,
- format: ['esm', 'cjs'],
-})
+export default defineConfig([
+ {
+ entry: ['src/index.ts'],
+ // purposefully not using the build tsconfig, so @tiptap/core's types can be resolved correctly
+ outDir: 'dist',
+ dts: true,
+ clean: true,
+ sourcemap: true,
+ format: ['esm', 'cjs'],
+ },
+ {
+ entry: ['src/jsx-runtime.ts'],
+ tsconfig: '../../tsconfig.build.json',
+ outDir: 'dist/jsx-runtime',
+ dts: true,
+ clean: true,
+ sourcemap: true,
+ format: ['esm', 'cjs'],
+ },
+])
diff --git a/packages/extension-blockquote/src/blockquote.ts b/packages/extension-blockquote/src/blockquote.tsx
similarity index 86%
rename from packages/extension-blockquote/src/blockquote.ts
rename to packages/extension-blockquote/src/blockquote.tsx
index 0f24cbb3e..ee8814fa2 100644
--- a/packages/extension-blockquote/src/blockquote.ts
+++ b/packages/extension-blockquote/src/blockquote.tsx
@@ -1,4 +1,6 @@
+/** @jsxImportSource @tiptap/core */
import { mergeAttributes, Node, wrappingInputRule } from '@tiptap/core'
+import type { DOMOutputSpecArray } from '@tiptap/core/jsx-runtime'
export interface BlockquoteOptions {
/**
@@ -57,7 +59,11 @@ export const Blockquote = Node.create({
},
renderHTML({ HTMLAttributes }) {
- return ['blockquote', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
+ return (
+
+
+
+ ) as unknown as DOMOutputSpecArray
},
addCommands() {
diff --git a/packages/extension-blockquote/src/index.ts b/packages/extension-blockquote/src/index.ts
index b805f47ae..e47cb14aa 100644
--- a/packages/extension-blockquote/src/index.ts
+++ b/packages/extension-blockquote/src/index.ts
@@ -1,5 +1,5 @@
-import { Blockquote } from './blockquote.js'
+import { Blockquote } from './blockquote.jsx'
-export * from './blockquote.js'
+export * from './blockquote.jsx'
export default Blockquote
diff --git a/packages/extension-bold/src/bold.ts b/packages/extension-bold/src/bold.tsx
similarity index 91%
rename from packages/extension-bold/src/bold.ts
rename to packages/extension-bold/src/bold.tsx
index 2229ee6f9..1de2faa13 100644
--- a/packages/extension-bold/src/bold.ts
+++ b/packages/extension-bold/src/bold.tsx
@@ -1,4 +1,6 @@
+/** @jsxImportSource @tiptap/core */
import { Mark, markInputRule, markPasteRule, mergeAttributes } from '@tiptap/core'
+import type { DOMOutputSpecArray } from '@tiptap/core/jsx-runtime'
export interface BoldOptions {
/**
@@ -82,7 +84,11 @@ export const Bold = Mark.create({
},
renderHTML({ HTMLAttributes }) {
- return ['strong', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
+ return (
+
+
+
+ ) as unknown as DOMOutputSpecArray
},
addCommands() {
diff --git a/packages/extension-bold/src/index.ts b/packages/extension-bold/src/index.ts
index 16582a530..3a2d6c8a7 100644
--- a/packages/extension-bold/src/index.ts
+++ b/packages/extension-bold/src/index.ts
@@ -1,5 +1,5 @@
-import { Bold } from './bold.js'
+import { Bold } from './bold.jsx'
-export * from './bold.js'
+export * from './bold.jsx'
export default Bold
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3ab946883..6b75d03ba 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -687,6 +687,15 @@ importers:
specifier: ^3.0.0-next.4
version: link:../pm
+ packages/jsx:
+ dependencies:
+ '@tiptap/core':
+ specifier: ^3.0.0-next.3
+ version: link:../core
+ '@tiptap/pm':
+ specifier: ^3.0.0-next.3
+ version: link:../pm
+
packages/pm:
dependencies:
prosemirror-changeset:
diff --git a/tests/cypress/plugins/index.js b/tests/cypress/plugins/index.js
index 58f7c5e11..3cd2498cc 100644
--- a/tests/cypress/plugins/index.js
+++ b/tests/cypress/plugins/index.js
@@ -66,6 +66,7 @@ module.exports = on => {
alias,
extensionAlias: {
'.js': ['.js', '.ts'],
+ '.jsx': ['.jsx', '.tsx'],
},
},
},
diff --git a/tsconfig.json b/tsconfig.json
index 67c10544d..634be28b7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,7 +4,7 @@
"target": "es2019",
"module": "esnext",
"strict": true,
- "jsx": "react",
+ "jsx": "react-jsx",
"importHelpers": true,
"moduleResolution": "node",
"resolveJsonModule": true,