-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: first version of a static renderer
- Loading branch information
1 parent
7cdaf65
commit 520cff2
Showing
11 changed files
with
1,385 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Change Log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# @tiptap/static-renderer | ||
|
||
[![Version](https://img.shields.io/npm/v/@tiptap/static-renderer.svg?label=version)](https://www.npmjs.com/package/@tiptap/static-renderer) | ||
[![Downloads](https://img.shields.io/npm/dm/@tiptap/static-renderer.svg)](https://npmcharts.com/compare/tiptap?minimal=true) | ||
[![License](https://img.shields.io/npm/l/@tiptap/static-renderer.svg)](https://www.npmjs.com/package/@tiptap/static-renderer) | ||
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) | ||
|
||
## Introduction | ||
|
||
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*. | ||
|
||
## Official Documentation | ||
|
||
Documentation can be found on the [Tiptap website](https://tiptap.dev). | ||
|
||
## License | ||
|
||
Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
{ | ||
"name": "@tiptap/static-renderer", | ||
"description": "statically render Tiptap JSON", | ||
"version": "2.6.6", | ||
"homepage": "https://tiptap.dev", | ||
"keywords": [ | ||
"tiptap", | ||
"tiptap static renderer", | ||
"tiptap react renderer" | ||
], | ||
"license": "MIT", | ||
"funding": { | ||
"type": "github", | ||
"url": "https://github.com/sponsors/ueberdosis" | ||
}, | ||
"exports": { | ||
".": { | ||
"types": "./dist/packages/static-renderer/src/index.d.ts", | ||
"import": "./dist/index.js", | ||
"require": "./dist/index.cjs" | ||
} | ||
}, | ||
"main": "dist/index.cjs", | ||
"module": "dist/index.js", | ||
"umd": "dist/index.umd.js", | ||
"types": "dist/packages/static-renderer/src/index.d.ts", | ||
"type": "module", | ||
"files": [ | ||
"src", | ||
"dist" | ||
], | ||
"dependencies": { | ||
"@tiptap/core": "^2.6.6", | ||
"@tiptap/pm": "^2.6.6" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/ueberdosis/tiptap", | ||
"directory": "packages/static-renderer" | ||
}, | ||
"scripts": { | ||
"clean": "rm -rf dist", | ||
"build": "npm run clean && rollup -c" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { baseConfig } from '@tiptap-shared/rollup-config' | ||
|
||
import pkg from './package.json' assert { type: 'json' } | ||
|
||
export default baseConfig({ input: 'src/index.ts', pkg }) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import type { MarkType, NodeType } from './types' | ||
|
||
/** | ||
* Props for a node renderer | ||
*/ | ||
export type NodeProps<TNodeType = any, TChildren = any> = { | ||
node: TNodeType; | ||
children?: TChildren; | ||
}; | ||
|
||
/** | ||
* Props for a mark renderer | ||
*/ | ||
export type MarkProps<TMarkType = any, TChildren = any> = { | ||
mark: TMarkType; | ||
children?: TChildren; | ||
}; | ||
|
||
export type TiptapStaticRendererOptions< | ||
/** | ||
* The return type of the render function (e.g. React.ReactNode, string) | ||
*/ | ||
TReturnType, | ||
/** | ||
* A mark type is either a JSON representation of a mark or a Prosemirror mark instance | ||
*/ | ||
TMarkType extends { type: any } = MarkType, | ||
/** | ||
* A node type is either a JSON representation of a node or a Prosemirror node instance | ||
*/ | ||
TNodeType extends { | ||
content?: { forEach: (cb: (node: TNodeType) => void) => void }; | ||
marks?: readonly TMarkType[]; | ||
type: string | { name: string }; | ||
} = NodeType, | ||
/** | ||
* A node renderer is a function that takes a node and its children and returns the rendered output | ||
*/ | ||
TNodeRender extends ( | ||
ctx: NodeProps<TNodeType, TReturnType | TReturnType[]> | ||
) => TReturnType = ( | ||
ctx: NodeProps<TNodeType, TReturnType | TReturnType[]> | ||
) => TReturnType, | ||
/** | ||
* A mark renderer is a function that takes a mark and its children and returns the rendered output | ||
*/ | ||
TMarkRender extends ( | ||
ctx: MarkProps<TMarkType, TReturnType | TReturnType[]> | ||
) => TReturnType = ( | ||
ctx: MarkProps<TMarkType, TReturnType | TReturnType[]> | ||
) => TReturnType, | ||
> = { | ||
/** | ||
* Mapping of node types to react components | ||
*/ | ||
nodeMapping: Record<string, TNodeRender>; | ||
/** | ||
* Mapping of mark types to react components | ||
*/ | ||
markMapping: Record<string, TMarkRender>; | ||
/** | ||
* Component to render if a node type is not handled | ||
*/ | ||
unhandledNode?: TNodeRender; | ||
/** | ||
* Component to render if a mark type is not handled | ||
*/ | ||
unhandledMark?: TMarkRender; | ||
}; | ||
|
||
/** | ||
* Tiptap Static Renderer | ||
* ---------------------- | ||
* | ||
* This function is a basis to allow for different renderers to be created. | ||
* Generic enough to be able to statically render Prosemirror JSON or Prosemirror Nodes. | ||
* | ||
* Using this function, you can create a renderer that takes a JSON representation of a Prosemirror document | ||
* and renders it using a mapping of node types to React components or even to a string. | ||
* This function is used as the basis to create the `reactRenderer` and `stringRenderer` functions. | ||
*/ | ||
export function TiptapStaticRenderer< | ||
/** | ||
* The return type of the render function (e.g. React.ReactNode, string) | ||
*/ | ||
TReturnType, | ||
/** | ||
* A mark type is either a JSON representation of a mark or a Prosemirror mark instance | ||
*/ | ||
TMarkType extends { type: string | { name: string } } = MarkType, | ||
/** | ||
* A node type is either a JSON representation of a node or a Prosemirror node instance | ||
*/ | ||
TNodeType extends { | ||
content?: { forEach:( | ||
cb: (node: TNodeType) => void) => void }; | ||
marks?: readonly TMarkType[]; | ||
type: string | { name: string }; | ||
} = NodeType, | ||
/** | ||
* A node renderer is a function that takes a node and its children and returns the rendered output | ||
*/ | ||
TNodeRender extends ( | ||
ctx: NodeProps<TNodeType, TReturnType | TReturnType[]> | ||
) => TReturnType = ( | ||
ctx: NodeProps<TNodeType, TReturnType | TReturnType[]> | ||
) => TReturnType, | ||
/** | ||
* A mark renderer is a function that takes a mark and its children and returns the rendered output | ||
*/ | ||
TMarkRender extends ( | ||
ctx: MarkProps<TMarkType, TReturnType | TReturnType[]> | ||
) => TReturnType = ( | ||
ctx: MarkProps<TMarkType, TReturnType | TReturnType[]> | ||
) => TReturnType, | ||
>( | ||
/** | ||
* The function that actually renders the component | ||
*/ | ||
renderComponent: ( | ||
ctx: | ||
| { | ||
component: TNodeRender; | ||
props: NodeProps<TNodeType, TReturnType | TReturnType[]>; | ||
} | ||
| { | ||
component: TMarkRender; | ||
props: MarkProps<TMarkType, TReturnType | TReturnType[]>; | ||
} | ||
) => TReturnType, | ||
{ | ||
nodeMapping, | ||
markMapping, | ||
unhandledNode, | ||
unhandledMark, | ||
}: TiptapStaticRendererOptions< | ||
TReturnType, | ||
TMarkType, | ||
TNodeType, | ||
TNodeRender, | ||
TMarkRender | ||
>, | ||
) { | ||
/** | ||
* Render Tiptap JSON and all its children using the provided node and mark mappings. | ||
*/ | ||
return function renderContent({ | ||
content, | ||
}: { | ||
/** | ||
* Tiptap JSON content to render | ||
*/ | ||
content: TNodeType; | ||
}): TReturnType { | ||
// recursively render child content nodes | ||
const children: TReturnType[] = [] | ||
|
||
if (content.content) { | ||
content.content.forEach(child => { | ||
children.push( | ||
renderContent({ | ||
content: child, | ||
}), | ||
) | ||
}) | ||
} | ||
const nodeType = typeof content.type === 'string' ? content.type : content.type.name | ||
const NodeHandler = nodeMapping[nodeType] ?? unhandledNode | ||
|
||
if (!NodeHandler) { | ||
throw new Error(`missing handler for node type ${nodeType}`) | ||
} | ||
|
||
const nodeContent = renderComponent({ | ||
component: NodeHandler, | ||
props: { node: content, children }, | ||
}) | ||
|
||
// apply marks to the content | ||
const markedContent = content.marks | ||
? content.marks.reduce((acc, mark) => { | ||
const markType = typeof mark.type === 'string' ? mark.type : mark.type.name | ||
const MarkHandler = markMapping[markType] ?? unhandledMark | ||
|
||
if (!MarkHandler) { | ||
throw new Error(`missing handler for mark type ${markType}`) | ||
} | ||
|
||
return renderComponent({ | ||
component: MarkHandler, | ||
props: { mark, node: undefined, children: acc }, | ||
}) | ||
}, nodeContent) | ||
: nodeContent | ||
|
||
return markedContent | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import { | ||
ExtensionAttribute, | ||
getAttributesFromExtensions, | ||
mergeAttributes, | ||
resolveExtensions, | ||
} from '@tiptap/core' | ||
import { TextAlign } from '@tiptap/extension-text-align' | ||
import { TextStyle } from '@tiptap/extension-text-style' | ||
import StarterKit from '@tiptap/starter-kit' | ||
|
||
import type { MarkType, NodeType } from './types' | ||
|
||
/** | ||
* This function returns the attributes of a node or mark that are defined by the given extension attributes. | ||
* @param nodeOrMark The node or mark to get the attributes from | ||
* @param extensionAttributes The extension attributes to use | ||
* @param onlyRenderedAttributes If true, only attributes that are rendered in the HTML are returned | ||
*/ | ||
export function getAttributes( | ||
nodeOrMark: NodeType | MarkType, | ||
extensionAttributes: ExtensionAttribute[], | ||
onlyRenderedAttributes?: boolean, | ||
): Record<string, any> { | ||
const nodeOrMarkAttributes = nodeOrMark.attrs | ||
|
||
if (!nodeOrMarkAttributes) { | ||
return {} | ||
} | ||
|
||
return extensionAttributes | ||
.filter(item => { | ||
if (item.type !== nodeOrMark.type) { | ||
return false | ||
} | ||
if (onlyRenderedAttributes) { | ||
return item.attribute.rendered | ||
} | ||
return true | ||
}) | ||
.map(item => { | ||
if (!item.attribute.renderHTML) { | ||
return { | ||
[item.name]: | ||
item.name in nodeOrMarkAttributes | ||
? nodeOrMarkAttributes[item.name] | ||
: item.attribute.default, | ||
} | ||
} | ||
|
||
return ( | ||
item.attribute.renderHTML(nodeOrMarkAttributes) || { | ||
[item.name]: | ||
item.name in nodeOrMarkAttributes | ||
? nodeOrMarkAttributes[item.name] | ||
: item.attribute.default, | ||
} | ||
) | ||
}) | ||
.reduce( | ||
(attributes, attribute) => mergeAttributes(attributes, attribute), | ||
{}, | ||
) | ||
} | ||
|
||
/** | ||
* This function returns the HTML attributes of a node or mark that are defined by the given extension attributes. | ||
* @param nodeOrMark The node or mark to get the attributes from | ||
* @param extensionAttributes The extension attributes to use | ||
*/ | ||
export function getHTMLAttributes( | ||
nodeOrMark: NodeType | MarkType, | ||
extensionAttributes: ExtensionAttribute[], | ||
) { | ||
return getAttributes(nodeOrMark, extensionAttributes, true) | ||
} | ||
|
||
const extensionAttributes = getAttributesFromExtensions( | ||
resolveExtensions([ | ||
StarterKit, | ||
TextAlign.configure({ | ||
types: ['paragraph', 'heading'], | ||
}), | ||
TextStyle, | ||
]), | ||
) | ||
const attributes = getAttributes( | ||
{ | ||
type: 'heading', | ||
attrs: { | ||
textAlign: 'right', | ||
}, | ||
content: [ | ||
{ | ||
type: 'text', | ||
text: 'hello world', | ||
}, | ||
], | ||
}, | ||
extensionAttributes, | ||
) | ||
|
||
console.log(attributes) |
Oops, something went wrong.