Skip to content

Commit

Permalink
feat: first version of a static renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
nperez0111 committed Aug 23, 2024
1 parent 7cdaf65 commit 520cff2
Show file tree
Hide file tree
Showing 11 changed files with 1,385 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/static-renderer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Change Log
18 changes: 18 additions & 0 deletions packages/static-renderer/README.md
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).
45 changes: 45 additions & 0 deletions packages/static-renderer/package.json
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"
}
}
5 changes: 5 additions & 0 deletions packages/static-renderer/rollup.config.js
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 })
199 changes: 199 additions & 0 deletions packages/static-renderer/src/base.ts
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
}
}
103 changes: 103 additions & 0 deletions packages/static-renderer/src/helpers.tsx
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)
Loading

0 comments on commit 520cff2

Please sign in to comment.