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,