Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jsx): add @tiptap/jsx for more convenient rendering of Tiptap content #5558

Merged
merged 13 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions demos/src/Examples/JSX/React/Paragraph.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<p {...mergeAttributes(HTMLAttributes, { style: 'color: red' })}>
<slot />
</p>
)
},
})
Empty file.
16 changes: 16 additions & 0 deletions demos/src/Examples/JSX/React/index.spec.js
Original file line number Diff line number Diff line change
@@ -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)')
})
})
25 changes: 25 additions & 0 deletions demos/src/Examples/JSX/React/index.tsx
Original file line number Diff line number Diff line change
@@ -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: `
<p>
Each paragraph will be red
</p>
`,
})

return <EditorContent editor={editor} />
}
91 changes: 91 additions & 0 deletions demos/src/Examples/JSX/React/styles.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 13 additions & 0 deletions demos/src/Examples/JSX/Vue/Paragraph.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<p {...mergeAttributes(HTMLAttributes, { style: 'color: red' })}>
<slot />
</p>
)
},
})
Empty file.
16 changes: 16 additions & 0 deletions demos/src/Examples/JSX/Vue/index.spec.js
Original file line number Diff line number Diff line change
@@ -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)')
})
})
52 changes: 52 additions & 0 deletions demos/src/Examples/JSX/Vue/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<template>
<editor-content :editor="editor" />
</template>

<script>
import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'

import { Paragraph } from './Paragraph.tsx'

export default {
components: {
EditorContent,
},

data() {
return {
editor: null,
}
},

mounted() {
this.editor = new Editor({
extensions: [
StarterKit.configure({
paragraph: false,
}),
Paragraph,
],
content: `
<p>
Each paragraph will be red
</p>
`,
})
},

beforeUnmount() {
this.editor.destroy()
},
}
</script>

<style lang="scss">
/* Basic editor styles */
.tiptap {
> * + * {
margin-top: 0.75em;
}
}
</style>
n
4 changes: 4 additions & 0 deletions demos/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/jsx-dev-runtime/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../dist/jsx-runtime/jsx-runtime.cjs'
1 change: 1 addition & 0 deletions packages/core/jsx-dev-runtime/index.d.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../src/jsx-runtime.ts'
1 change: 1 addition & 0 deletions packages/core/jsx-dev-runtime/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type * from '../src/jsx-runtime.js'
1 change: 1 addition & 0 deletions packages/core/jsx-dev-runtime/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../dist/jsx-runtime/jsx-runtime.js'
1 change: 1 addition & 0 deletions packages/core/jsx-runtime/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../dist/jsx-runtime/jsx-runtime.cjs'
1 change: 1 addition & 0 deletions packages/core/jsx-runtime/index.d.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../src/jsx-runtime.ts'
1 change: 1 addition & 0 deletions packages/core/jsx-runtime/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type * from '../src/jsx-runtime.ts'
1 change: 1 addition & 0 deletions packages/core/jsx-runtime/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../dist/jsx-runtime/jsx-runtime.js'
19 changes: 18 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,31 @@
},
"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",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"src",
"dist"
"dist",
"jsx-runtime"
],
"devDependencies": {
"@tiptap/pm": "^3.0.0-next.4"
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
64 changes: 64 additions & 0 deletions packages/core/src/jsx-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export type Attributes = Record<string, any>

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 }
29 changes: 20 additions & 9 deletions packages/core/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -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'],
},
])
Loading
Loading