├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .eslintrc.js ├── .github ├── CONTRIBUTING.md ├── label.yml └── workflows │ ├── lint.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .lintstagedrc ├── .prettierrc ├── LICENSE.md ├── README.md ├── commitlint.config.js ├── examples ├── content-example.ts ├── react │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── svelte │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.svelte │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── svelte.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── vue │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ ├── App.vue │ └── main.js │ └── vite.config.js ├── lerna.json ├── package.json ├── packages ├── html-renderer │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── defaultElements.tsx │ │ ├── elements │ │ │ ├── Audio.tsx │ │ │ ├── Class.tsx │ │ │ ├── IFrame.tsx │ │ │ ├── Image.tsx │ │ │ ├── Link.tsx │ │ │ ├── Video.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── types.ts │ ├── test │ │ ├── __snapshots__ │ │ │ └── index.test.ts.snap │ │ ├── content.ts │ │ └── index.test.ts │ └── tsconfig.build.json ├── html-to-slate-ast │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── examples │ │ ├── graphql-request-script.js │ │ └── node-script.js │ ├── package.json │ ├── src │ │ └── index.ts │ ├── test │ │ ├── google-docs_input.html │ │ ├── html_input.html │ │ ├── html_input_iframe.html │ │ ├── html_input_table.html │ │ ├── image.html │ │ ├── index.test.ts │ │ ├── pre.html │ │ └── word-document.html │ ├── tsconfig.build.json │ └── tsup.config.ts ├── react-renderer │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── RenderText.tsx │ │ ├── RichText.tsx │ │ ├── defaultElements.tsx │ │ ├── elements │ │ │ ├── Audio.tsx │ │ │ ├── Class.tsx │ │ │ ├── IFrame.tsx │ │ │ ├── Image.tsx │ │ │ ├── Link.tsx │ │ │ ├── Video.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── types.ts │ ├── test │ │ ├── RichText.test.tsx │ │ ├── __snapshots__ │ │ │ └── RichText.test.tsx.snap │ │ └── content.ts │ └── tsconfig.build.json └── types │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── package.json │ ├── src │ ├── index.ts │ └── util │ │ ├── isElement.ts │ │ ├── isEmpty.ts │ │ └── isText.ts │ └── tsconfig.build.json ├── tsconfig.build.json ├── tsconfig.json ├── types └── global.d.ts └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets). 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md). 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.3.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "hygraph/rich-text" } 6 | ], 7 | "commit": false, 8 | "linked": [], 9 | "access": "public", 10 | "baseBranch": "main", 11 | "updateInternalDependencies": "patch", 12 | "ignore": [] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['react-app', 'prettier/@typescript-eslint', 'prettier'], 3 | plugins: ['testing-library', 'jest-dom', 'prettier'], 4 | settings: { 5 | react: { 6 | version: '999.999.999', 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | ## Getting started 4 | 5 | First off, we would like to thank you for taking the time to contribute and make this a better project! 6 | 7 | Here we have a set of instructions and guidelines to reduce misunderstandings and make the process of contributing to the Rich Text project as smooth as possible. We hope this guide makes the contribution process clear and answers any questions you may have. 8 | 9 | ## Local Development 10 | 11 | ### Prerequisites 12 | 13 | - [Node.js](http://nodejs.org/) >= v12 must be installed. 14 | - [Yarn](https://yarnpkg.com/en/docs/install) 15 | 16 | There are 2 packages: 17 | 18 | - `@graphcms/rich-text-react-renderer` - Rich text React renderer 19 | - `@graphcms/rich-text-types` - TypeScript definition for the Rich Text field 20 | 21 | You can install all the dependencies in the root directory. Since the monorepo uses Lerna and Yarn Workspaces, npm CLI is not supported (only yarn). 22 | 23 | ```sh 24 | yarn install 25 | ``` 26 | 27 | This will install all dependencies in each project, build them, and symlink them via Lerna. 28 | 29 | ## Development workflow 30 | 31 | In one terminal, run tsdx watch in parallel: 32 | 33 | ```sh 34 | yarn start 35 | ``` 36 | 37 | This builds each package to `//dist` and runs the project in watch mode so any edits you save inside `//src` cause a rebuild to `//dist`. The results will stream to to the terminal. 38 | 39 | ### Using the React example/playground 40 | 41 | You can play with local React packages in the Parcel-powered example/playground. 42 | 43 | ```sh 44 | yarn start:react:app 45 | ``` 46 | 47 | This will start the example/playground on `localhost:1234`. If you have lerna running watch in parallel mode in one terminal, and then you run parcel, your playground will hot reload when you make changes to any imported module whose source is inside of `packages/*/src/*`. Note that to accomplish this, each package's `start` command passes TDSX the `--noClean` flag. This prevents Parcel from exploding between rebuilds because of File Not Found errors. 48 | 49 | Important Safety Tip: When adding/altering packages in the playground, use `alias` object in package.json. This will tell Parcel to resolve them to the filesystem instead of trying to install the package from NPM. It also fixes duplicate React errors you may run into. 50 | 51 | ## Why all these rules? 52 | 53 | We try to enforce these rules for the following reasons: 54 | 55 | - Automatically generating changelog; 56 | - Communicating in a better way the nature of changes; 57 | - Triggering build and publish processes; 58 | - Automatically determining a semantic version bump (based on the types of commits); 59 | - Making it easier for people to contribute, by allowing them to explore a more structured commit history. 60 | 61 | ## Pull Requests 62 | 63 | When opening a pull request, please be sure to update any relevant documentation in the READMEs or write some additional tests to ensure functionality. Also include a high-level list of changes. 64 | 65 | ## Changesets 66 | 67 | This repository uses [changesets][] to do versioning. What that means for contributors is that you need to add a changeset by running `yarn changeset` which contains what packages should be bumped, their associated semver bump types, and some markdown which will be inserted into changelogs. 68 | 69 | ### Publish canary version 70 | 71 | To publish a canary version using `changesets`, you'll need to be in the Hygraph npm organization. Otherwise, ask a maintainer to do it for you. To get started, enter prerelease mode. You can do that with the `pre enter `. The tag that you need to pass is used in versions(e.g. `1.0.0-canary.0`) and the npm dist tag. 72 | 73 | A prerelease workflow might look something like this: 74 | 75 | ```sh 76 | yarn changeset pre enter canary 77 | yarn changeset 78 | yarn changeset version 79 | yarn build 80 | git add . 81 | git commit -m "chore(release): v1.0.0-canary.0" 82 | yarn changeset publish 83 | git push --follow-tags 84 | ``` 85 | 86 | For more information, [check this link](https://github.com/atlassian/changesets/blob/main/docs/prereleases.md). 87 | 88 | [yarn workspaces]: https://yarnpkg.com/en/docs/workspaces 89 | [changesets]: https://github.com/atlassian/changesets 90 | -------------------------------------------------------------------------------- /.github/label.yml: -------------------------------------------------------------------------------- 1 | examples: 2 | - examples/* 3 | - examples/**/* 4 | 5 | html-to-slate-ast: 6 | - packages/html-to-slate-ast/* 7 | - packages/html-to-slate-ast/**/* 8 | 9 | react-renderer: 10 | - packages/react-renderer/* 11 | - packages/react-renderer/**/* 12 | 13 | types: 14 | - packages/types/* 15 | - packages/types/**/* 16 | 17 | repo: 18 | - ./* 19 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: Set up Node 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: lts/* 18 | 19 | - name: Install deps and build (with cache) 20 | uses: bahmutov/npm-install@v1 21 | 22 | - name: Lint 23 | run: yarn lint 24 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | name: Build and test on Node ${{ matrix.node }} and ${{ matrix.os }} 8 | 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | node: ['18.x', '20.x'] 13 | os: [ubuntu-latest] 14 | 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@v2 18 | 19 | - name: Use Node ${{ matrix.node }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node }} 23 | 24 | - name: Install deps and build (with cache) 25 | uses: bahmutov/npm-install@v1 26 | 27 | - name: Test 28 | run: yarn test --ci --coverage --maxWorkers=2 29 | 30 | - name: Run Node.js Environment Test for @graphcms/html-to-slate-ast 31 | run: yarn test:node 32 | working-directory: ./packages/html-to-slate-ast 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up Node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: lts/* 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Create Release Pull Request or Publish to npm 26 | uses: changesets/action@master 27 | with: 28 | commit: 'chore(release): publish' 29 | publish: yarn release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | 6 | node_modules 7 | package-lock.json 8 | yarn.lock 9 | !/yarn.lock 10 | coverage/ 11 | 12 | .idea 13 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "**/*.{ts,tsx}": [ 3 | "yarn lint --fix", 4 | "yarn format" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hygraph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rich Text Helpers 2 | 3 | A set of companion packages for Hygraph's Rich Text Field 4 | 5 | ## ✨ Packages 6 | 7 | - [rich-text-html-renderer](./packages/html-renderer): Framework agnostic Rich Text renderer. 8 | - [rich-text-react-renderer](./packages/react-renderer): Out of the box Rich Text renderer for React; 9 | - [rich-text-types](./packages/types): TypeScript definitions for the Hygraph Rich Text field type; 10 | - [html-to-slate-ast](./packages/html-to-slate-ast): HTML to Slate AST converter for the Hygraph's RichTextAST format. 11 | 12 | ## ⚡️ Examples (Rich Text Renderer) 13 | 14 | - [Vue](./examples/vue) 15 | - [Svelte](./examples/svelte/) 16 | - [React](./examples/react/) 17 | 18 | ## 🤝 Contributing 19 | 20 | Thanks for being interested in contributing! We're so glad you want to help! All types of contributions are welcome, such as bug fixes, issues, or feature requests. Also, don't forget to check the roadmap. See [`CONTRIBUTING.md`](./.github/CONTRIBUTING.md) for ways to get started. 21 | 22 | ## 📝 License 23 | 24 | Licensed under the [MIT License](./LICENSE.md). 25 | 26 | --- 27 | 28 | Made with 💜 by Hygraph 👋 [join our community](https://slack.hygraph.com/)! 29 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /examples/content-example.ts: -------------------------------------------------------------------------------- 1 | import { EmbedReferences, RichTextContent } from '@graphcms/rich-text-types'; 2 | 3 | export const content: RichTextContent = { 4 | children: [ 5 | { 6 | type: 'heading-two', 7 | children: [{ text: 'Awesome new Hygraph cap!' }], 8 | }, 9 | { 10 | type: 'paragraph', 11 | children: [ 12 | { text: 'Sweet black ' }, 13 | { bold: true, text: 'cap' }, 14 | { text: ' ' }, 15 | { text: 'with', underline: true }, 16 | { text: ' ' }, 17 | { text: 'embroidered', italic: true }, 18 | { text: ' ' }, 19 | { bold: true, text: 'Hygraph' }, 20 | { text: ' logo.' }, 21 | ], 22 | }, 23 | { 24 | type: 'bulleted-list', 25 | children: [ 26 | { 27 | type: 'list-item', 28 | children: [ 29 | { 30 | type: 'list-item-child', 31 | children: [{ text: 'Embroided logo' }], 32 | }, 33 | ], 34 | }, 35 | { 36 | type: 'list-item', 37 | children: [ 38 | { 39 | type: 'list-item-child', 40 | children: [{ text: 'Fits well' }], 41 | }, 42 | ], 43 | }, 44 | { 45 | type: 'list-item', 46 | children: [ 47 | { 48 | type: 'list-item-child', 49 | children: [{ text: 'Comes in black' }], 50 | }, 51 | ], 52 | }, 53 | { 54 | type: 'list-item', 55 | children: [ 56 | { 57 | type: 'list-item-child', 58 | children: [{ text: 'Reasonably priced' }], 59 | }, 60 | ], 61 | }, 62 | ], 63 | }, 64 | { type: 'paragraph', children: [{ text: '', code: true }] }, 65 | { 66 | type: 'code-block', 67 | children: [ 68 | { 69 | text: "const teste = 'teste'", 70 | }, 71 | ], 72 | }, 73 | { 74 | type: 'code-block', 75 | children: [ 76 | { 77 | text: 'const hy = \'graph\'\n\n', 78 | }, 79 | ], 80 | }, 81 | { 82 | type: 'embed', 83 | nodeId: 'ckrus0f14ao760b32mz2dwvgx', 84 | children: [ 85 | { 86 | text: '', 87 | }, 88 | ], 89 | nodeType: 'Asset', 90 | }, 91 | { 92 | type: 'embed', 93 | nodeId: 'ckrxv7b74g8il0d782lf66dup', 94 | children: [ 95 | { 96 | text: '', 97 | }, 98 | ], 99 | nodeType: 'Asset', 100 | }, 101 | { 102 | type: 'embed', 103 | nodeId: 'ckrxv6otkg6ez0c8743xp9bzs', 104 | children: [ 105 | { 106 | text: '', 107 | }, 108 | ], 109 | nodeType: 'Asset', 110 | }, 111 | { 112 | type: 'embed', 113 | nodeId: 'custom_post_id', 114 | children: [ 115 | { 116 | text: '', 117 | }, 118 | ], 119 | nodeType: 'Post', 120 | }, 121 | ], 122 | }; 123 | 124 | export const references: EmbedReferences = [ 125 | { 126 | id: 'ckrus0f14ao760b32mz2dwvgx', 127 | handle: '7M0lXLdCQfeIDXnT2SVS', 128 | fileName: 'file_example_MP4_480_1_5MG.mp4', 129 | height: null, 130 | width: null, 131 | url: 'https://media.graphassets.com/7M0lXLdCQfeIDXnT2SVS', 132 | mimeType: 'video/mp4', 133 | }, 134 | { 135 | id: 'ckrxv7b74g8il0d782lf66dup', 136 | handle: '7VA0p81VQfmZQC9jPB2I', 137 | fileName: 'teste.txt', 138 | height: null, 139 | width: null, 140 | url: 'https://media.graphassets.com/7VA0p81VQfmZQC9jPB2I', 141 | mimeType: 'text/plain', 142 | }, 143 | { 144 | id: 'ckrxv6otkg6ez0c8743xp9bzs', 145 | handle: 'HzsAGQyASM2B6B3dHY0n', 146 | fileName: 'pdf-test.pdf', 147 | height: null, 148 | width: null, 149 | url: 'https://media.graphassets.com/HzsAGQyASM2B6B3dHY0n', 150 | mimeType: 'application/pdf', 151 | }, 152 | { 153 | id: 'custom_post_id', 154 | title: 'Hygraph is awesome :rocket:', 155 | }, 156 | ]; 157 | -------------------------------------------------------------------------------- /examples/react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Example 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "prismjs": "^1.27.0", 12 | "react": "^17.0.2", 13 | "react-dom": "^17.0.2" 14 | }, 15 | "devDependencies": { 16 | "@types/prismjs": "^1.26.0", 17 | "@types/react": "^17.0.33", 18 | "@types/react-dom": "^17.0.10", 19 | "@vitejs/plugin-react": "^1.0.7", 20 | "typescript": "^4.5.4", 21 | "vite": "^2.8.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { RichText } from '@graphcms/rich-text-react-renderer'; 4 | 5 | import Prism from 'prismjs'; 6 | import 'prismjs/plugins/line-numbers/prism-line-numbers'; 7 | import 'prismjs/themes/prism-tomorrow.css'; 8 | import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; 9 | 10 | import { content, references } from '../../content-example'; 11 | 12 | export default function App() { 13 | React.useEffect(() => { 14 | Prism.highlightAll(); 15 | }, []); 16 | 17 | return ( 18 |
19 |

React example

20 | 21 |

{children}

, 26 | blockquote: ({ children }) => ( 27 |
34 | {children} 35 |
36 | ), 37 | a: ({ children, href, openInNewTab }) => ( 38 | 44 | {children} 45 | 46 | ), 47 | h2: ({ children }) => ( 48 |

{children}

49 | ), 50 | bold: ({ children }) => {children}, 51 | code_block: ({ children }) => { 52 | return ( 53 |
54 |                 {children}
55 |               
56 | ); 57 | }, 58 | Asset: { 59 | application: () => ( 60 |
61 |

Asset

62 |
63 | ), 64 | text: () => ( 65 |
66 |

text plain

67 |
68 | ), 69 | }, 70 | }} 71 | /> 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /examples/react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ); 11 | -------------------------------------------------------------------------------- /examples/react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import { join } from 'path'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | '@graphcms/rich-text-types': join(__dirname, '../../packages/types'), 10 | '@graphcms/rich-text-react-renderer': join( 11 | __dirname, 12 | '../../packages/react-renderer' 13 | ), 14 | }, 15 | }, 16 | plugins: [react()], 17 | }); 18 | -------------------------------------------------------------------------------- /examples/svelte/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/svelte/README.md: -------------------------------------------------------------------------------- 1 | ## Rich Text Renderer with Svelte 2 | 3 | This example shows how to use the Hygraph Rich Text Renderer package with Svelte. 4 | -------------------------------------------------------------------------------- /examples/svelte/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Svelte Example 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json" 11 | }, 12 | "devDependencies": { 13 | "@sveltejs/vite-plugin-svelte": "^1.0.0-next.30", 14 | "@tsconfig/svelte": "^2.0.1", 15 | "svelte": "^3.44.0", 16 | "svelte-check": "^2.2.7", 17 | "svelte-preprocess": "^4.9.8", 18 | "tslib": "^2.3.1", 19 | "typescript": "^4.5.4", 20 | "vite": "^2.8.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/svelte/src/App.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 |

Svelte Example

21 | 22 |
{@html html}
23 |
24 | -------------------------------------------------------------------------------- /examples/svelte/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.getElementById('app') as Element, 5 | }); 6 | 7 | export default app; 8 | -------------------------------------------------------------------------------- /examples/svelte/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/svelte/svelte.config.js: -------------------------------------------------------------------------------- 1 | import sveltePreprocess from 'svelte-preprocess' 2 | 3 | export default { 4 | // Consult https://github.com/sveltejs/svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: sveltePreprocess() 7 | } 8 | -------------------------------------------------------------------------------- /examples/svelte/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "resolveJsonModule": true, 8 | "baseUrl": ".", 9 | /** 10 | * Typecheck JS in `.svelte` and `.js` files by default. 11 | * Disable checkJs if you'd like to use dynamic types in JS. 12 | * Note that setting allowJs false does not prevent the use 13 | * of JS in `.svelte` files. 14 | */ 15 | "allowJs": true, 16 | "checkJs": true 17 | }, 18 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /examples/svelte/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/svelte/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 3 | import { join } from 'path'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | '@graphcms/rich-text-types': join(__dirname, '../../packages/types'), 10 | '@graphcms/rich-text-html-renderer': join( 11 | __dirname, 12 | '../../packages/html-renderer' 13 | ), 14 | }, 15 | }, 16 | plugins: [svelte()], 17 | }); 18 | -------------------------------------------------------------------------------- /examples/vue/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/vue/README.md: -------------------------------------------------------------------------------- 1 | ## Rich Text Renderer with Vue 2 | 3 | This example shows how to use the Hygraph Rich Text Renderer package with Vue. 4 | -------------------------------------------------------------------------------- /examples/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vue Example 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "vue": "^3.2.25" 12 | }, 13 | "devDependencies": { 14 | "@vitejs/plugin-vue": "^2.2.0", 15 | "vite": "^2.8.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 28 | -------------------------------------------------------------------------------- /examples/vue/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /examples/vue/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import path from 'path'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | '@graphcms/rich-text-types': path.join(__dirname, '../../packages/types'), 10 | '@graphcms/rich-text-html-renderer': path.join( 11 | __dirname, 12 | '../../packages/html-renderer' 13 | ), 14 | }, 15 | }, 16 | plugins: [vue()], 17 | }); 18 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independant", 3 | "registry": "https://registry.npmjs.org/", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "npmClient": "yarn", 8 | "useWorkspaces": true, 9 | "packages": ["packages/*"] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "@changesets/changelog-github": "^0.2.7", 6 | "@changesets/cli": "^2.10.3", 7 | "@commitlint/cli": "^12.1.1", 8 | "@commitlint/config-conventional": "^12.1.1", 9 | "@testing-library/jest-dom": "^5.12.0", 10 | "@testing-library/react": "^11.2.6", 11 | "@types/node": "^15.12.4", 12 | "@types/react": "^17.0.4", 13 | "@types/react-dom": "^17.0.3", 14 | "eslint-plugin-jest-dom": "^3.9.0", 15 | "eslint-plugin-testing-library": "^4.2.1", 16 | "husky": "^6.0.0", 17 | "lerna": "^3.15.0", 18 | "lint-staged": "^11.0.0", 19 | "react": "^17.0.2", 20 | "react-dom": "^17.0.2", 21 | "tsdx": "^0.14.1", 22 | "typescript": "^4.2.4" 23 | }, 24 | "workspaces": [ 25 | "packages/*" 26 | ], 27 | "scripts": { 28 | "lerna": "lerna", 29 | "start": "lerna run start --stream --parallel", 30 | "test": "yarn prepublish && lerna run test --", 31 | "lint": "lerna run lint -- --fix", 32 | "format": "prettier --ignore-path .gitignore \"**/*.+(ts|tsx)\" --write", 33 | "build": "lerna run build", 34 | "prepublish": "lerna run prepublish", 35 | "changeset": "changeset", 36 | "release": "changeset publish", 37 | "prepare": "husky install" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/html-renderer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @graphcms/rich-text-html-renderer 2 | 3 | ## 0.3.1 4 | 5 | ### Patch Changes 6 | 7 | - [`c8a5a7c`](https://github.com/hygraph/rich-text/commit/c8a5a7c409efd8570e77bd28a66352eb9e519a42) [#127](https://github.com/hygraph/rich-text/pull/127) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - fix: add closing tag for iframe 8 | 9 | ## 0.3.0 10 | 11 | ### Minor Changes 12 | 13 | - [`7992b80`](https://github.com/hygraph/rich-text/commit/7992b80923dad0b88cd55b406770fdc15a050743) [#107](https://github.com/hygraph/rich-text/pull/107) Thanks [@rbastiansch](https://github.com/rbastiansch)! - Export `defaultElements` 14 | 15 | ## 0.2.0 16 | 17 | ### Minor Changes 18 | 19 | - [`b272253`](https://github.com/hygraph/rich-text/commit/b2722534275efd2c5e473d549d0f0e5a28100025) [#84](https://github.com/hygraph/rich-text/pull/84) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Adds support for Link Embeds 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [[`b272253`](https://github.com/hygraph/rich-text/commit/b2722534275efd2c5e473d549d0f0e5a28100025)]: 24 | - @graphcms/rich-text-types@0.5.0 25 | 26 | ## 0.1.1 27 | 28 | ### Patch Changes 29 | 30 | - [`12cb7f9`](https://github.com/hygraph/rich-text/commit/12cb7f914cf9d1404e0783c168d61910e346a391) [#81](https://github.com/hygraph/rich-text/pull/81) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Add escape-html as a dependency 31 | 32 | ## 0.1.0 33 | 34 | ### Minor Changes 35 | 36 | - [`b7f16fa`](https://github.com/hygraph/rich-text/commit/b7f16fa76a28ad0f5cdbe6cb1f58d7fafa63df15) [#77](https://github.com/hygraph/rich-text/pull/77) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Initial version of the `html-renderer` for Rich Text content. 37 | 38 | Features 39 | 40 | - `astToHtmlString` function for returning HTML 41 | - Types for the package 42 | - Vue and Svelte examples 43 | 44 | ### Patch Changes 45 | 46 | - Updated dependencies [[`b7f16fa`](https://github.com/hygraph/rich-text/commit/b7f16fa76a28ad0f5cdbe6cb1f58d7fafa63df15), [`b7f16fa`](https://github.com/hygraph/rich-text/commit/b7f16fa76a28ad0f5cdbe6cb1f58d7fafa63df15)]: 47 | - @graphcms/rich-text-types@0.4.0 48 | -------------------------------------------------------------------------------- /packages/html-renderer/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Hygraph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/html-renderer/README.md: -------------------------------------------------------------------------------- 1 | # @graphcms/rich-text-html-renderer 2 | 3 | Render Rich Text content from Hygraph in any application. 4 | 5 | ## ⚡ Getting started 6 | 7 | You can get it on npm or Yarn. 8 | 9 | ```sh 10 | # npm 11 | npm i @graphcms/rich-text-html-renderer 12 | 13 | # Yarn 14 | yarn add @graphcms/rich-text-html-renderer 15 | ``` 16 | 17 | ## 🔥 Usage/Examples 18 | 19 | To render the content on your application, you'll need to provide the array of elements returned from the Hygraph API to the `astToHtmlString` function. The content has to be returned in `raw` (or `json`) format as the AST representation. For more information on how to query the Rich Text content, [check our documentation](https://hygraph.com/docs/api-reference/schema/field-types#rich-text). 20 | 21 | ```js 22 | import { astToHtmlString } from '@graphcms/rich-text-html-renderer'; 23 | 24 | const content = { 25 | children: [ 26 | { 27 | type: 'paragraph', 28 | children: [ 29 | { 30 | bold: true, 31 | text: 'Hello World!', 32 | }, 33 | ], 34 | }, 35 | ], 36 | }; 37 | 38 | const html = astToHtmlString({ 39 | content, 40 | }); 41 | ``` 42 | 43 | The content from the example above will render: 44 | 45 | ```html 46 |

47 | Hello world! 48 |

49 | ``` 50 | 51 | ## Custom elements 52 | 53 | By default, the elements won't have any styling, despite the `IFrame`, which we designed to be responsive. If you need to customize the elements, you can do it using the renderers argument. 54 | 55 | ```js 56 | import { astToHtmlString } from '@graphcms/rich-text-html-renderer'; 57 | 58 | const content = { 59 | /* ... */ 60 | }; 61 | 62 | const html = astToHtmlString({ 63 | content: inlineContent, 64 | renderers: { 65 | bold: ({ children }) => `${children}`, 66 | }, 67 | }); 68 | ``` 69 | 70 | If needed, you can also import the `defaultElements` from the package and use it as a base for your custom renderers. 71 | 72 | ```js 73 | import { 74 | astToHtmlString, 75 | defaultElements, 76 | } from '@graphcms/rich-text-html-renderer'; 77 | 78 | const content = { 79 | /* ... */ 80 | }; 81 | 82 | const html = astToHtmlString({ 83 | content: inlineContent, 84 | renderers: { 85 | bold: props => defaultElements.bold(props), 86 | }, 87 | }); 88 | ``` 89 | 90 | Below you can check the full list of elements you can customize, alongside the props available for each of them. 91 | 92 | - `a` 93 | - `children`: string; 94 | - `href`: string; 95 | - `className`: string; 96 | - `rel`: string; 97 | - `id`: string; 98 | - `title`: string; 99 | - `openInNewTab`: boolean; 100 | - `class` 101 | - `children`: string; 102 | - `className`: string; 103 | - `img` 104 | - `src`: string; 105 | - `title`: string; 106 | - `width`: number; 107 | - `height`: number; 108 | - `mimeType`: ImageMimeTypes; 109 | - `altText`: string; 110 | - `video` 111 | - `src`: string; 112 | - `title`: string; 113 | - `width`: number; 114 | - `height`: number; 115 | - `iframe` 116 | - `url`: string; 117 | - `width`: number; 118 | - `height`: number; 119 | - `h1` 120 | - `children`: string; 121 | - `h2` 122 | - `children`: string; 123 | - `h3` 124 | - `children`: string; 125 | - `h4` 126 | - `children`: string; 127 | - `h5` 128 | - `children`: string; 129 | - `h6` 130 | - `children`: string; 131 | - `p` 132 | - `children`: string; 133 | - `ul` 134 | - `children`: string; 135 | - `ol` 136 | - `children`: string; 137 | - `li` 138 | - `children`: string; 139 | - `table` 140 | - `children`: string; 141 | - `table_head` 142 | - `children`: string; 143 | - `table_header_cell` 144 | - `children`: string; 145 | - `table_body` 146 | - `children`: string; 147 | - `table_row` 148 | - `children`: string; 149 | - `table_cell` 150 | - `children`: string; 151 | - `blockquote` 152 | - `children`: string; 153 | - `bold` 154 | - `children`: string; 155 | - `italic` 156 | - `children`: string; 157 | - `underline` 158 | - `children`: string; 159 | - `code` 160 | - `children`: string; 161 | - `code_block` 162 | - `children`: string; 163 | 164 | ## Custom assets 165 | 166 | The Rich Text field allows you to embed assets. By default, we render images, videos and audios out of the box. However, you can define custom components for each mime type group. Below you can see the complete list of `mimeType` groups. 167 | 168 | - `audio` 169 | - `application` 170 | - `image` 171 | - `video` 172 | - `font` 173 | - `model` 174 | - `text` 175 | 176 | We don't have components to render fonts, models, text and application files, but you can write your own depending on your needs and project. If you need, you can also have a custom renderer for a specific `mimeType`. Here's an example: 177 | 178 | ```js 179 | import { astToHtmlString } from '@graphcms/rich-text-html-renderer'; 180 | 181 | const content = [ 182 | { 183 | type: 'embed', 184 | nodeId: 'cknjbzowggjo90b91kjisy03a', 185 | children: [ 186 | { 187 | text: '', 188 | }, 189 | ], 190 | nodeType: 'Asset', 191 | }, 192 | { 193 | type: 'embed', 194 | nodeId: 'ckrus0f14ao760b32mz2dwvgx', 195 | children: [ 196 | { 197 | text: '', 198 | }, 199 | ], 200 | nodeType: 'Asset', 201 | }, 202 | ]; 203 | 204 | const references = [ 205 | { 206 | id: 'cknjbzowggjo90b91kjisy03a', 207 | url: 'https://media.graphassets.com/dsQtt0ARqO28baaXbVy9', 208 | mimeType: 'image/png', 209 | }, 210 | { 211 | id: 'ckrus0f14ao760b32mz2dwvgx', 212 | url: 'https://media.graphassets.com/7M0lXLdCQfeIDXnT2SVS', 213 | mimeType: 'video/mp4', 214 | }, 215 | ]; 216 | 217 | const html = astToHtmlString({ 218 | content, 219 | references, 220 | renderers: { 221 | Asset: { 222 | video: () => `
custom VIDEO
`, 223 | image: () => `
custom IMAGE
`, 224 | 'video/mp4': () => { 225 | return `
custom video/mp4 renderer
`; 226 | }, 227 | }, 228 | }, 229 | }); 230 | ``` 231 | 232 | As mentioned, you can write renderers for all `mimeType` groups or to specific `mimeType`. 233 | 234 | ### References 235 | 236 | References are required on the `astToHtmlString` function to render embed assets. 237 | 238 | `id`, `mimeType` and `url` are required in your `Asset` query. 239 | 240 | **Query example:** 241 | 242 | ```graphql 243 | { 244 | articles { 245 | content { 246 | json 247 | references { 248 | ... on Asset { 249 | id 250 | url 251 | mimeType 252 | } 253 | } 254 | } 255 | } 256 | } 257 | ``` 258 | 259 | ## Custom embeds 260 | 261 | Imagine you have an embed `Post` on your Rich Text field. To render it, you can have a custom renderer. Let's see an example: 262 | 263 | ```js 264 | import { astToHtmlString } from '@graphcms/rich-text-html-renderer'; 265 | 266 | const content = [ 267 | { 268 | type: 'embed', 269 | nodeId: 'custom_post_id', 270 | children: [ 271 | { 272 | text: '', 273 | }, 274 | ], 275 | nodeType: 'Post', 276 | }, 277 | ]; 278 | 279 | const references = [ 280 | { 281 | id: 'custom_post_id', 282 | title: 'Hygraph is awesome :rocket:', 283 | }, 284 | ]; 285 | 286 | const html = astToHtmlString({ 287 | content, 288 | references, 289 | renderers: { 290 | embed: { 291 | Post: ({ title, nodeId }) => { 292 | return ` 293 |
294 |

${title}

295 |

${nodeId}

296 |
297 | `; 298 | }, 299 | }, 300 | }, 301 | }); 302 | ``` 303 | 304 | ### References 305 | 306 | References are required on the `astToHtmlString` function. You also need to include your model in your query. 307 | 308 | - `id` is always required in your model query. It won't render if it's not present. 309 | 310 | ```graphql 311 | { 312 | articles { 313 | content { 314 | json 315 | references { 316 | ... on Asset { 317 | id 318 | url 319 | mimeType 320 | } 321 | # Your post query 322 | ... on Post { 323 | id # required 324 | title 325 | slug 326 | description 327 | } 328 | } 329 | } 330 | } 331 | } 332 | ``` 333 | 334 | ### Link embeds 335 | 336 | The Rich Text Field also supports Link Embeds, which work similarly to normal embeds. Based on the model name, you can have a custom renderer for it. Example: 337 | 338 | ```js 339 | import { astToHtmlString } from '@graphcms/rich-text-html-renderer'; 340 | 341 | const content = [ 342 | { 343 | type: 'link', 344 | nodeId: 'post_id', 345 | children: [ 346 | { 347 | text: 'click here', 348 | }, 349 | ], 350 | nodeType: 'Post', 351 | }, 352 | ]; 353 | 354 | const references = [ 355 | { 356 | id: 'post_id', 357 | slug: 'hygraph-is-awesome', 358 | }, 359 | ]; 360 | 361 | const html = astToHtmlString({ 362 | content: contentObject, 363 | references, 364 | renderers: { 365 | link: { 366 | Article: ({ slug, children }) => { 367 | return `${children}`; 368 | }, 369 | }, 370 | }, 371 | }); 372 | ``` 373 | 374 | ## Empty elements 375 | 376 | By default, we remove empty headings from the element list to prevent SEO issues. Other elements, such as `thead` are also removed. You can find the complete list [here](https://github.com/hygraph/rich-text/blob/main/packages/types/src/index.ts#L168). 377 | 378 | ## TypeScript 379 | 380 | If you are using TypeScript in your project, we recommend installing the `@graphcms/rich-text-types` package. It contains types for the elements, alongside the props accepted by them. You can use them in your application to create custom components. 381 | 382 | ### Children Type 383 | 384 | If you need to type the content from the Rich Text field, you can do so by using the types package. Example: 385 | 386 | ```ts 387 | import { ElementNode } from '@graphcms/rich-text-types'; 388 | 389 | type Content = { 390 | content: { 391 | raw: { 392 | children: ElementNode[]; 393 | }; 394 | }; 395 | }; 396 | ``` 397 | 398 | ### Custom Embeds/Assets 399 | 400 | Depending on your reference query and model, fields may change, which applies to types. To have a better DX using the package, we have `EmbedProps` and `LinkEmbedProps` types that you can import from `@graphcms/rich-text-types` (you may need to install it if you don't have done it already). 401 | 402 | In this example, we have seen how to write a renderer for a `Post` model, but it applies the same way to any other model and `Asset` on your project. 403 | 404 | ```ts 405 | import { astToHtmlString } from '@graphcms/rich-text-html-renderer'; 406 | import { EmbedProps, LinkEmbedProps } from '@graphcms/rich-text-types'; 407 | 408 | type Post = { 409 | title: string; 410 | slug: string; 411 | description: string; 412 | }; 413 | 414 | const content = { 415 | /* ... */ 416 | }; 417 | 418 | const references = [ 419 | /* ... */ 420 | ]; 421 | 422 | const html = astToHtmlString({ 423 | content, 424 | references, 425 | renderers: { 426 | embed: { 427 | Post: ({ title, description, slug }: EmbedProps) => { 428 | return ` 429 | 435 | `; 436 | }, 437 | }, 438 | link: { 439 | Article: ({ slug, children }) => { 440 | return `${children}`; 441 | }, 442 | }, 443 | }, 444 | }); 445 | ``` 446 | 447 | ## 📝 License 448 | 449 | Licensed under the MIT License. 450 | 451 | --- 452 | 453 | Made with 💜 by Hygraph 👋 [join our community](https://slack.hygraph.com/)! 454 | -------------------------------------------------------------------------------- /packages/html-renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphcms/rich-text-html-renderer", 3 | "description": "Hygraph Rich Text HTML renderer", 4 | "version": "0.3.1", 5 | "author": "João Pedro Schmitz (https://joaopedro.dev)", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "tsdx watch --tsconfig tsconfig.build.json --verbose --noClean", 9 | "build": "tsdx build --tsconfig tsconfig.build.json", 10 | "test": "tsdx test --passWithNoTests --silent", 11 | "lint": "tsdx lint", 12 | "prepublish": "npm run build" 13 | }, 14 | "dependencies": { 15 | "@graphcms/rich-text-types": "^0.5.0", 16 | "escape-html": "^1.0.3" 17 | }, 18 | "devDependencies": { 19 | "@types/escape-html": "^1.0.2" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "keywords": [ 25 | "html", 26 | "rich-text", 27 | "renderer", 28 | "hygraph" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/hygraph/rich-text.git", 33 | "directory": "packages/html-renderer" 34 | }, 35 | "main": "dist/index.js", 36 | "module": "dist/rich-text-html-renderer.esm.js", 37 | "types": "dist/index.d.ts", 38 | "files": [ 39 | "README.md", 40 | "LICENSE.md", 41 | "dist" 42 | ], 43 | "jest": { 44 | "setupFilesAfterEnv": [ 45 | "@testing-library/jest-dom/extend-expect" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/html-renderer/src/defaultElements.tsx: -------------------------------------------------------------------------------- 1 | import { RichTextProps } from './types'; 2 | 3 | import { IFrame, Image, Video, Class, Link, Audio } from './elements'; 4 | 5 | function FallbackForCustomAsset({ mimeType }: { mimeType: string }) { 6 | if (__DEV__) { 7 | console.warn( 8 | `[@graphcms/rich-text-html-renderer]: Unsupported mimeType encountered: ${mimeType}. You need to write your renderer to render it since we are not opinionated about how this asset should be rendered (check our docs for more info).` 9 | ); 10 | } 11 | 12 | return ``; 13 | } 14 | 15 | export const defaultElements: Required = { 16 | a: Link, 17 | class: Class, 18 | video: Video, 19 | img: Image, 20 | iframe: IFrame, 21 | blockquote: ({ children }) => `
${children}
`, 22 | ul: ({ children }) => `
    ${children}
`, 23 | ol: ({ children }) => `
    ${children}
`, 24 | li: ({ children }) => `
  • ${children}
  • `, 25 | p: ({ children }) => `

    ${children}

    `, 26 | h1: ({ children }) => `

    ${children}

    `, 27 | h2: ({ children }) => `

    ${children}

    `, 28 | h3: ({ children }) => `

    ${children}

    `, 29 | h4: ({ children }) => `

    ${children}

    `, 30 | h5: ({ children }) => `
    ${children}
    `, 31 | h6: ({ children }) => `
    ${children}
    `, 32 | table: ({ children }) => `${children}
    `, 33 | table_head: ({ children }) => `${children}`, 34 | table_body: ({ children }) => `${children}`, 35 | table_row: ({ children }) => `${children}`, 36 | table_cell: ({ children }) => `${children}`, 37 | table_header_cell: ({ children }) => `${children}`, 38 | bold: ({ children }) => `${children}`, 39 | italic: ({ children }) => `${children}`, 40 | underline: ({ children }) => `${children}`, 41 | code: ({ children }) => `${children}`, 42 | code_block: ({ children }) => 43 | `
    52 |       ${children}
    53 |     
    `, 54 | list_item_child: ({ children }) => `${children}`, 55 | Asset: { 56 | audio: Audio, 57 | image: props => Image({ ...props, src: props.url }), 58 | video: props => Video({ ...props, src: props.url }), 59 | font: FallbackForCustomAsset, 60 | application: FallbackForCustomAsset, 61 | model: FallbackForCustomAsset, 62 | text: FallbackForCustomAsset, 63 | }, 64 | embed: {}, 65 | link: {}, 66 | }; 67 | -------------------------------------------------------------------------------- /packages/html-renderer/src/elements/Audio.tsx: -------------------------------------------------------------------------------- 1 | export type AudioProps = { 2 | url: string; 3 | }; 4 | 5 | export function Audio({ url }: AudioProps) { 6 | return ` 7 | 18 | `; 19 | } 20 | -------------------------------------------------------------------------------- /packages/html-renderer/src/elements/Class.tsx: -------------------------------------------------------------------------------- 1 | import { ClassRendererProps } from '../types'; 2 | 3 | export function Class({ className, children }: ClassRendererProps) { 4 | return `
    ${children}
    `; 5 | } 6 | -------------------------------------------------------------------------------- /packages/html-renderer/src/elements/IFrame.tsx: -------------------------------------------------------------------------------- 1 | import escapeHtml from 'escape-html'; 2 | import { IFrameProps } from '@graphcms/rich-text-types'; 3 | 4 | export function IFrame({ url }: Partial) { 5 | return ` 6 |
    14 | 30 |
    31 | `; 32 | } 33 | -------------------------------------------------------------------------------- /packages/html-renderer/src/elements/Image.tsx: -------------------------------------------------------------------------------- 1 | import escapeHtml from 'escape-html'; 2 | import { ImageProps } from '@graphcms/rich-text-types'; 3 | 4 | export function Image({ 5 | src, 6 | width, 7 | height, 8 | altText, 9 | title, 10 | }: Partial) { 11 | if (__DEV__ && !src) { 12 | console.warn( 13 | `[@graphcms/rich-text-html-renderer]: src is required. You need to include a \`url\` in your query` 14 | ); 15 | } 16 | 17 | return ` 18 | 23 | `; 24 | } 25 | -------------------------------------------------------------------------------- /packages/html-renderer/src/elements/Link.tsx: -------------------------------------------------------------------------------- 1 | import escapeHtml from 'escape-html'; 2 | import { LinkRendererProps } from '../types'; 3 | 4 | export function Link({ children, ...rest }: LinkRendererProps) { 5 | const { href, rel, id, title, openInNewTab, className } = rest; 6 | 7 | return ` 8 | 13 | ${children} 14 | 15 | `; 16 | } 17 | -------------------------------------------------------------------------------- /packages/html-renderer/src/elements/Video.tsx: -------------------------------------------------------------------------------- 1 | import escapeHtml from 'escape-html'; 2 | import { VideoProps } from '@graphcms/rich-text-types'; 3 | 4 | export function Video({ src, width, height, title }: Partial) { 5 | return ` 6 | 13 | `; 14 | } 15 | -------------------------------------------------------------------------------- /packages/html-renderer/src/elements/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Audio'; 2 | export * from './IFrame'; 3 | export * from './Image'; 4 | export * from './Video'; 5 | export * from './Class'; 6 | export * from './Link'; 7 | -------------------------------------------------------------------------------- /packages/html-renderer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ElementNode, 3 | elementTypeKeys, 4 | EmbedReferences, 5 | isElement, 6 | isEmpty, 7 | isText, 8 | Node, 9 | EmptyElementsToRemove, 10 | Text, 11 | } from '@graphcms/rich-text-types'; 12 | import escape from 'escape-html'; 13 | 14 | import { defaultElements } from './defaultElements'; 15 | import { RichTextProps, NodeRendererType } from './types'; 16 | 17 | function getArrayOfElements(content: RichTextProps['content']) { 18 | return Array.isArray(content) ? content : content.children; 19 | } 20 | 21 | function serialize(text: string) { 22 | if (text.includes('\n')) { 23 | const splitText = text.split('\n'); 24 | 25 | return splitText 26 | .map( 27 | (line, index) => 28 | `${line}${index === splitText.length - 1 ? '' : '
    '}` 29 | ) 30 | .join(''); 31 | } 32 | 33 | return text; 34 | } 35 | 36 | type RenderText = { 37 | textNode: Text; 38 | renderers?: RichTextProps['renderers']; 39 | shouldSerialize: boolean | null; 40 | }; 41 | 42 | function renderText({ shouldSerialize, textNode, renderers }: RenderText) { 43 | const { text, bold, italic, underline, code } = textNode; 44 | 45 | const escapedText = escape(text); 46 | let parsedText = shouldSerialize ? serialize(escapedText) : escapedText; 47 | 48 | const Bold: NodeRendererType['bold'] = renderers?.['bold']; 49 | const Italic: NodeRendererType['italic'] = renderers?.['italic']; 50 | const Underline: NodeRendererType['underline'] = renderers?.['underline']; 51 | const Code: NodeRendererType['code'] = renderers?.['code']; 52 | 53 | if (bold && Bold) { 54 | parsedText = Bold({ children: parsedText as string }); 55 | } 56 | 57 | if (italic && Italic) { 58 | parsedText = Italic({ children: parsedText as string }); 59 | } 60 | 61 | if (underline && Underline) { 62 | parsedText = Underline({ children: parsedText as string }); 63 | } 64 | 65 | if (code && Code) { 66 | parsedText = Code({ children: parsedText as string }); 67 | } 68 | 69 | return parsedText as string; 70 | } 71 | 72 | type RenderElement = { 73 | element: ElementNode; 74 | renderers?: NodeRendererType; 75 | references?: EmbedReferences; 76 | }; 77 | 78 | function renderElement({ 79 | element, 80 | references, 81 | renderers, 82 | }: RenderElement): string { 83 | const { children, type, ...rest } = element; 84 | const { nodeId, nodeType } = rest; 85 | 86 | if (type in EmptyElementsToRemove && isEmpty({ children })) { 87 | return ``; 88 | } 89 | 90 | const isEmbed = nodeId && nodeType; 91 | 92 | const referenceValues = isEmbed 93 | ? references?.filter(ref => ref.id === nodeId)[0] 94 | : null; 95 | 96 | if (__DEV__ && isEmbed && !referenceValues?.id) { 97 | console.error( 98 | `[@graphcms/rich-text-html-renderer]: No id found for embed node ${nodeId}. In order to render custom embeds, \`id\` is required in your reference query.` 99 | ); 100 | 101 | return ``; 102 | } 103 | 104 | if ( 105 | __DEV__ && 106 | isEmbed && 107 | nodeType === 'Asset' && 108 | !referenceValues?.mimeType 109 | ) { 110 | console.error( 111 | `[@graphcms/rich-text-html-renderer]: No mimeType found for embed node ${nodeId}. In order to render custom assets, \`mimeType\` is required in your reference query.` 112 | ); 113 | 114 | return ``; 115 | } 116 | 117 | if (__DEV__ && isEmbed && nodeType === 'Asset' && !referenceValues?.url) { 118 | console.error( 119 | `[@graphcms/rich-text-html-renderer]: No url found for embed node ${nodeId}. In order to render custom assets, \`url\` is required in your reference query.` 120 | ); 121 | 122 | return ``; 123 | } 124 | 125 | let elementToRender; 126 | 127 | if (isEmbed && nodeType !== 'Asset') { 128 | const element = 129 | type === 'link' 130 | ? renderers?.link?.[nodeType as string] 131 | : renderers?.embed?.[nodeType as string]; 132 | 133 | if (element !== undefined) { 134 | elementToRender = element; 135 | } else { 136 | console.warn( 137 | `[@graphcms/rich-text-html-renderer]: No renderer found for custom ${type} nodeType ${nodeType}.` 138 | ); 139 | return ``; 140 | } 141 | } 142 | 143 | if (isEmbed && nodeType === 'Asset') { 144 | const element = renderers?.Asset?.[referenceValues?.mimeType]; 145 | 146 | if (element !== undefined) { 147 | elementToRender = element; 148 | } else { 149 | const mimeTypeGroup = referenceValues?.mimeType.split('/')[0]; 150 | elementToRender = renderers?.Asset?.[mimeTypeGroup]; 151 | } 152 | } 153 | 154 | const elementNodeRenderer = isEmbed 155 | ? elementToRender 156 | : renderers?.[elementTypeKeys[type] as keyof RichTextProps['renderers']]; 157 | 158 | if (elementNodeRenderer) { 159 | const props = { ...rest, ...referenceValues }; 160 | 161 | const nextElements = renderElements({ 162 | content: children as ElementNode[], 163 | renderers, 164 | references, 165 | parent: element, 166 | }).join(''); 167 | 168 | return elementNodeRenderer({ ...props, children: nextElements }); 169 | } 170 | 171 | return ``; 172 | } 173 | 174 | type RenderNode = { 175 | node: Node; 176 | parent: Node | null; 177 | renderers?: NodeRendererType; 178 | references?: EmbedReferences; 179 | }; 180 | 181 | function renderNode({ 182 | node, 183 | parent, 184 | references, 185 | renderers, 186 | }: RenderNode): string { 187 | if (isText(node)) { 188 | const shouldSerialize = 189 | parent && isElement(parent) && parent.type !== 'code-block'; 190 | 191 | return renderText({ 192 | shouldSerialize, 193 | textNode: node, 194 | renderers, 195 | }); 196 | } 197 | 198 | if (isElement(node)) { 199 | return renderElement({ 200 | element: node, 201 | renderers, 202 | references, 203 | }); 204 | } 205 | 206 | const { type } = node as ElementNode; 207 | 208 | if (__DEV__) { 209 | console.warn( 210 | `[@graphcms/rich-text-html-renderer]: Unknown node type encountered: ${type}` 211 | ); 212 | } 213 | 214 | return ``; 215 | } 216 | 217 | type RenderElements = RichTextProps & { 218 | parent?: Node | null; 219 | }; 220 | 221 | function renderElements({ 222 | content, 223 | parent, 224 | references, 225 | renderers, 226 | }: RenderElements) { 227 | const elements = getArrayOfElements(content); 228 | 229 | return elements.map(node => { 230 | return renderNode({ 231 | node, 232 | parent: parent || null, 233 | renderers, 234 | references, 235 | }); 236 | }); 237 | } 238 | 239 | export function astToHtmlString({ 240 | renderers: resolvers, 241 | content, 242 | references, 243 | }: RichTextProps): string { 244 | const assetRenderers = { 245 | ...defaultElements?.Asset, 246 | ...resolvers?.Asset, 247 | }; 248 | 249 | const renderers: NodeRendererType = { 250 | ...defaultElements, 251 | ...resolvers, 252 | Asset: assetRenderers, 253 | }; 254 | 255 | if (__DEV__ && !content) { 256 | console.error(`[@graphcms/rich-text-html-renderer]: content is required.`); 257 | 258 | return ``; 259 | } 260 | 261 | if (__DEV__ && !Array.isArray(content) && !content.children) { 262 | console.error( 263 | `[@graphcms/rich-text-html-renderer]: children is required in content.` 264 | ); 265 | 266 | return ``; 267 | } 268 | 269 | /* 270 | Checks if there's a embed type inside the content and if the `references` prop is defined 271 | 272 | If it isn't defined and there's embed elements, it will show a warning 273 | */ 274 | if (__DEV__) { 275 | const elements = getArrayOfElements(content); 276 | 277 | const embedElements = elements.filter(element => element.type === 'embed'); 278 | 279 | if (embedElements.length > 0 && !references) { 280 | console.warn( 281 | `[@graphcms/rich-text-html-renderer]: to render embed elements you need to provide the \`references\` prop` 282 | ); 283 | } 284 | } 285 | 286 | return renderElements({ 287 | content, 288 | references, 289 | renderers, 290 | }).join(''); 291 | } 292 | 293 | export { defaultElements } from './defaultElements'; 294 | export * from './types'; 295 | -------------------------------------------------------------------------------- /packages/html-renderer/src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EmbedReferences, 3 | IFrameProps, 4 | ImageProps, 5 | RichTextContent, 6 | VideoProps, 7 | ClassProps, 8 | LinkProps, 9 | } from '@graphcms/rich-text-types'; 10 | 11 | export interface DefaultElementProps { 12 | children: string; 13 | } 14 | 15 | export interface ClassRendererProps 16 | extends DefaultElementProps, 17 | Partial {} 18 | 19 | export interface LinkRendererProps 20 | extends DefaultElementProps, 21 | Partial {} 22 | 23 | type DefaultNodeRenderer = (props: DefaultElementProps) => string; 24 | type LinkNodeRenderer = (props: LinkRendererProps) => string; 25 | type ClassNodeRenderer = (props: ClassRendererProps) => string; 26 | type ImageNodeRenderer = (props: Partial) => string; 27 | type VideoNodeRenderer = (props: Partial) => string; 28 | type IFrameNodeRenderer = (props: Partial) => string; 29 | type EmbedNodeRenderer = (props: any) => string; 30 | 31 | export type NodeRendererType = { 32 | a?: LinkNodeRenderer; 33 | class?: ClassNodeRenderer; 34 | img?: ImageNodeRenderer; 35 | video?: VideoNodeRenderer; 36 | iframe?: IFrameNodeRenderer; 37 | h1?: DefaultNodeRenderer; 38 | h2?: DefaultNodeRenderer; 39 | h3?: DefaultNodeRenderer; 40 | h4?: DefaultNodeRenderer; 41 | h5?: DefaultNodeRenderer; 42 | h6?: DefaultNodeRenderer; 43 | p?: DefaultNodeRenderer; 44 | ul?: DefaultNodeRenderer; 45 | ol?: DefaultNodeRenderer; 46 | li?: DefaultNodeRenderer; 47 | list_item_child?: DefaultNodeRenderer; 48 | table?: DefaultNodeRenderer; 49 | table_head?: DefaultNodeRenderer; 50 | table_body?: DefaultNodeRenderer; 51 | table_row?: DefaultNodeRenderer; 52 | table_cell?: DefaultNodeRenderer; 53 | table_header_cell?: DefaultNodeRenderer; 54 | blockquote?: DefaultNodeRenderer; 55 | bold?: DefaultNodeRenderer; 56 | italic?: DefaultNodeRenderer; 57 | underline?: DefaultNodeRenderer; 58 | code?: DefaultNodeRenderer; 59 | code_block?: DefaultNodeRenderer; 60 | Asset?: { 61 | application?: EmbedNodeRenderer; 62 | audio?: EmbedNodeRenderer; 63 | font?: EmbedNodeRenderer; 64 | image?: EmbedNodeRenderer; 65 | model?: EmbedNodeRenderer; 66 | text?: EmbedNodeRenderer; 67 | video?: EmbedNodeRenderer; 68 | [key: string]: EmbedNodeRenderer | undefined; 69 | }; 70 | embed?: { 71 | [key: string]: EmbedNodeRenderer | undefined; 72 | }; 73 | link?: { 74 | [key: string]: EmbedNodeRenderer | undefined; 75 | }; 76 | }; 77 | 78 | export type RichTextProps = { 79 | content: RichTextContent; 80 | references?: EmbedReferences; 81 | renderers?: NodeRendererType; 82 | }; 83 | -------------------------------------------------------------------------------- /packages/html-renderer/test/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`custom embeds and assets should render video, image, and audio assets 1`] = ` 4 | " 5 | 6 | 7 | 13 | 14 | 25 | " 26 | `; 27 | -------------------------------------------------------------------------------- /packages/html-renderer/test/content.ts: -------------------------------------------------------------------------------- 1 | import { RichTextContent } from '@graphcms/rich-text-types'; 2 | 3 | export const defaultContent: RichTextContent = [ 4 | { 5 | type: 'paragraph', 6 | children: [ 7 | { 8 | bold: true, 9 | text: 'Hello World!', 10 | }, 11 | ], 12 | }, 13 | ]; 14 | 15 | export const emptyContent: RichTextContent = [ 16 | { 17 | type: 'heading-two', 18 | children: [ 19 | { 20 | text: '', 21 | }, 22 | { 23 | href: 'https://hygraph.com', 24 | type: 'link', 25 | children: [ 26 | { 27 | text: 'Testing Link', 28 | }, 29 | ], 30 | }, 31 | ], 32 | }, 33 | { 34 | type: 'heading-two', 35 | children: [ 36 | { 37 | text: '', 38 | }, 39 | { 40 | href: 'https://hygraph.com', 41 | type: 'link', 42 | children: [ 43 | { 44 | text: 'Link', 45 | }, 46 | ], 47 | }, 48 | { 49 | text: ' 2', 50 | }, 51 | ], 52 | }, 53 | { 54 | type: 'heading-one', 55 | children: [ 56 | { 57 | text: '', 58 | }, 59 | ], 60 | }, 61 | { 62 | type: 'heading-two', 63 | children: [ 64 | { 65 | text: '', 66 | }, 67 | ], 68 | }, 69 | { 70 | type: 'heading-three', 71 | children: [ 72 | { 73 | text: '', 74 | }, 75 | ], 76 | }, 77 | { 78 | type: 'heading-four', 79 | children: [ 80 | { 81 | text: '', 82 | }, 83 | ], 84 | }, 85 | { 86 | type: 'heading-five', 87 | children: [ 88 | { 89 | text: '', 90 | }, 91 | ], 92 | }, 93 | { 94 | type: 'table', 95 | children: [ 96 | { 97 | type: 'table_head', 98 | children: [ 99 | { 100 | text: '', 101 | }, 102 | ], 103 | }, 104 | { 105 | type: 'table_body', 106 | children: [ 107 | { 108 | type: 'table_row', 109 | children: [ 110 | { 111 | type: 'table_cell', 112 | children: [ 113 | { 114 | type: 'paragraph', 115 | children: [ 116 | { 117 | text: 'Row 1 - Col 1', 118 | }, 119 | ], 120 | }, 121 | ], 122 | }, 123 | { 124 | type: 'table_cell', 125 | children: [ 126 | { 127 | type: 'paragraph', 128 | children: [ 129 | { 130 | text: 'Row 1 - Col 2', 131 | }, 132 | ], 133 | }, 134 | ], 135 | }, 136 | ], 137 | }, 138 | ], 139 | }, 140 | ], 141 | }, 142 | ]; 143 | 144 | export const tableContent: RichTextContent = [ 145 | { 146 | type: 'table', 147 | children: [ 148 | { 149 | type: 'table_head', 150 | children: [ 151 | { 152 | type: 'table_row', 153 | children: [ 154 | { 155 | type: 'table_header_cell', 156 | children: [ 157 | { 158 | type: 'paragraph', 159 | children: [ 160 | { 161 | text: 'Row 1 - Header 1', 162 | }, 163 | ], 164 | }, 165 | ], 166 | }, 167 | { 168 | type: 'table_header_cell', 169 | children: [ 170 | { 171 | type: 'paragraph', 172 | children: [ 173 | { 174 | text: 'Row 1 - Header 2', 175 | }, 176 | ], 177 | }, 178 | ], 179 | }, 180 | ], 181 | }, 182 | ], 183 | }, 184 | { 185 | type: 'table_body', 186 | children: [ 187 | { 188 | type: 'table_row', 189 | children: [ 190 | { 191 | type: 'table_cell', 192 | children: [ 193 | { 194 | type: 'paragraph', 195 | children: [ 196 | { 197 | text: 'Row 2 - Col 1', 198 | }, 199 | ], 200 | }, 201 | ], 202 | }, 203 | { 204 | type: 'table_cell', 205 | children: [ 206 | { 207 | type: 'paragraph', 208 | children: [ 209 | { 210 | text: 'Row 2 - Col 2', 211 | }, 212 | ], 213 | }, 214 | ], 215 | }, 216 | ], 217 | }, 218 | ], 219 | }, 220 | ], 221 | }, 222 | ]; 223 | 224 | export const simpleH1Content: RichTextContent = [ 225 | { 226 | type: 'heading-one', 227 | children: [ 228 | { 229 | text: 'heading', 230 | }, 231 | ], 232 | }, 233 | ]; 234 | 235 | export const inlineContent: RichTextContent = [ 236 | { 237 | type: 'paragraph', 238 | children: [ 239 | { 240 | text: 'Hey, ', 241 | bold: true, 242 | }, 243 | { 244 | text: 'how', 245 | italic: true, 246 | }, 247 | { 248 | text: 'are', 249 | underline: true, 250 | }, 251 | { 252 | text: 'you?', 253 | code: true, 254 | }, 255 | ], 256 | }, 257 | ]; 258 | 259 | export const iframeContent: RichTextContent = [ 260 | { 261 | type: 'class', 262 | children: [ 263 | { 264 | type: 'paragraph', 265 | children: [ 266 | { 267 | text: 'wow', 268 | }, 269 | ], 270 | }, 271 | ], 272 | className: 'test', 273 | }, 274 | ]; 275 | 276 | export const imageContent: RichTextContent = [ 277 | { 278 | src: 279 | 'https://media.graphassets.com/output=format:webp/resize=,width:667,height:1000/8xrjYm4CR721mAZ1YAoy', 280 | type: 'image', 281 | title: 'photo-1564631027894-5bdb17618445.jpg', 282 | width: 667, 283 | handle: '8xrjYm4CR721mAZ1YAoy', 284 | height: 1000, 285 | altText: 'photo-1564631027894-5bdb17618445.jpg', 286 | children: [ 287 | { 288 | text: '', 289 | }, 290 | ], 291 | mimeType: 'image/webp', 292 | }, 293 | ]; 294 | 295 | export const videoContent: RichTextContent = [ 296 | { 297 | src: 'https://media.graphassets.com/oWd7OYr5Q5KGRJW9ujRO', 298 | type: 'video', 299 | title: 'file_example_MP4_480_1_5MG.m4v', 300 | width: 400, 301 | handle: 'oWd7OYr5Q5KGRJW9ujRO', 302 | height: 400, 303 | children: [ 304 | { 305 | text: '', 306 | }, 307 | ], 308 | }, 309 | ]; 310 | 311 | export const listContent: RichTextContent = [ 312 | { 313 | type: 'bulleted-list', 314 | children: [ 315 | { 316 | type: 'list-item', 317 | children: [ 318 | { 319 | type: 'list-item-child', 320 | children: [{ text: 'Embroided logo' }], 321 | }, 322 | ], 323 | }, 324 | { 325 | type: 'list-item', 326 | children: [ 327 | { type: 'list-item-child', children: [{ text: 'Fits well' }] }, 328 | ], 329 | }, 330 | { 331 | type: 'list-item', 332 | children: [ 333 | { 334 | type: 'list-item-child', 335 | children: [{ text: 'Comes in black' }], 336 | }, 337 | ], 338 | }, 339 | { 340 | type: 'list-item', 341 | children: [ 342 | { 343 | type: 'list-item-child', 344 | children: [{ text: 'Reasonably priced' }], 345 | }, 346 | ], 347 | }, 348 | ], 349 | }, 350 | ]; 351 | 352 | export const embedAssetContent: RichTextContent = [ 353 | { 354 | type: 'embed', 355 | nodeId: 'ckrxv7b74g8il0d782lf66dup', 356 | children: [ 357 | { 358 | text: '', 359 | }, 360 | ], 361 | nodeType: 'Asset', 362 | }, 363 | { 364 | type: 'embed', 365 | nodeId: 'ckrxv6otkg6ez0c8743xp9bzs', 366 | children: [ 367 | { 368 | text: '', 369 | }, 370 | ], 371 | nodeType: 'Asset', 372 | }, 373 | { 374 | type: 'embed', 375 | nodeId: 'cknjbzowggjo90b91kjisy03a', 376 | children: [ 377 | { 378 | text: '', 379 | }, 380 | ], 381 | nodeType: 'Asset', 382 | }, 383 | { 384 | type: 'embed', 385 | nodeId: 'ckrus0f14ao760b32mz2dwvgx', 386 | children: [ 387 | { 388 | text: '', 389 | }, 390 | ], 391 | nodeType: 'Asset', 392 | }, 393 | { 394 | type: 'embed', 395 | nodeId: 'ckryzom5si5vw0d78d13bnwix', 396 | children: [ 397 | { 398 | text: '', 399 | }, 400 | ], 401 | nodeType: 'Asset', 402 | }, 403 | { 404 | type: 'embed', 405 | nodeId: 'cks2osfk8t19a0b32vahjhn36', 406 | children: [ 407 | { 408 | text: '', 409 | }, 410 | ], 411 | nodeType: 'Asset', 412 | }, 413 | { 414 | type: 'embed', 415 | nodeId: 'ckq2eek7c00ek0d83iakzoxuh', 416 | children: [ 417 | { 418 | text: '', 419 | }, 420 | ], 421 | nodeType: 'Asset', 422 | }, 423 | { 424 | type: 'embed', 425 | nodeId: 'model_example', 426 | children: [ 427 | { 428 | text: '', 429 | }, 430 | ], 431 | nodeType: 'Asset', 432 | }, 433 | ]; 434 | 435 | export const nestedEmbedAssetContent: RichTextContent = [ 436 | { 437 | type: 'paragraph', 438 | children: [ 439 | { 440 | text: 'Inline asset', 441 | }, 442 | { 443 | type: 'embed', 444 | nodeId: 'ckrus0f14ao760b32mz2dwvgx', 445 | children: [ 446 | { 447 | text: '', 448 | }, 449 | ], 450 | nodeType: 'Asset', 451 | }, 452 | { 453 | text: 'continued', 454 | }, 455 | ], 456 | }, 457 | ]; 458 | -------------------------------------------------------------------------------- /packages/html-renderer/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": ["src", "types", "../../types"], 4 | "compilerOptions": { 5 | "typeRoots": ["./node_modules/@types"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @graphcms/html-to-slate-ast 2 | 3 | ## 0.14.1 4 | 5 | ### Patch Changes 6 | 7 | - [`fa2b896`](https://github.com/hygraph/rich-text/commit/fa2b8969dd8b58209565844cfef2a127ca203d59) [#121](https://github.com/hygraph/rich-text/pull/121) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Fix jsdom being bundled with the package 8 | 9 | ## 0.14.0 10 | 11 | ### Minor Changes 12 | 13 | - [`c831239`](https://github.com/hygraph/rich-text/commit/c8312392b3371ba58a9b7c1fed30696ba9b2a9f7) [#115](https://github.com/hygraph/rich-text/pull/115) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Add htmlToSlateASTSync function 14 | 15 | ### Patch Changes 16 | 17 | - [`786beef`](https://github.com/hygraph/rich-text/commit/786beef2a0736e26239e5d6267567961d64f97ea) [#115](https://github.com/hygraph/rich-text/pull/115) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Fix htmlToSlateASTSync not working 18 | 19 | * [`bb5a39a`](https://github.com/hygraph/rich-text/commit/bb5a39aec1b91dc02de18729c3bd5c9af6bf3e5c) [#115](https://github.com/hygraph/rich-text/pull/115) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Revert export changes which breaks types 20 | 21 | - [`2cac5c4`](https://github.com/hygraph/rich-text/commit/2cac5c4e20f6882ac5588c31197a6be723b2294e) [#115](https://github.com/hygraph/rich-text/pull/115) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Correctly export it from /client 22 | 23 | * [`eb9ffd6`](https://github.com/hygraph/rich-text/commit/eb9ffd693dd3abe285a5f37608c51304e0b0b75e) [#115](https://github.com/hygraph/rich-text/pull/115) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Export htmlToSlateASTSync from /client 24 | 25 | - [`9faadd1`](https://github.com/hygraph/rich-text/commit/9faadd1138de3cf38bef56aad86197713ec5b340) [#115](https://github.com/hygraph/rich-text/pull/115) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Export htmlToSlateASTSync from index 26 | 27 | ## 0.14.0-canary.5 28 | 29 | ### Patch Changes 30 | 31 | - Revert export changes which breaks types 32 | 33 | ## 0.14.0-canary.4 34 | 35 | ### Patch Changes 36 | 37 | - Export htmlToSlateASTSync from index 38 | 39 | ## 0.14.0-canary.3 40 | 41 | ### Patch Changes 42 | 43 | - Correctly export it from /client 44 | 45 | ## 0.14.0-canary.2 46 | 47 | ### Patch Changes 48 | 49 | - Export htmlToSlateASTSync from /client 50 | 51 | ## 0.14.0-canary.1 52 | 53 | ### Patch Changes 54 | 55 | - Fix htmlToSlateASTSync not working 56 | 57 | ## 0.14.0-canary.0 58 | 59 | ### Minor Changes 60 | 61 | - Add htmlToSlateASTSync function 62 | 63 | ## 0.13.3 64 | 65 | ### Patch Changes 66 | 67 | - [`4774158`](https://github.com/hygraph/rich-text/commit/477415821d347c2265d304e0146d0c138f2bb5dc) [#112](https://github.com/hygraph/rich-text/pull/112) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Update @braintree/sanitize-url to fix vulnerability issue 68 | 69 | ## 0.13.2 70 | 71 | ### Patch Changes 72 | 73 | - [`2cf8a43`](https://github.com/hygraph/rich-text/commit/2cf8a43e9a3d77672e29f52bb500317b4e3d2db6) [#110](https://github.com/hygraph/rich-text/pull/110) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Updated dev and peer dependencies alongside docs to fix vulnerability warning report 74 | 75 | ## 0.13.1 76 | 77 | ### Patch Changes 78 | 79 | - [`c28a87d`](https://github.com/hygraph/rich-text/commit/c28a87d64b5ab3ed09b0dd8b840a3a806aa61eba) [#95](https://github.com/hygraph/rich-text/pull/95) Thanks [@anmolarora1](https://github.com/anmolarora1)! - fix: replace hardcoded iframe url with src 80 | 81 | ## 0.13.0 82 | 83 | ### Minor Changes 84 | 85 | - [`37b3d32`](https://github.com/hygraph/rich-text/commit/37b3d3292b4c7b31dd388e32c6ba9619571cc352) [#93](https://github.com/hygraph/rich-text/pull/93) Thanks [@anmolarora1](https://github.com/anmolarora1)! - - Add IFrame support 86 | - Add bold text support to table cells 87 | - Add nested tags support to table cells 88 | 89 | ## 0.12.1 90 | 91 | ### Patch Changes 92 | 93 | - Updated dependencies [[`b272253`](https://github.com/hygraph/rich-text/commit/b2722534275efd2c5e473d549d0f0e5a28100025)]: 94 | - @graphcms/rich-text-types@0.5.0 95 | 96 | ## 0.12.0 97 | 98 | ### Minor Changes 99 | 100 | - [`3f454e8`](https://github.com/hygraph/rich-text/commit/3f454e82d2c84506b70554af75b66971858e238f) [#78](https://github.com/hygraph/rich-text/pull/78) Thanks [@larisachristie](https://github.com/larisachristie)! - Parse GCMS embeds 101 | 102 | ### Patch Changes 103 | 104 | - Updated dependencies [[`b7f16fa`](https://github.com/hygraph/rich-text/commit/b7f16fa76a28ad0f5cdbe6cb1f58d7fafa63df15)]: 105 | - @graphcms/rich-text-types@0.4.0 106 | 107 | ## 0.11.1 108 | 109 | ### Patch Changes 110 | 111 | - [`30a4886`](https://github.com/hygraph/rich-text/commit/30a4886511a313075cd36c2c38f2891ceaf95ad8) [#72](https://github.com/hygraph/rich-text/pull/72) Thanks [@larisachristie](https://github.com/larisachristie)! - Clarify the htmlToSlateAST Readme on mutation variable 112 | 113 | ## 0.11.0 114 | 115 | ### Minor Changes 116 | 117 | - [`17f5244`](https://github.com/hygraph/rich-text/commit/17f52440c3fae398f8fd49d4ef61a6fe46ff8635) [#53](https://github.com/hygraph/rich-text/pull/53) Thanks [@anmolarora1](https://github.com/anmolarora1)! - feat: adds `` table_header_cell element support 118 | 119 | ### Patch Changes 120 | 121 | - Updated dependencies [[`c2e0a75`](https://github.com/hygraph/rich-text/commit/c2e0a75e995591bb299250f4d14092b1843b1183)]: 122 | - @graphcms/rich-text-types@0.3.1 123 | 124 | ## 0.10.1 125 | 126 | ### Patch Changes 127 | 128 | - Updated dependencies [[`bc9e612`](https://github.com/hygraph/rich-text/commit/bc9e61293ec0535328541c95c33e71f51ec09c43)]: 129 | - @graphcms/rich-text-types@0.3.0 130 | 131 | ## 0.10.0 132 | 133 | ### Minor Changes 134 | 135 | - [`28455b3`](https://github.com/hygraph/rich-text/commit/28455b3cb7407785ed6ddce3dfd6d29504888f01) [#48](https://github.com/hygraph/rich-text/pull/48) Thanks [@larisachristie](https://github.com/larisachristie)! - Refactor nested lists handling 136 | 137 | ## 0.9.0 138 | 139 | ### Minor Changes 140 | 141 | - [`f6871a6`](https://github.com/hygraph/rich-text/commit/f6871a60e56af84b6c6276a84a0e6cb1d95dd062) [#39](https://github.com/hygraph/rich-text/pull/39) Thanks [@larisachristie](https://github.com/larisachristie)! - Add marks to links; apply multiple marks if multiple style attributes are present; wrap li tag content in a list-item-child node; add tests 142 | 143 | ## 0.8.1 144 | 145 | ### Patch Changes 146 | 147 | - [`9222643`](https://github.com/hygraph/rich-text/commit/9222643f6ac086bcca3d227138ec3deeb2af910b) [#37](https://github.com/hygraph/rich-text/pull/37) Thanks [@igneosaur][https://github.com/igneosaur] for the report! - Fix a regression on NodeJS caused by the direct use of the window object instead of a jsdom fallback 148 | 149 | ## 0.8.0 150 | 151 | ### Minor Changes 152 | 153 | - [`5a618d5`](https://github.com/hygraph/rich-text/commit/5a618d5a53703f1e0a2a76815a7f9b0f9c98df80) [#34](https://github.com/hygraph/rich-text/pull/34) Thanks [@larisachristie](https://github.com/larisachristie)! - Update Slate; refactor types; fix pre tag handling; wrap parentless breaks in a paragraph; do not add thead to headless tables 154 | 155 | ## 0.7.0 156 | 157 | ### Minor Changes 158 | 159 | - [`8e2a3a4`](https://github.com/hygraph/rich-text/commit/8e2a3a4660176eb957977f2b01c3c26c79e54dd2) [#31](https://github.com/hygraph/rich-text/pull/31) Thanks [@larisachristie](https://github.com/larisachristie)! - Populate empty children array with text node 160 | 161 | ## 0.6.0 162 | 163 | ### Minor Changes 164 | 165 | - [`a594c49`](https://github.com/hygraph/rich-text/commit/a594c49620fe27346f39ec3f0fd44d84927a70f7) [#27](https://github.com/hygraph/rich-text/pull/27) Thanks [@larisachristie](https://github.com/larisachristie)! - Fix text node when pasting images; sanitize URLs 166 | 167 | ## 0.5.1 168 | 169 | ### Patch Changes 170 | 171 | - Updated dependencies [[`768492a`](https://github.com/hygraph/rich-text/commit/768492a5dd5e642cc639b82cd7e13f2ce7f2dc96)]: 172 | - @graphcms/rich-text-types@0.2.0 173 | 174 | ## 0.5.0 175 | 176 | ### Minor Changes 177 | 178 | - [`b2c8f91`](https://github.com/hygraph/rich-text/commit/b2c8f9163abe9e1f50aaf3da5e242a8beb0efe31) [#23](https://github.com/hygraph/rich-text/pull/23) Thanks [@larisachristie](https://github.com/larisachristie)! - Fix the AST shape of a converted copy-pasted image 179 | 180 | ## 0.4.0 181 | 182 | ### Minor Changes 183 | 184 | - [`eea403f`](https://github.com/hygraph/rich-text/commit/eea403faf1074f3532b4697296014c3c436083d0) [#21](https://github.com/hygraph/rich-text/pull/21) Thanks [@notrab](https://github.com/notrab)! - Pass supported attributes to links 185 | 186 | ## 0.3.0 187 | 188 | ### Minor Changes 189 | 190 | - [`90a3f7d`](https://github.com/hygraph/rich-text/commit/90a3f7d6c1e135bb1d9a8e57fda49cb0e24a1c53) [#18](https://github.com/hygraph/rich-text/pull/18) Thanks [@OKJulian](https://github.com/OKJulian)! - Fix window check in node 191 | 192 | ## 0.2.0 193 | 194 | ### Minor Changes 195 | 196 | - [`672b2b9`](https://github.com/hygraph/rich-text/commit/672b2b97566d6ecf2f9071a1fff0b2e172bdc56d) [#12](https://github.com/hygraph/rich-text/pull/12) Thanks [@feychenie](https://github.com/feychenie)! - @graphcms/html-to-slate-ast is now isomorphic and async. It uses DOMParser in the browser, and jsdom in node. 197 | 198 | ## 0.1.2 199 | 200 | ### Patch Changes 201 | 202 | - [`7cb7b7e`](https://github.com/hygraph/rich-text/commit/7cb7b7ef78a465c54982f81c77432d001ea9645b) [#9](https://github.com/hygraph/rich-text/pull/9) Thanks [@feychenie](https://github.com/feychenie)! - Moved html-to-slate-ast package to this repo. 203 | 204 | - Updated dependencies [[`7cb7b7e`](https://github.com/hygraph/rich-text/commit/7cb7b7ef78a465c54982f81c77432d001ea9645b)]: 205 | - @graphcms/rich-text-types@0.1.3 206 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Hygraph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/README.md: -------------------------------------------------------------------------------- 1 | # @graphcms/html-to-slate-ast 2 | 3 | HTML to Slate AST converter for the Hygraph's RichTextAST format. 4 | 5 | > ⚠️ This converter outputs the custom flavour of Slate AST that is used at Hygraph, and will most likely not produce an output compatible with your own Slate implementation. But feel free to fork it and adapt it to your needs. 6 | 7 | ## ⚡ Usage 8 | 9 | > Note: If you're using this package with Node.js, you'll need to use version 18 or higher. 10 | 11 | ### 1. Install 12 | 13 | This package needs to have the packages `slate` and `slate-hyperscript` installed, and `jsdom` as well if you need to run the converter in Node.js. 14 | 15 | ```bash 16 | npm install slate@0.66.1 slate-hyperscript@0.67.0 @graphcms/html-to-slate-ast 17 | 18 | # for Node.js or isomorphic usage, jsdom is required 19 | npm install jsdom 20 | ``` 21 | 22 | ### 2. Convert your data 23 | 24 | If you are using Node.js, you will need to use the `htmlToSlateAST` function, which returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises). If you are using this package in the browser, you can use the `htmlToSlateASTSync` function, which is synchronous and doesn't require `jsdom`. 25 | 26 | ```js 27 | import { htmlToSlateAST } from '@graphcms/html-to-slate-ast'; 28 | // Or if you are using this package in the browser 29 | import { htmlToSlateASTSync } from '@graphcms/html-to-slate-ast'; 30 | 31 | async function main() { 32 | const htmlString = '

    test

    '; // or import from a file or database 33 | const ast = await htmlToSlateAST(htmlString); 34 | console.log(JSON.stringify(ast, null, 2)); 35 | } 36 | 37 | main() 38 | .then(() => process.exit(0)) 39 | .catch(e => console.error(e)); 40 | ``` 41 | 42 | ### 3. Use it in your Content API mutations 43 | 44 | The output of this converstion is compatible with our [`RichTextAST`](https://hygraph.com/docs/api-reference/content-api/rich-text-field) GraphQL type and can be used to import content in your Rich Text fields. Here's a mutation example: 45 | 46 | ```graphql 47 | mutation newArticle($title: String!, $content: RichTextAST) { 48 | createArticle(data: { title: $title, content: $content }) { 49 | id 50 | title 51 | content { 52 | html 53 | raw 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | The output generated by `htmlToSlateAST` _will represent the `children` array_ of the [Slate editor object](https://docs.slatejs.org/api/nodes/editor). However, when creating or updating the value of a Rich text field, you are setting the value of the editor node itself. This means that the output should be transformed into a Rich text compatible object, for example: 60 | 61 | ```js 62 | const data = await client.request(newArticleQuery, { 63 | title: 'Example title for an article', 64 | content: { children: ast }, 65 | }); 66 | ``` 67 | 68 | Here, in terms of Slate, `$content` is the editor node, so the `$ast` array must be assigned to the `children` key in that object. If you don't assign it to the `children` key, the mutation will fail with the following error. 69 | 70 | ``` 71 | ClientError: could not transform richText: Values should be an array of objects containing raw rich text values. 72 | ``` 73 | 74 | You can see the full example using [graphql-request](https://github.com/prisma-labs/graphql-request) to mutate the data into Hygraph [here](https://github.com/hygraph/rich-text/blob/main/packages/html-to-slate-ast/examples/graphql-request-script.js). 75 | 76 | See the docs about the [Rich Text field type](https://hygraph.com/docs/schema/field-types#rich-text) and [Content Api mutations](https://hygraph.com/docs/content-api/mutations). 77 | 78 | ## 📝 License 79 | 80 | Licensed under the MIT License. 81 | 82 | --- 83 | 84 | Made with 💜 by Hygraph 👋 [join our community](https://slack.hygraph.com/)! 85 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/examples/graphql-request-script.js: -------------------------------------------------------------------------------- 1 | import { GraphQLClient, gql } from 'graphql-request'; 2 | import { htmlToSlateAST } from '@graphcms/html-to-slate-ast'; 3 | 4 | const client = new GraphQLClient(`${process.env.HYGRAPH_ENDPOINT}`, { 5 | headers: { 6 | Authorization: `Bearer ${process.env.HYGRAPH_TOKEN}`, 7 | }, 8 | }); 9 | 10 | const newArticleQuery = gql` 11 | mutation newArticle($title: String!, $content: RichTextAST) { 12 | createArticle(data: { title: $title, content: $content }) { 13 | id 14 | title 15 | content { 16 | html 17 | raw 18 | } 19 | } 20 | } 21 | `; 22 | 23 | async function main() { 24 | const htmlString = ''; 25 | const ast = await htmlToSlateAST(htmlString); 26 | 27 | // Create a RichText object from the AST 28 | const content = { 29 | children: ast, 30 | }; 31 | 32 | const data = await client.request(newArticleQuery, { 33 | title: 'Example title for an article', 34 | content, // Pass the RichText object as the content 35 | }); 36 | 37 | console.log(data); 38 | } 39 | 40 | main() 41 | .then(() => process.exit(0)) 42 | .catch(e => console.error(e)); 43 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/examples/node-script.js: -------------------------------------------------------------------------------- 1 | /* 2 | Simple script that makes sure the library works on a barebones node environment (non-browser) 3 | */ 4 | const { htmlToSlateAST } = require('../dist'); 5 | 6 | async function main() { 7 | const htmlString = ''; 8 | const ast = await htmlToSlateAST(htmlString); 9 | console.log(JSON.stringify(ast, null, 2)); 10 | } 11 | 12 | main() 13 | .then(() => process.exit(0)) 14 | .catch(e => { 15 | console.error(e); 16 | process.exit(1); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphcms/html-to-slate-ast", 3 | "version": "0.14.1", 4 | "description": "Convert HTML to Hygraph's RichTextAST (slate)", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "tsup --watch", 8 | "build": "tsup", 9 | "test": "tsdx test --passWithNoTests", 10 | "test:watch": "tsdx test --watch --passWithNoTests", 11 | "lint": "tsdx lint", 12 | "prepublish": "npm run build", 13 | "test:node": "node examples/node-script.js" 14 | }, 15 | "peerDependencies": { 16 | "slate": "^0.66.1", 17 | "slate-hyperscript": "^0.67.0", 18 | "jsdom": "^24.0.0" 19 | }, 20 | "peerDependenciesMeta": { 21 | "jsdom": { 22 | "optional": true 23 | } 24 | }, 25 | "devDependencies": { 26 | "@types/jsdom": "^21.1.6", 27 | "jsdom": "^24.0.0", 28 | "slate": "^0.66.1", 29 | "slate-hyperscript": "^0.67.0", 30 | "tsup": "^8.0.1" 31 | }, 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "keywords": [ 36 | "slate", 37 | "rich-text", 38 | "hygraph" 39 | ], 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/hygraph/rich-text.git", 43 | "directory": "packages/html-to-slate-ast" 44 | }, 45 | "main": "dist/index.js", 46 | "module": "dist/index.mjs", 47 | "types": "dist/index.d.ts", 48 | "files": [ 49 | "README.md", 50 | "LICENSE.md", 51 | "dist" 52 | ], 53 | "jest": {}, 54 | "dependencies": { 55 | "@braintree/sanitize-url": "^7.0.0", 56 | "@graphcms/rich-text-types": "^0.5.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseElement, 3 | Descendant, 4 | Element as SlateElement, 5 | Text as SlateText, 6 | } from 'slate'; 7 | import { jsx } from 'slate-hyperscript'; 8 | import { sanitizeUrl } from '@braintree/sanitize-url'; 9 | import { Element, Mark } from '@graphcms/rich-text-types'; 10 | 11 | type AttributesType = Omit; 12 | 13 | const ELEMENT_TAGS: Record< 14 | HTMLElement['nodeName'], 15 | (el: HTMLElement) => AttributesType 16 | > = { 17 | LI: () => ({ type: 'list-item' }), 18 | OL: () => ({ type: 'numbered-list' }), 19 | UL: () => ({ type: 'bulleted-list' }), 20 | P: () => ({ type: 'paragraph' }), 21 | A: el => { 22 | const href = el.getAttribute('href'); 23 | if (href === null) return {}; 24 | return { 25 | type: 'link', 26 | href: sanitizeUrl(href), 27 | ...(el.hasAttribute('title') && { title: el.getAttribute('title') }), 28 | ...(el.hasAttribute('rel') && { rel: el.getAttribute('rel') }), 29 | ...(el.hasAttribute('id') && { id: el.getAttribute('id') }), 30 | ...(el.hasAttribute('class') && { 31 | className: el.getAttribute('class'), 32 | }), 33 | openInNewTab: Boolean(el.getAttribute('target') === '_blank'), 34 | }; 35 | }, 36 | BLOCKQUOTE: () => ({ type: 'block-quote' }), 37 | H1: () => ({ type: 'heading-one' }), 38 | H2: () => ({ type: 'heading-two' }), 39 | H3: () => ({ type: 'heading-three' }), 40 | H4: () => ({ type: 'heading-four' }), 41 | H5: () => ({ type: 'heading-five' }), 42 | H6: () => ({ type: 'heading-six' }), 43 | TABLE: () => ({ type: 'table' }), 44 | THEAD: () => ({ type: 'table_head' }), 45 | TBODY: () => ({ type: 'table_body' }), 46 | TR: () => ({ type: 'table_row' }), 47 | TD: () => ({ type: 'table_cell' }), 48 | TH: () => ({ type: 'table_header_cell' }), 49 | IMG: el => { 50 | const href = el.getAttribute('src'); 51 | const title = Boolean(el.getAttribute('alt')) 52 | ? el.getAttribute('alt') 53 | : Boolean(el.getAttribute('title')) 54 | ? el.getAttribute('title') 55 | : '(Image)'; 56 | if (href === null) return {}; 57 | return { 58 | type: 'link', 59 | href: sanitizeUrl(href), 60 | title, 61 | openInNewTab: true, 62 | }; 63 | }, 64 | PRE: () => ({ type: 'code-block' }), 65 | IFRAME: el => { 66 | const src = el.getAttribute('src'); 67 | if (!src) return {}; 68 | const height = el.getAttribute('height'); 69 | const width = el.getAttribute('width'); 70 | return { 71 | type: 'iframe', 72 | url: src, 73 | // default iframe height is 150 74 | height: Number(height || 150), 75 | // default iframe width is 300 76 | width: Number(width || 300), 77 | children: [ 78 | { 79 | text: '', 80 | }, 81 | ], 82 | }; 83 | }, 84 | }; 85 | 86 | const TEXT_TAGS: Record< 87 | HTMLElement['nodeName'], 88 | (el?: HTMLElement) => Partial> 89 | > = { 90 | CODE: () => ({ code: true }), 91 | EM: () => ({ italic: true }), 92 | I: () => ({ italic: true }), 93 | STRONG: () => ({ bold: true }), 94 | U: () => ({ underline: true }), 95 | }; 96 | 97 | function deserialize< 98 | T extends { 99 | Node: typeof window.Node; 100 | } 101 | >(el: Node, global: T): string | ChildNode[] | BaseElement | Descendant[]; 102 | 103 | function deserialize< 104 | T extends { 105 | Node: typeof window.Node; 106 | } 107 | >(el: Node, global: T) { 108 | if (el.nodeType === 3) { 109 | return el.textContent; 110 | } else if (el.nodeType !== 1) { 111 | return null; 112 | } else if (el.nodeName === 'BR') { 113 | // wrap parentless breaks in a paragraph 114 | if (el.parentElement?.nodeName === 'BODY') { 115 | return jsx('element', { type: 'paragraph' }, [{ text: '' }]); 116 | } else { 117 | return '\n'; 118 | } 119 | } 120 | 121 | const { nodeName } = el; 122 | let parent = el; 123 | 124 | if ( 125 | nodeName === 'PRE' && 126 | el.childNodes[0] && 127 | el.childNodes[0].nodeName === 'CODE' 128 | ) { 129 | parent = el.childNodes[0]; 130 | } 131 | let children = Array.from(parent.childNodes) 132 | .map(c => deserialize(c, global)) 133 | .flat(); 134 | 135 | if (children.length === 0) { 136 | if (!['COLGROUP', 'COL', 'CAPTION', 'TFOOT'].includes(nodeName)) { 137 | const textNode = jsx('text', {}, ''); 138 | children = [textNode]; 139 | } 140 | } 141 | if (el.nodeName === 'BODY') { 142 | return jsx('fragment', {}, children); 143 | } 144 | 145 | if ( 146 | isElementNode(el) && 147 | Array.from(el.attributes).find( 148 | attr => attr.name === 'role' && attr.value === 'heading' 149 | ) 150 | ) { 151 | const level = el.attributes.getNamedItem('aria-level')?.value; 152 | switch (level) { 153 | case '1': { 154 | return jsx('element', { type: 'heading-one' }, children); 155 | } 156 | case '2': { 157 | return jsx('element', { type: 'heading-two' }, children); 158 | } 159 | case '3': { 160 | return jsx('element', { type: 'heading-three' }, children); 161 | } 162 | case '4': { 163 | return jsx('element', { type: 'heading-four' }, children); 164 | } 165 | case '5': { 166 | return jsx('element', { type: 'heading-five' }, children); 167 | } 168 | case '6': { 169 | return jsx('element', { type: 'heading-six' }, children); 170 | } 171 | 172 | default: 173 | break; 174 | } 175 | } 176 | 177 | if (ELEMENT_TAGS[nodeName]) { 178 | const attrs = ELEMENT_TAGS[nodeName](el as HTMLElement); 179 | // li children must be rendered in spans, like in list plugin 180 | if (nodeName === 'LI') { 181 | const hasNestedListChild = children.find( 182 | item => 183 | SlateElement.isElement(item) && 184 | // if element has a nested list as a child, all children must be wrapped in individual list-item-child nodes 185 | // TODO: sync with GCMS types for Slate elements 186 | // @ts-expect-error 187 | (item.type === 'numbered-list' || item.type === 'bulleted-list') 188 | ); 189 | if (hasNestedListChild) { 190 | const wrappedChildren = children.map(item => 191 | jsx('element', { type: 'list-item-child' }, item) 192 | ); 193 | return jsx('element', attrs, wrappedChildren); 194 | } 195 | // in any case we add a single list-item-child containing the children 196 | const child = jsx('element', { type: 'list-item-child' }, children); 197 | return jsx('element', attrs, [child]); 198 | } else if (nodeName === 'TR') { 199 | if ( 200 | el.parentElement?.nodeName === 'THEAD' && 201 | (el as HTMLTableRowElement).cells.length === 0 202 | ) { 203 | return [ 204 | { 205 | type: 'table_header_cell', 206 | children: [ 207 | { 208 | type: 'paragraph', 209 | children: [{ text: el.textContent ? el.textContent : '' }], 210 | }, 211 | ], 212 | }, 213 | ]; 214 | } 215 | // if TR is empty, insert a cell with a paragraph to ensure selection can be placed inside 216 | const modifiedChildren = 217 | (el as HTMLTableRowElement).cells.length === 0 218 | ? [ 219 | { 220 | type: 221 | el.parentElement?.nodeName === 'THEAD' 222 | ? 'table_header_cell' 223 | : 'table_cell', 224 | children: [ 225 | { 226 | type: 'paragraph', 227 | children: [{ text: el.textContent ? el.textContent : '' }], 228 | }, 229 | ], 230 | }, 231 | ] 232 | : children; 233 | return jsx('element', attrs, modifiedChildren); 234 | } else if (nodeName === 'TD' || nodeName === 'TH') { 235 | // if TD or TH is empty, insert a paragraph to ensure selection can be placed inside 236 | const childNodes = Array.from((el as HTMLTableCellElement).childNodes); 237 | if (childNodes.length === 0) { 238 | return jsx('element', attrs, [ 239 | { 240 | type: 'paragraph', 241 | children: [{ text: '' }], 242 | }, 243 | ]); 244 | } else { 245 | const children = childNodes.map(c => deserialize(c, global)).flat(); 246 | return jsx('element', attrs, children); 247 | } 248 | } else if (nodeName === 'IMG') { 249 | return jsx('element', attrs, [attrs.href]); 250 | } 251 | return jsx('element', attrs, children); 252 | } 253 | 254 | if (nodeName === 'DIV') { 255 | if (isElementNode(el)) { 256 | const nodeType = el.getAttribute('data-gcms-embed-type'); 257 | const nodeId = el.getAttribute('data-gcms-embed-id'); 258 | if (nodeType && nodeId) { 259 | return jsx('element', { type: 'embed', nodeId, nodeType }, children); 260 | } 261 | } 262 | 263 | const childNodes = Array.from(el.childNodes); 264 | const isParagraph = childNodes.every( 265 | child => 266 | (isElementNode(child) && isInlineElement(child)) || isTextNode(child) 267 | ); 268 | if (isParagraph) { 269 | return jsx('element', { type: 'paragraph' }, children); 270 | } 271 | } 272 | 273 | if (nodeName === 'SPAN') { 274 | const parentElement = el.parentElement; 275 | // Handle users copying parts of paragraphs 276 | // When they copy multiple paragraphs we don't need to do anything, because all spans have block parents in that case 277 | if (!parentElement || parentElement.nodeName === 'BODY') { 278 | return jsx('element', { type: 'paragraph' }, children); 279 | } 280 | const element = el as HTMLElement; 281 | 282 | // boolean attribute 283 | const isInlineEmbed = element.getAttribute('data-gcms-embed-inline'); 284 | 285 | if (isInlineEmbed !== null && isElementNode(element)) { 286 | const nodeType = element.getAttribute('data-gcms-embed-type'); 287 | const nodeId = element.getAttribute('data-gcms-embed-id'); 288 | if (nodeId && nodeType) { 289 | return jsx( 290 | 'element', 291 | { type: 'embed', nodeId, nodeType, isInline: true }, 292 | children 293 | ); 294 | } 295 | } 296 | 297 | // handles italic, bold and undeline that are not expressed as tags 298 | // important for pasting from Google Docs 299 | const attrs = getSpanAttributes(element); 300 | 301 | if (attrs) { 302 | return children.map(child => { 303 | if (typeof child === 'string') { 304 | return jsx('text', attrs, child); 305 | } 306 | 307 | if (isChildNode(child, global)) return child; 308 | 309 | if (SlateElement.isElement(child) && !SlateText.isText(child)) { 310 | child.children = child.children.map(c => ({ ...c, ...attrs })); 311 | return child; 312 | } 313 | 314 | return child; 315 | }); 316 | } 317 | } 318 | 319 | if (TEXT_TAGS[nodeName]) { 320 | const attrs = TEXT_TAGS[nodeName](el as HTMLElement); 321 | return children.map(child => { 322 | if (typeof child === 'string') { 323 | return jsx('text', attrs, child); 324 | } 325 | 326 | if (isChildNode(child, global)) return child; 327 | 328 | if (SlateElement.isElement(child) && !SlateText.isText(child)) { 329 | child.children = child.children.map(c => ({ ...c, ...attrs })); 330 | return child; 331 | } 332 | 333 | return child; 334 | }); 335 | } 336 | 337 | // general fallback 338 | // skips unsupported tags and prevents block-level element nesting 339 | return children; 340 | } 341 | 342 | /* 343 | CKEditor's Word normalizer functions 344 | Tried importing @ckeditor/ckeditor5-paste-from-office, but it depends on a lot of ckeditor packages we don't need, so decided on just copying these three functions that we need 345 | */ 346 | 347 | // https://github.com/ckeditor/ckeditor5/blob/bce8267e16fccb25448b4c68acc3bf54336aa087/packages/ckeditor5-paste-from-office/src/filters/space.js#L57 348 | function normalizeSafariSpaceSpans(htmlString: string) { 349 | return htmlString.replace( 350 | /(\s+)<\/span>/g, 351 | (_, spaces) => { 352 | return spaces.length === 1 353 | ? ' ' 354 | : Array(spaces.length + 1) 355 | .join('\u00A0 ') 356 | .substring(0, spaces.length + 1); 357 | } 358 | ); 359 | } 360 | 361 | // https://github.com/ckeditor/ckeditor5/blob/bce8267e16fccb25448b4c68acc3bf54336aa087/packages/ckeditor5-paste-from-office/src/filters/space.js#L19 362 | function normalizeSpacing(htmlString: string) { 363 | // Run normalizeSafariSpaceSpans() two times to cover nested spans. 364 | return ( 365 | normalizeSafariSpaceSpans(normalizeSafariSpaceSpans(htmlString)) 366 | // Remove all \r\n from "spacerun spans" so the last replace line doesn't strip all whitespaces. 367 | .replace( 368 | /([^\S\r\n]*?)[\r\n]+([^\S\r\n]*<\/span>)/g, 369 | '$1$2' 370 | ) 371 | .replace(/<\/span>/g, '') 372 | .replace(/ <\//g, '\u00A0<\/o:p>/g, '\u00A0') 374 | // Remove block filler from empty paragraph. Safari uses \u00A0 instead of  . 375 | .replace(/( |\u00A0)<\/o:p>/g, '') 376 | // Remove all whitespaces when they contain any \r or \n. 377 | .replace(/>([^\S\r\n]*[\r\n]\s*)<') 378 | ); 379 | } 380 | 381 | // https://github.com/ckeditor/ckeditor5/blob/bce8267e16fccb25448b4c68acc3bf54336aa087/packages/ckeditor5-paste-from-office/src/filters/parse.js#L102 382 | function cleanContentAfterBody(htmlString: string) { 383 | const bodyCloseTag = ''; 384 | const htmlCloseTag = ''; 385 | 386 | const bodyCloseIndex = htmlString.indexOf(bodyCloseTag); 387 | 388 | if (bodyCloseIndex < 0) { 389 | return htmlString; 390 | } 391 | 392 | const htmlCloseIndex = htmlString.indexOf( 393 | htmlCloseTag, 394 | bodyCloseIndex + bodyCloseTag.length 395 | ); 396 | 397 | return ( 398 | htmlString.substring(0, bodyCloseIndex + bodyCloseTag.length) + 399 | (htmlCloseIndex >= 0 ? htmlString.substring(htmlCloseIndex) : '') 400 | ); 401 | } 402 | 403 | function normalizeHtml(html: string) { 404 | return cleanContentAfterBody(normalizeSpacing(html)); 405 | } 406 | 407 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#node_type_constants 408 | // An Element node like

    or

    409 | function isElementNode(node: Node): node is HTMLElement { 410 | return node.nodeType === 1; 411 | } 412 | 413 | // The actual Text inside an Element or Attr 414 | function isTextNode(node: Node): node is Text { 415 | return node.nodeType === 3; 416 | } 417 | 418 | function isChildNode( 419 | node: string | ChildNode | Descendant, 420 | global: T 421 | ): node is ChildNode { 422 | return node instanceof global.Node; 423 | } 424 | function isInlineElement(element: HTMLElement) { 425 | const allInlineElements: Array = [ 426 | 'a', 427 | 'abbr', 428 | 'audio', 429 | 'b', 430 | 'bdi', 431 | 'bdo', 432 | 'br', 433 | 'button', 434 | 'canvas', 435 | 'cite', 436 | 'code', 437 | 'data', 438 | 'datalist', 439 | 'del', 440 | 'dfn', 441 | 'em', 442 | 'embed', 443 | 'i', 444 | 'iframe', 445 | 'img', 446 | 'input', 447 | 'ins', 448 | 'kbd', 449 | 'label', 450 | 'map', 451 | 'mark', 452 | 'meter', 453 | 'noscript', 454 | 'object', 455 | 'output', 456 | 'picture', 457 | 'progress', 458 | 'q', 459 | 'ruby', 460 | 's', 461 | 'samp', 462 | 'script', 463 | 'select', 464 | 'slot', 465 | 'small', 466 | 'span', 467 | 'strong', 468 | 'sub', 469 | 'sup', 470 | 'template', 471 | 'textarea', 472 | 'time', 473 | 'u', 474 | 'var', 475 | 'video', 476 | 'wbr', 477 | ]; 478 | return allInlineElements.includes( 479 | element.tagName.toLowerCase() as keyof HTMLElementTagNameMap 480 | ); 481 | } 482 | 483 | function getSpanAttributes(element: HTMLElement): AttributesType | null { 484 | const names = []; 485 | if (element.style.textDecoration === 'underline') { 486 | names.push('U'); 487 | } 488 | if (element.style.fontStyle === 'italic') { 489 | names.push('EM'); 490 | } 491 | if ( 492 | parseInt(element.style.fontWeight, 10) > 400 || 493 | element.style.fontWeight === 'bold' 494 | ) { 495 | names.push('STRONG'); 496 | } 497 | if (names.length === 0) return null; 498 | const attrs: AttributesType = names.reduce((acc, current) => { 499 | return { ...acc, ...TEXT_TAGS[current]() }; 500 | }, {}); 501 | return attrs; 502 | } 503 | 504 | const parseDomDocument = async (normalizedHTML: string) => { 505 | if (typeof window !== 'undefined' && window.DOMParser) { 506 | return new DOMParser().parseFromString(normalizedHTML, 'text/html'); 507 | } else { 508 | const jsdom = await import('jsdom'); 509 | const dom = new jsdom.JSDOM(normalizedHTML, { contentType: 'text/html' }); 510 | return dom.window.document; 511 | } 512 | }; 513 | 514 | const parseDomDocumentSync = (normalizedHTML: string) => { 515 | return new DOMParser().parseFromString(normalizedHTML, 'text/html'); 516 | }; 517 | 518 | export function htmlToSlateAST( 519 | html: string 520 | ): Promise; 521 | export async function htmlToSlateAST(html: string) { 522 | const normalizedHTML = normalizeHtml(html); 523 | const domDocument = await parseDomDocument(normalizedHTML); 524 | const global = await (async () => { 525 | if (typeof window !== 'undefined') return window; 526 | return await import('jsdom').then(jsdom => new jsdom.JSDOM().window); 527 | })(); 528 | return deserialize(domDocument.body, global); 529 | } 530 | 531 | export function htmlToSlateASTSync( 532 | html: string 533 | ): string | Descendant | ChildNode[] | Descendant[] | T | T[] { 534 | if ( 535 | typeof window === 'undefined' || 536 | typeof window.DOMParser === 'undefined' 537 | ) { 538 | throw new Error( 539 | 'This function is intended to be used in a browser environment only' 540 | ); 541 | } 542 | 543 | const normalizedHTML = normalizeHtml(html); 544 | const domDocument = parseDomDocumentSync(normalizedHTML); 545 | return deserialize(domDocument.body, window); 546 | } 547 | 548 | export default htmlToSlateAST; 549 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/test/google-docs_input.html: -------------------------------------------------------------------------------- 1 |

    Heading 1

    Heading 2

    Heading 3

    Heading 4

    Heading 5
    Heading 6

    Link to Google 

    Unordered list:

    • One

    • Two

    • Three

    Ordered list:

    1. One

    2. Two

    Table:

    Cell one

    Cell two



    Screenshot 2021-06-10 at 15.56.22.png


    -------------------------------------------------------------------------------- /packages/html-to-slate-ast/test/html_input.html: -------------------------------------------------------------------------------- 1 | 2 |
    4 |
    if (el.nodeName === 'BODY') {
    7 |
    return jsx('fragment', {}, children);
    10 |
    }
    11 |
    just text
    12 |
    -------------------------------------------------------------------------------- /packages/html-to-slate-ast/test/html_input_iframe.html: -------------------------------------------------------------------------------- 1 |
    2 |

    3 | SANTA CLARA, Calif., (June 20, 2017) – experience for fans and newcomers alike. 4 |

    5 |
    14 | 22 |
    23 |

     

    24 |

    25 | .hack is a multimedia franchise created and developed by famed 26 | Japanese developer CyberConnect2. Comprising of video games, anime, novels, 27 | and manga, the world of .hack focuses on the mysterious events 28 | surrounding a wildly popular in-universe massively multiplayer role-playing 29 | game called The World. .hack//G.U. begins after the events of the 30 | original .hack series with players assuming the role of Haseo as he 31 | tracks down a powerful Player Killer named Tri-Edge who killed his friend’s 32 | in-game avatar Shino, and put her into a coma in real life. 33 |

    34 |
    35 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/test/html_input_table.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 8 | 11 | 12 | 13 | 14 | 17 | 20 | 25 | 26 | 27 |
    6 |

    R1C1 - BOLD TEXT

    7 |
    9 |
    R1C2
    10 |

    R1C3

    15 |

    R2C1

    16 |
    18 | R2C2 - ITALIC TEXT 19 | 21 |

    22 | R2C3 - BOLD TEXT 23 |

    24 |
    28 |
    29 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/test/image.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/test/pre.html: -------------------------------------------------------------------------------- 1 |
     2 |   L          TE
     3 |     A       A
     4 |       C    V
     5 |        R A
     6 |        DOU
     7 |        LOU
     8 |       REUSE
     9 |       QUE TU
    10 |       PORTES
    11 |     ET QUI T'
    12 |     ORNE O CI
    13 |      VILISÉ
    14 |     OTE-  TU VEUX
    15 |      LA    BIEN
    16 |     SI      RESPI
    17 |             RER       - Apollinaire
    -------------------------------------------------------------------------------- /packages/html-to-slate-ast/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": ["src", "types", "../../types"], 4 | "compilerOptions": { 5 | "typeRoots": ["./node_modules/@types"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/html-to-slate-ast/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig(options => ({ 4 | entry: ['src/index.ts'], 5 | tsconfig: 'tsconfig.build.json', 6 | minify: !options.watch, 7 | splitting: true, 8 | sourcemap: true, 9 | dts: true, 10 | treeshake: true, 11 | clean: true, 12 | format: ['esm', 'cjs'], 13 | skipNodeModulesBundle: true, 14 | })); 15 | -------------------------------------------------------------------------------- /packages/react-renderer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @graphcms/rich-text-react-renderer 2 | 3 | ## 0.6.2 4 | 5 | ### Patch Changes 6 | 7 | - [`50696b8`](https://github.com/hygraph/rich-text/commit/50696b85ef30b1561f686a75a42e84bde3a39190) [#130](https://github.com/hygraph/rich-text/pull/130) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Update documentation 8 | 9 | ## 0.6.1 10 | 11 | ### Patch Changes 12 | 13 | - [`80c399f`](https://github.com/hygraph/rich-text/commit/80c399ff57f3f6e03cd5ecd8d23dd118ce3bb69d) [#87](https://github.com/hygraph/rich-text/pull/87) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Make sure width and height are not included if set to 0 14 | 15 | ## 0.6.0 16 | 17 | ### Minor Changes 18 | 19 | - [`b272253`](https://github.com/hygraph/rich-text/commit/b2722534275efd2c5e473d549d0f0e5a28100025) [#84](https://github.com/hygraph/rich-text/pull/84) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Adds support for Link Embeds 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [[`b272253`](https://github.com/hygraph/rich-text/commit/b2722534275efd2c5e473d549d0f0e5a28100025)]: 24 | - @graphcms/rich-text-types@0.5.0 25 | 26 | ## 0.5.0 27 | 28 | ### Minor Changes 29 | 30 | - [`b7f16fa`](https://github.com/hygraph/rich-text/commit/b7f16fa76a28ad0f5cdbe6cb1f58d7fafa63df15) [#77](https://github.com/hygraph/rich-text/pull/77) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - This update has no new features, only new types. 31 | 32 | ⚡️ New 33 | 34 | - Add `NodeRendererType` type 35 | - Add `RichTextProps` type 36 | - Add `DefaultElementProps` type 37 | - Add `ClassRendererProps` type 38 | - Add `LinkRendererProps` type 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies [[`b7f16fa`](https://github.com/hygraph/rich-text/commit/b7f16fa76a28ad0f5cdbe6cb1f58d7fafa63df15)]: 43 | - @graphcms/rich-text-types@0.4.0 44 | 45 | ## 0.4.3 46 | 47 | ### Patch Changes 48 | 49 | - [`9d7bead`](https://github.com/hygraph/rich-text/commit/9d7bead10fa1a0de7d4742e097b58ad738205fc1) [#74](https://github.com/hygraph/rich-text/pull/74) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Use index as key on line break to fix React warning of same keys 50 | 51 | ## 0.4.2 52 | 53 | ### Patch Changes 54 | 55 | - [`2f3c345`](https://github.com/hygraph/rich-text/commit/2f3c34517b4bec585bed3c334fd2526a45354088) [#61](https://github.com/hygraph/rich-text/pull/61) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - fix \n is not converted to
    56 | 57 | ## 0.4.1 58 | 59 | ### Patch Changes 60 | 61 | - [`805119b`](https://github.com/hygraph/rich-text/commit/805119bb157d3d359df844f7159453cfefbda0a9) [#59](https://github.com/hygraph/rich-text/pull/59) Thanks [@feychenie](https://github.com/feychenie)! - Fix missing references in inline embeds 62 | 63 | ## 0.4.0 64 | 65 | ### Minor Changes 66 | 67 | - [`17f5244`](https://github.com/hygraph/rich-text/commit/17f52440c3fae398f8fd49d4ef61a6fe46ff8635) [#53](https://github.com/hygraph/rich-text/pull/53) Thanks [@anmolarora1](https://github.com/anmolarora1)! - feat: adds `` table_header_cell element support 68 | 69 | ### Patch Changes 70 | 71 | - Updated dependencies [[`c2e0a75`](https://github.com/hygraph/rich-text/commit/c2e0a75e995591bb299250f4d14092b1843b1183)]: 72 | - @graphcms/rich-text-types@0.3.1 73 | 74 | ## 0.3.3 75 | 76 | ### Patch Changes 77 | 78 | - Updated dependencies [[`bc9e612`](https://github.com/hygraph/rich-text/commit/bc9e61293ec0535328541c95c33e71f51ec09c43)]: 79 | - @graphcms/rich-text-types@0.3.0 80 | 81 | ## 0.3.2 82 | 83 | ### Patch Changes 84 | 85 | - [`5dd9acb`](https://github.com/hygraph/rich-text/commit/5dd9acb12b13cca098a4bdc01906f173cf1d65a2) [#45](https://github.com/hygraph/rich-text/pull/45) Thanks [@nrandell](https://github.com/nrandell)! - fix(react): simple elements are not empty 86 | 87 | ## 0.3.1 88 | 89 | ### Patch Changes 90 | 91 | - [`6835755`](https://github.com/hygraph/rich-text/commit/6835755e2f7b07adbd3ca0b8497730d19a858bda) [#42](https://github.com/hygraph/rich-text/pull/42) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - docs: add note about query, types and gatsby image 92 | 93 | * [`c7ea848`](https://github.com/hygraph/rich-text/commit/c7ea8483ed3353843e1eb43d00ff57e785d046c3) [#41](https://github.com/hygraph/rich-text/pull/41) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Fix heading with links not being rendered 94 | 95 | ## 0.3.0 96 | 97 | ### Minor Changes 98 | 99 | - [`91495b9`](https://github.com/hygraph/rich-text/commit/91495b9f3649c0bf92326d52365473d376ad598f) [#29](https://github.com/hygraph/rich-text/pull/29) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Add support for code blocks 100 | 101 | ### Patch Changes 102 | 103 | - Updated dependencies [[`91495b9`](https://github.com/hygraph/rich-text/commit/91495b9f3649c0bf92326d52365473d376ad598f)]: 104 | - @graphcms/rich-text-types@0.2.1 105 | 106 | ## 0.2.0 107 | 108 | ### Minor Changes 109 | 110 | - [`768492a`](https://github.com/hygraph/rich-text/commit/768492a5dd5e642cc639b82cd7e13f2ce7f2dc96) [#25](https://github.com/hygraph/rich-text/pull/25) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Add support for embeds assets and custom models 111 | 112 | ### Patch Changes 113 | 114 | - Updated dependencies [[`768492a`](https://github.com/hygraph/rich-text/commit/768492a5dd5e642cc639b82cd7e13f2ce7f2dc96)]: 115 | - @graphcms/rich-text-types@0.2.0 116 | 117 | ## 0.1.5 118 | 119 | ### Patch Changes 120 | 121 | - [`e950c91`](https://github.com/hygraph/rich-text/commit/e950c917befe31060c77891dd44f7722c9c93c77) [#17](https://github.com/hygraph/rich-text/pull/17) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - fix: empty thead being renderered 122 | 123 | * [`1a0354c`](https://github.com/hygraph/rich-text/commit/1a0354c13c1ca6b5eef0cd6b41281f413360de87) [#16](https://github.com/hygraph/rich-text/pull/16) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - docs: add examples for next image and gatsby link 124 | 125 | * Updated dependencies [[`e950c91`](https://github.com/hygraph/rich-text/commit/e950c917befe31060c77891dd44f7722c9c93c77)]: 126 | - @graphcms/rich-text-types@0.1.4 127 | 128 | ## 0.1.4 129 | 130 | ### Patch Changes 131 | 132 | - [`7cb7b7e`](https://github.com/hygraph/rich-text/commit/7cb7b7ef78a465c54982f81c77432d001ea9645b) [#9](https://github.com/hygraph/rich-text/pull/9) Thanks [@feychenie](https://github.com/feychenie)! - Moved html-to-slate-ast package to this repo. 133 | 134 | - Updated dependencies [[`7cb7b7e`](https://github.com/hygraph/rich-text/commit/7cb7b7ef78a465c54982f81c77432d001ea9645b)]: 135 | - @graphcms/rich-text-types@0.1.3 136 | 137 | ## 0.1.3 138 | 139 | ### Patch Changes 140 | 141 | - [`23b87f6`](https://github.com/hygraph/rich-text/commit/23b87f6218040df283d112307c3720645a5936aa) [#6](https://github.com/hygraph/rich-text/pull/6) Thanks [@KaterBasilisk6](https://github.com/KaterBasilisk6)! 142 | 143 | ⚡️ Improvements: 144 | 145 | - feat(react): remove empty headings from rendering 146 | - Updated dependencies [[`23b87f6`](https://github.com/hygraph/rich-text/commit/23b87f6218040df283d112307c3720645a5936aa)]: 147 | - @graphcms/rich-text-types@0.1.2 148 | 149 | ## 0.1.2 150 | 151 | ### Patch Changes 152 | 153 | - [`c064507`](https://github.com/hygraph/rich-text/commit/c06450766c911bd680e71130d71eff34865ec4de) [#3](https://github.com/hygraph/rich-text/pull/3) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! 154 | 155 | 🐛 Bug fixes 156 | 157 | - fix(react): empty space not being rendered 158 | 159 | ## 0.1.1 160 | 161 | ### Patch Changes 162 | 163 | [`b1a0bae`](https://github.com/hygraph/rich-text/commit/b1a0bae5e09e3db4173517e1342b8e5059a59fa0) [#2](https://github.com/hygraph/rich-text/pull/2) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! 164 | 165 | 🐛 Bug fixes: 166 | 167 | - Move @graphcms/rich-text-types to dependecy section to fix the error "can't resolve @graphcms/rich-text-types" 168 | 169 | [`d831c93`](https://github.com/hygraph/rich-text/commit/d831c93be2f1a07aea2377e0d5842e130e104bfd) [#2](https://github.com/hygraph/rich-text/pull/2) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! 170 | 171 | 🐛 Bug fixes: 172 | 173 | - fix(react): not rendering list children 174 | - fix(react): fix can't resolve @graphcms/rich-text-types 175 | - fix(react): html and jsx tags not being rendered correctly 176 | 177 | ⚡️ Improvements: 178 | 179 | - feat(react): accept both array and object for content 180 | 181 | * Updated dependencies [[`d831c93`](https://github.com/hygraph/rich-text/commit/d831c93be2f1a07aea2377e0d5842e130e104bfd)]: 182 | - @graphcms/rich-text-types@0.1.1 183 | 184 | ## 0.1.1-canary.1 185 | 186 | ### Patch Changes 187 | 188 | 🐛 Bug fixes: 189 | 190 | - Move @graphcms/rich-text-types to dependency section to fix the error "can't resolve @graphcms/rich-text-types" 191 | 192 | ## 0.1.1-canary.0 193 | 194 | ### Patch Changes 195 | 196 | 🐛 Bug fixes: 197 | 198 | - fix(react): not rendering list children 199 | - fix(react): fix can't resolve @graphcms/rich-text-types 200 | - fix(react): html and jsx tags not being rendered correctly 201 | 202 | ⚡️ Improvements: 203 | 204 | - feat(react): accept both array and object for content 205 | -------------------------------------------------------------------------------- /packages/react-renderer/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Hygraph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/react-renderer/README.md: -------------------------------------------------------------------------------- 1 | # @graphcms/rich-text-react-renderer 2 | 3 | Render Rich Text content from Hygraph in React applications. 4 | 5 | ## ⚡ Getting started 6 | 7 | You can get it on npm or Yarn. 8 | 9 | ```sh 10 | # npm 11 | npm i @graphcms/rich-text-react-renderer 12 | 13 | # Yarn 14 | yarn add @graphcms/rich-text-react-renderer 15 | ``` 16 | 17 | ## 🔥 Usage/Examples 18 | 19 | To render the content on your application, you'll need to provide the array of elements returned from the Hygraph API to the `RichText` component. The content has to be returned in `raw` (or `json`) format as the AST representation. For more information on how to query the Rich Text content, [check our documentation](https://hygraph.com/docs/api-reference/schema/field-types#rich-text). 20 | 21 | ```tsx 22 | import { RichText } from '@graphcms/rich-text-react-renderer'; 23 | 24 | const content = { 25 | children: [ 26 | { 27 | type: 'paragraph', 28 | children: [ 29 | { 30 | bold: true, 31 | text: 'Hello World!', 32 | }, 33 | ], 34 | }, 35 | ], 36 | }; 37 | 38 | function App() { 39 | return ; 40 | } 41 | ``` 42 | 43 | The content from the example above will render: 44 | 45 | ```html 46 |

    47 | Hello world! 48 |

    49 | ``` 50 | 51 | ## Custom elements 52 | 53 | By default, the elements won't have any styling, despite the `IFrame`, which we designed to be responsive. But if you have, for example, a design system and wants to use your own components with styling, you can pass a `renderers` prop to the `RichText` component. Let's see an example: 54 | 55 | ```tsx 56 | import { RichText } from '@graphcms/rich-text-react-renderer'; 57 | 58 | const content = { 59 | /* ... */ 60 | }; 61 | 62 | function App() { 63 | return ( 64 |
    65 |

    {children}

    , 69 | bold: ({ children }) => {children}, 70 | }} 71 | /> 72 |
    73 | ); 74 | } 75 | ``` 76 | 77 | Below you can check the full list of elements you can customize, alongside the props available for each of them. 78 | 79 | - `a` 80 | - `children`: ReactNode; 81 | - `href`: string; 82 | - `className`: string; 83 | - `rel`: string; 84 | - `id`: string; 85 | - `title`: string; 86 | - `openInNewTab`: boolean; 87 | - `class` 88 | - `children`: ReactNode; 89 | - `className`: string; 90 | - `img` 91 | - `src`: string; 92 | - `title`: string; 93 | - `width`: number; 94 | - `height`: number; 95 | - `mimeType`: ImageMimeTypes; 96 | - `altText`: string; 97 | - `video` 98 | - `src`: string; 99 | - `title`: string; 100 | - `width`: number; 101 | - `height`: number; 102 | - `iframe` 103 | - `url`: string; 104 | - `width`: number; 105 | - `height`: number; 106 | - `h1` 107 | - `children`: ReactNode; 108 | - `h2` 109 | - `children`: ReactNode; 110 | - `h3` 111 | - `children`: ReactNode; 112 | - `h4` 113 | - `children`: ReactNode; 114 | - `h5` 115 | - `children`: ReactNode; 116 | - `h6` 117 | - `children`: ReactNode; 118 | - `p` 119 | - `children`: ReactNode; 120 | - `ul` 121 | - `children`: ReactNode; 122 | - `ol` 123 | - `children`: ReactNode; 124 | - `li` 125 | - `children`: ReactNode; 126 | - `table` 127 | - `children`: ReactNode; 128 | - `table_head` 129 | - `children`: ReactNode; 130 | - `table_header_cell` 131 | - `children`: ReactNode; 132 | - `table_body` 133 | - `children`: ReactNode; 134 | - `table_row` 135 | - `children`: ReactNode; 136 | - `table_cell` 137 | - `children`: ReactNode; 138 | - `blockquote` 139 | - `children`: ReactNode; 140 | - `bold` 141 | - `children`: ReactNode; 142 | - `italic` 143 | - `children`: ReactNode; 144 | - `underline` 145 | - `children`: ReactNode; 146 | - `code` 147 | - `children`: ReactNode; 148 | - `code_block` 149 | - `children`: ReactNode; 150 | 151 | ## Custom assets 152 | 153 | The Rich Text field allows you to embed assets. By default, we render images, videos and audios out of the box. However, you can define custom components for each mime type group. Below you can see the complete list of `mimeType` groups. 154 | 155 | - `audio` 156 | - `application` 157 | - `image` 158 | - `video` 159 | - `font` 160 | - `model` 161 | - `text` 162 | 163 | We don't have components to render fonts, models, text and application files, but you can write your own depending on your needs and project. If you need, you can also have a custom renderer for a specific `mimeType`. Here's an example: 164 | 165 | ```js 166 | import { RichText } from '@graphcms/rich-text-react-renderer'; 167 | 168 | const content = [ 169 | { 170 | type: 'embed', 171 | nodeId: 'cknjbzowggjo90b91kjisy03a', 172 | children: [ 173 | { 174 | text: '', 175 | }, 176 | ], 177 | nodeType: 'Asset', 178 | }, 179 | { 180 | type: 'embed', 181 | nodeId: 'ckrus0f14ao760b32mz2dwvgx', 182 | children: [ 183 | { 184 | text: '', 185 | }, 186 | ], 187 | nodeType: 'Asset', 188 | }, 189 | ]; 190 | 191 | const references = [ 192 | { 193 | id: 'cknjbzowggjo90b91kjisy03a', 194 | url: 'https://media.graphassets.com/dsQtt0ARqO28baaXbVy9', 195 | mimeType: 'image/png', 196 | }, 197 | { 198 | id: 'ckrus0f14ao760b32mz2dwvgx', 199 | url: 'https://media.graphassets.com/7M0lXLdCQfeIDXnT2SVS', 200 | mimeType: 'video/mp4', 201 | }, 202 | ]; 203 | 204 | function App() { 205 | return ( 206 |
    custom VIDEO
    , 212 | image: () =>
    custom IMAGE
    , 213 | 'video/mp4': () => { 214 | return
    custom video/mp4 renderer
    ; 215 | }, 216 | }, 217 | }} 218 | /> 219 | ); 220 | } 221 | ``` 222 | 223 | As mentioned, you can write renderers for all `mimeType` groups or to specific `mimeType`. 224 | 225 | ### References 226 | 227 | References are required on the `RichText` component to render embed assets. 228 | 229 | `id`, `mimeType` and `url` are required in your `Asset` query. 230 | 231 | **Query example:** 232 | 233 | ```graphql 234 | { 235 | articles { 236 | content { 237 | json 238 | references { 239 | ... on Asset { 240 | id 241 | url 242 | mimeType 243 | } 244 | } 245 | } 246 | } 247 | } 248 | ``` 249 | 250 | ## Custom embeds 251 | 252 | Imagine you have an embed `Post` on your Rich Text field. To render it, you can have a custom renderer. Let's see an example: 253 | 254 | ```jsx 255 | import { RichText } from '@graphcms/rich-text-react-renderer'; 256 | 257 | const content = [ 258 | { 259 | type: 'embed', 260 | nodeId: 'custom_post_id', 261 | children: [ 262 | { 263 | text: '', 264 | }, 265 | ], 266 | nodeType: 'Post', 267 | }, 268 | ]; 269 | 270 | const references = [ 271 | { 272 | id: 'custom_post_id', 273 | title: 'Hygraph is awesome :rocket:', 274 | }, 275 | ]; 276 | 277 | function App() { 278 | return ( 279 | { 285 | return ( 286 |
    287 |

    {title}

    288 |

    {nodeId}

    289 |
    290 | ); 291 | }, 292 | }, 293 | }} 294 | /> 295 | ); 296 | } 297 | ``` 298 | 299 | ### References 300 | 301 | References are required on the `RichText` component. You also need to include your model in your query. 302 | 303 | - `id` is always required in your model query. It won't render if it's not present. 304 | 305 | ```graphql 306 | { 307 | articles { 308 | content { 309 | json 310 | references { 311 | ... on Asset { 312 | id 313 | url 314 | mimeType 315 | } 316 | # Your post query 317 | ... on Post { 318 | id # required 319 | title 320 | slug 321 | description 322 | } 323 | } 324 | } 325 | } 326 | } 327 | ``` 328 | 329 | ### Link embeds 330 | 331 | The Rich Text Field also supports Link Embeds, which work similarly to normal embeds. Based on the model name, you can have a custom renderer for it. Example: 332 | 333 | ```jsx 334 | import { RichText } from '@graphcms/rich-text-react-renderer'; 335 | 336 | const content = [ 337 | { 338 | type: 'link', 339 | nodeId: 'post_id', 340 | children: [ 341 | { 342 | text: 'click here', 343 | }, 344 | ], 345 | nodeType: 'Post', 346 | }, 347 | ]; 348 | 349 | const references = [ 350 | { 351 | id: 'post_id', 352 | slug: 'hygraph-is-awesome', 353 | }, 354 | ]; 355 | 356 | function App() { 357 | return ( 358 | { 364 | return {children}; 365 | }, 366 | }, 367 | }} 368 | /> 369 | ); 370 | } 371 | ``` 372 | 373 | ## Empty elements 374 | 375 | By default, we remove empty headings from the element list to prevent SEO issues. Other elements, such as `thead` are also removed. You can find the complete list [here](https://github.com/hygraph/rich-text/blob/main/packages/types/src/index.ts#L168). 376 | 377 | ## TypeScript 378 | 379 | If you are using TypeScript in your project, we recommend installing the `@graphcms/rich-text-types` package. It contains types for the elements, alongside the props accepted by them. You can use them in your application to create custom components. 380 | 381 | ### Children Type 382 | 383 | If you need to type the content from the Rich Text field, you can do so by using the types package. Example: 384 | 385 | ```ts 386 | import { ElementNode } from '@graphcms/rich-text-types'; 387 | 388 | type Content = { 389 | content: { 390 | raw: { 391 | children: ElementNode[]; 392 | }; 393 | }; 394 | }; 395 | ``` 396 | 397 | ### Custom Embeds/Assets 398 | 399 | Depending on your reference query and model, fields may change, which applies to types. To have a better DX using the package, we have `EmbedProps` and `LinkEmbedProps` types that you can import from `@graphcms/rich-text-types` (you may need to install it if you don't have done it already). 400 | 401 | In this example, we have seen how to write a renderer for a `Post` model, but it applies the same way to any other model and `Asset` on your project. 402 | 403 | ```tsx 404 | import { EmbedProps, LinkEmbedProps } from '@graphcms/rich-text-types'; 405 | 406 | type Post = { 407 | title: string; 408 | slug: string; 409 | description: string; 410 | }; 411 | 412 | function App() { 413 | return ( 414 | ) => { 419 | return ( 420 | 426 | ); 427 | }, 428 | }, 429 | link: { 430 | Post: ({ slug, children }: LinkEmbedProps) => { 431 | return {children}; 432 | }, 433 | }, 434 | }} 435 | /> 436 | ); 437 | } 438 | ``` 439 | 440 | ## Examples 441 | 442 | ### Next.js Link component 443 | 444 | ```tsx 445 | import Link from 'next/link'; 446 | import { RichText } from '@graphcms/rich-text-react-renderer'; 447 | 448 | const content = { 449 | /* ... */ 450 | }; 451 | 452 | function App() { 453 | return ( 454 | { 458 | if (href.match(/^https?:\/\/|^\/\//i)) { 459 | return ( 460 | 466 | {children} 467 | 468 | ); 469 | } 470 | 471 | return ( 472 | 473 | {children} 474 | 475 | ); 476 | }, 477 | }} 478 | /> 479 | ); 480 | } 481 | ``` 482 | 483 | ### Next.js Image component 484 | 485 | ```js 486 | import Image from 'next/image'; 487 | import { RichText } from '@graphcms/rich-text-react-renderer'; 488 | 489 | const content = { 490 | /* ... */ 491 | }; 492 | 493 | function App() { 494 | return ( 495 | ( 499 | {altText} 506 | ), 507 | }} 508 | /> 509 | ); 510 | } 511 | ``` 512 | 513 | Since the images are in the Hygraph CDN, you need to specify our domain in the `next.config.js` file. For more information, check [this guide](https://nextjs.org/docs/app/api-reference/components/image#remotepatterns). 514 | 515 | ```js 516 | module.exports = { 517 | images: { 518 | remotePatterns: [ 519 | { 520 | protocol: 'https', 521 | hostname: '**.graphassets.com', 522 | }, 523 | ], 524 | }, 525 | }; 526 | ``` 527 | 528 | ### Placeholder images for Next.js Image component 529 | 530 | For low quality image placeholders (LQIP) we can use [Plaiceholder](https://github.com/joe-bell/plaiceholder). Plaiceholder will generate base64 encoded images which we pass to `next/image` as the [`blurDataUrl`](https://nextjs.org/docs/api-reference/next/image#blurdataurl) prop. In this example we'll query a rich text field and generate a placeholder image for each embedded asset. 531 | 532 | First, install Plaiceholder: 533 | 534 | ```sh 535 | # npm 536 | npm i plaiceholder 537 | 538 | # Yarn 539 | yarn add plaiceholder 540 | ``` 541 | 542 | Note that Plaiceholder uses `sharp` under the hood, but as `next/image` ships with it, we don't need to install it separately. 543 | 544 | Here's a full blown example for a single blog post page with rich text content. 545 | 546 | ```js 547 | // [slug.jsx] 548 | import { RichText } from '@graphcms/rich-text-react-renderer'; 549 | import { getPlaiceholder } from 'plaiceholder'; 550 | import { fetchFromHygraph } from '../../lib/hygraph'; 551 | import Image from 'next/image'; 552 | 553 | // Page template 554 | const SinglePostPage = ({ data }) => { 555 | const { title, description, content } = data; 556 | 557 | return ( 558 | <> 559 | {/* ... */} 560 | { 566 | return ( 567 | {alt} 575 | ); 576 | }, 577 | }, 578 | }} 579 | /> 580 | 581 | ); 582 | }; 583 | 584 | export const getStaticPaths = async () => { 585 | // Get your paths here. 586 | }; 587 | 588 | export const getStaticProps = async context => { 589 | const data = await fetchFromHygraph({ 590 | // Sample query, adjust to your content structure. 591 | // Note: 'id' and 'mimeType' are required for custom components. 592 | query: ` 593 | query ($slug: String!) { 594 | post(where: { slug: $slug }) { 595 | slug 596 | content { 597 | json 598 | references { 599 | ... on Asset { 600 | id 601 | mimeType 602 | url 603 | alt 604 | caption 605 | width 606 | height 607 | } 608 | } 609 | } 610 | } 611 | } 612 | `, 613 | variables: { 614 | slug: context.params?.slug, 615 | }, 616 | preview: context.preview, 617 | }); 618 | 619 | // Pick images from assets 620 | const images = data.post.content.references.filter(asset => 621 | asset.mimeType.includes('image') 622 | ); 623 | 624 | // Use Plaiceholder to generate placeholder images (LQIP) 625 | // As a result the images will have a `blurDataUrl` prop with the 626 | // base64 encoded image. 627 | await Promise.all( 628 | images.map(async image => { 629 | const { base64 } = await getPlaiceholder(image.url); 630 | image.blurDataUrl = base64; 631 | }) 632 | ); 633 | 634 | return { 635 | props: { 636 | data: data.post, 637 | }, 638 | }; 639 | }; 640 | 641 | export default SinglePostPage; 642 | ``` 643 | 644 | ### Gatsby Link component 645 | 646 | ```js 647 | import { Link } from 'gatsby'; 648 | import { RichText } from '@graphcms/rich-text-react-renderer'; 649 | 650 | const content = { 651 | /* ... */ 652 | }; 653 | 654 | function App() { 655 | return ( 656 | { 660 | if (href.match(/^https?:\/\/|^\/\//i)) { 661 | return ( 662 | 668 | {children} 669 | 670 | ); 671 | } 672 | 673 | return ( 674 | 675 | {children} 676 | 677 | ); 678 | }, 679 | }} 680 | /> 681 | ); 682 | } 683 | ``` 684 | 685 | ### Gatsby Image component 686 | 687 | Unfortunately, there's no way to use the Gatsby Image component with this package at the moment. The `GatsbyImage` component (for dynamic images) fetches the image from a query during build time, which is not possible to accomplish right now. For more information, see [hygraph/rich-text#16](https://github.com/hygraph/rich-text/pull/16). 688 | 689 | ### Code blocks with [Prism.js](https://prismjs.com/) 690 | 691 | By default, as you may have already realized, the code-blocks rendered by the package don't have any unique styling since we're unopinionated on how it should look on your application. But, if you need, you can create your code block, add a background color for it, add some padding, and adjust based on your needs. 692 | 693 | If you want to go one step away, you can also integrate with [Prism.js](https://prismjs.com/) or [highlight.js](https://highlightjs.org/). Below you can see an example using Prism.js: 694 | 695 | > Note: we still don't support defining a custom language for a code block in the Rich Text field. 696 | 697 | ```jsx 698 | import { useEffect } from 'react'; 699 | import { RichText } from '@graphcms/rich-text-react-renderer'; 700 | 701 | import Prism from 'prismjs'; 702 | import 'prismjs/plugins/line-numbers/prism-line-numbers'; 703 | import 'prismjs/themes/prism-tomorrow.css'; 704 | import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; 705 | 706 | const content = { 707 | /* ... */ 708 | }; 709 | 710 | function App() { 711 | useEffect(() => { 712 | Prism.highlightAll(); 713 | }, []); 714 | 715 | return ( 716 | { 720 | return ( 721 |
    722 |               {children}
    723 |             
    724 | ); 725 | }, 726 | }} 727 | /> 728 | ); 729 | } 730 | ``` 731 | 732 | ## 📝 License 733 | 734 | Licensed under the MIT License. 735 | 736 | --- 737 | 738 | Made with 💜 by Hygraph 👋 [join our community](https://slack.hygraph.com/)! 739 | -------------------------------------------------------------------------------- /packages/react-renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphcms/rich-text-react-renderer", 3 | "description": "Hygraph Rich Text React renderer", 4 | "version": "0.6.2", 5 | "author": "João Pedro Schmitz (https://joaopedro.dev)", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "tsdx watch --tsconfig tsconfig.build.json --verbose --noClean", 9 | "build": "tsdx build --tsconfig tsconfig.build.json", 10 | "test": "tsdx test --passWithNoTests --silent", 11 | "lint": "tsdx lint", 12 | "prepublish": "npm run build" 13 | }, 14 | "dependencies": { 15 | "@graphcms/rich-text-types": "^0.5.0", 16 | "escape-html": "^1.0.3" 17 | }, 18 | "devDependencies": { 19 | "@types/escape-html": "^1.0.2" 20 | }, 21 | "peerDependencies": { 22 | "react": ">=16", 23 | "react-dom": ">=16" 24 | }, 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "keywords": [ 29 | "react", 30 | "rich-text", 31 | "renderer", 32 | "hygraph" 33 | ], 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/hygraph/rich-text.git", 37 | "directory": "packages/react-renderer" 38 | }, 39 | "main": "dist/index.js", 40 | "module": "dist/rich-text-react-renderer.esm.js", 41 | "types": "dist/index.d.ts", 42 | "files": [ 43 | "README.md", 44 | "LICENSE.md", 45 | "dist" 46 | ], 47 | "jest": { 48 | "setupFilesAfterEnv": [ 49 | "@testing-library/jest-dom/extend-expect" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-renderer/src/RenderText.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { Text } from '@graphcms/rich-text-types'; 3 | 4 | import { RichTextProps, NodeRendererType } from './types'; 5 | 6 | function serialize(text: string) { 7 | if (text.includes('\n')) { 8 | const splitText = text.split('\n'); 9 | 10 | return splitText.map((line, index) => ( 11 | 12 | {line} 13 | {index === splitText.length - 1 ? null :
    } 14 |
    15 | )); 16 | } 17 | 18 | return text; 19 | } 20 | 21 | export function RenderText({ 22 | textNode, 23 | renderers, 24 | shouldSerialize, 25 | }: { 26 | textNode: Text; 27 | renderers?: RichTextProps['renderers']; 28 | shouldSerialize: boolean; 29 | }) { 30 | const { text, bold, italic, underline, code } = textNode; 31 | 32 | let parsedText: ReactNode = shouldSerialize ? serialize(text) : text; 33 | 34 | const Bold: NodeRendererType['bold'] = renderers?.['bold']; 35 | const Italic: NodeRendererType['italic'] = renderers?.['italic']; 36 | const Underline: NodeRendererType['underline'] = renderers?.['underline']; 37 | const Code: NodeRendererType['code'] = renderers?.['code']; 38 | 39 | if (bold && Bold) { 40 | parsedText = {parsedText}; 41 | } 42 | 43 | if (italic && Italic) { 44 | parsedText = {parsedText}; 45 | } 46 | 47 | if (underline && Underline) { 48 | parsedText = {parsedText}; 49 | } 50 | 51 | if (code && Code) { 52 | parsedText = {parsedText}; 53 | } 54 | 55 | return <>{parsedText}; 56 | } 57 | -------------------------------------------------------------------------------- /packages/react-renderer/src/RichText.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { 3 | ElementNode, 4 | EmptyElementsToRemove, 5 | Node, 6 | isElement, 7 | isText, 8 | isEmpty, 9 | elementTypeKeys, 10 | } from '@graphcms/rich-text-types'; 11 | 12 | import { defaultElements } from './defaultElements'; 13 | import { RenderText } from './RenderText'; 14 | import { RichTextProps } from './types'; 15 | 16 | function getArrayOfElements(content: RichTextProps['content']) { 17 | return Array.isArray(content) ? content : content.children; 18 | } 19 | 20 | function RenderNode({ 21 | node, 22 | parent, 23 | renderers, 24 | references, 25 | }: { 26 | node: Node; 27 | parent: Node | null; 28 | renderers?: RichTextProps['renderers']; 29 | references?: RichTextProps['references']; 30 | }) { 31 | if (isText(node)) { 32 | let text = node.text; 33 | 34 | const shouldSerialize = 35 | parent && isElement(parent) && parent.type !== 'code-block'; 36 | 37 | return ( 38 | 43 | ); 44 | } 45 | 46 | if (isElement(node)) { 47 | return ( 48 | 53 | ); 54 | } 55 | 56 | const { type } = node as ElementNode; 57 | 58 | if (__DEV__) { 59 | console.warn( 60 | `[@graphcms/rich-text-react-renderer]: Unknown node type encountered: ${type}` 61 | ); 62 | } 63 | 64 | return ; 65 | } 66 | 67 | function RenderElement({ 68 | element, 69 | renderers, 70 | references, 71 | }: { 72 | element: ElementNode; 73 | renderers?: RichTextProps['renderers']; 74 | references?: RichTextProps['references']; 75 | }) { 76 | const { children, type, ...rest } = element; 77 | const { nodeId, nodeType } = rest; 78 | 79 | // Checks if the element is empty so that it can be removed. 80 | if (type in EmptyElementsToRemove && isEmpty({ children })) { 81 | return ; 82 | } 83 | 84 | const isEmbed = nodeId && nodeType; 85 | 86 | /** 87 | * The .filter method returns an array with all elements found. 88 | * Since there won't be duplicated ID's, it's safe to use the first element. 89 | */ 90 | const referenceValues = isEmbed 91 | ? references?.filter(ref => ref.id === nodeId)[0] 92 | : null; 93 | 94 | /** 95 | * `id` is used to correctly find the props for the reference. 96 | * If it's not present, we show an error and render a Fragment. 97 | */ 98 | if (__DEV__ && isEmbed && !referenceValues?.id) { 99 | console.error( 100 | `[@graphcms/rich-text-react-renderer]: No id found for embed node ${nodeId}. In order to render custom embeds, \`id\` is required in your reference query.` 101 | ); 102 | 103 | return ; 104 | } 105 | 106 | /** 107 | * `mimeType` is used to determine if the node is an image or a video. 108 | * That's why this is required and we show an error if it's not present. 109 | * Only for custom assets embeds. 110 | */ 111 | if ( 112 | __DEV__ && 113 | isEmbed && 114 | nodeType === 'Asset' && 115 | !referenceValues?.mimeType 116 | ) { 117 | console.error( 118 | `[@graphcms/rich-text-react-renderer]: No mimeType found for embed node ${nodeId}. In order to render custom assets, \`mimeType\` is required in your reference query.` 119 | ); 120 | 121 | return ; 122 | } 123 | 124 | /** 125 | * `url` is needed to correctly render the image, video, audio or any other asset 126 | * Only for custom assets embeds. 127 | */ 128 | if (__DEV__ && isEmbed && nodeType === 'Asset' && !referenceValues?.url) { 129 | console.error( 130 | `[@graphcms/rich-text-react-renderer]: No url found for embed node ${nodeId}. In order to render custom assets, \`url\` is required in your reference query.` 131 | ); 132 | 133 | return ; 134 | } 135 | 136 | /** 137 | * There's two options if the element is an embed. 138 | * 1. If it isn't an asset, then we simply try to use the renderer for that model. 139 | * 1.1 If we don't find a renderer, we render a Fragment and show a warning. 140 | * 2. If it is an asset, then: 141 | * 2.1 If we have a custom renderer for that specific mimeType, we use it. 142 | * 2.2 If we don't have, we use the default mimeType group renderer (application, image, video...). 143 | */ 144 | let elementToRender; 145 | 146 | // Option 1 147 | if (isEmbed && nodeType !== 'Asset') { 148 | const element = 149 | type === 'link' 150 | ? renderers?.link?.[nodeType as string] 151 | : renderers?.embed?.[nodeType as string]; 152 | 153 | if (element !== undefined) { 154 | elementToRender = element; 155 | } else { 156 | // Option 1.1 157 | console.warn( 158 | `[@graphcms/rich-text-react-renderer]: No renderer found for custom ${type} nodeType ${nodeType}.` 159 | ); 160 | return ; 161 | } 162 | } 163 | 164 | // Option 2 165 | if (isEmbed && nodeType === 'Asset') { 166 | const element = renderers?.Asset?.[referenceValues?.mimeType]; 167 | 168 | // Option 2.1 169 | if (element !== undefined) { 170 | elementToRender = element; 171 | } else { 172 | // Option 2.2 173 | const mimeTypeGroup = referenceValues?.mimeType.split('/')[0]; 174 | elementToRender = renderers?.Asset?.[mimeTypeGroup]; 175 | } 176 | } 177 | 178 | const elementNodeRenderer = isEmbed 179 | ? elementToRender 180 | : renderers?.[elementTypeKeys[type] as keyof RichTextProps['renderers']]; 181 | 182 | const NodeRenderer = elementNodeRenderer as React.ElementType; 183 | 184 | const props = { ...rest, ...referenceValues }; 185 | 186 | if (NodeRenderer) { 187 | return ( 188 | 189 | 195 | 196 | ); 197 | } 198 | 199 | return ; 200 | } 201 | 202 | type RenderElementsProps = RichTextProps & { 203 | parent?: Node | null; 204 | }; 205 | 206 | function RenderElements({ 207 | content, 208 | references, 209 | renderers, 210 | parent, 211 | }: RenderElementsProps) { 212 | const elements = getArrayOfElements(content); 213 | 214 | return ( 215 | <> 216 | {elements.map((node, index) => { 217 | return ( 218 | 225 | ); 226 | })} 227 | 228 | ); 229 | } 230 | 231 | export function RichText({ 232 | content, 233 | renderers: resolvers, 234 | references, 235 | }: RichTextProps) { 236 | // Shallow merge doensn't work here because if we spread over the elements, the 237 | // Asset object will be completly overriden by the resolvers. We need to keep 238 | // the default elements for the Asset that hasn't been writen. 239 | const assetRenderers = { 240 | ...defaultElements?.Asset, 241 | ...resolvers?.Asset, 242 | }; 243 | 244 | const renderers: RichTextProps['renderers'] = { 245 | ...defaultElements, 246 | ...resolvers, 247 | Asset: assetRenderers, 248 | }; 249 | 250 | if (__DEV__ && !content) { 251 | console.error(`[@graphcms/rich-text-react-renderer]: content is required.`); 252 | 253 | return ; 254 | } 255 | 256 | if (__DEV__ && !Array.isArray(content) && !content.children) { 257 | console.error( 258 | `[@graphcms/rich-text-react-renderer]: children is required in content.` 259 | ); 260 | 261 | return ; 262 | } 263 | 264 | /* 265 | Checks if there's a embed type inside the content and if the `references` prop is defined 266 | 267 | If it isn't defined and there's embed elements, it will show a warning 268 | */ 269 | if (__DEV__) { 270 | const elements = getArrayOfElements(content); 271 | 272 | const embedElements = elements.filter(element => element.type === 'embed'); 273 | 274 | if (embedElements.length > 0 && !references) { 275 | console.warn( 276 | `[@graphcms/rich-text-react-renderer]: to render embed elements you need to provide the \`references\` prop` 277 | ); 278 | } 279 | } 280 | 281 | return ( 282 | 287 | ); 288 | } 289 | -------------------------------------------------------------------------------- /packages/react-renderer/src/defaultElements.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { RichTextProps } from './types'; 3 | 4 | import { IFrame, Image, Video, Class, Link, Audio } from './elements'; 5 | 6 | function FallbackForCustomAsset({ mimeType }: { mimeType: string }) { 7 | if (__DEV__) { 8 | console.warn( 9 | `[@graphcms/rich-text-react-renderer]: Unsupported mimeType encountered: ${mimeType}. You need to write your renderer to render it since we are not opinionated about how this asset should be rendered (check our docs for more info).` 10 | ); 11 | } 12 | 13 | return ; 14 | } 15 | 16 | export const defaultElements: Required = { 17 | a: Link, 18 | class: Class, 19 | video: Video, 20 | img: Image, 21 | iframe: IFrame, 22 | blockquote: ({ children }) =>
    {children}
    , 23 | ul: ({ children }) =>
      {children}
    , 24 | ol: ({ children }) =>
      {children}
    , 25 | li: ({ children }) =>
  • {children}
  • , 26 | p: ({ children }) =>

    {children}

    , 27 | h1: ({ children }) =>

    {children}

    , 28 | h2: ({ children }) =>

    {children}

    , 29 | h3: ({ children }) =>

    {children}

    , 30 | h4: ({ children }) =>

    {children}

    , 31 | h5: ({ children }) =>
    {children}
    , 32 | h6: ({ children }) =>
    {children}
    , 33 | table: ({ children }) => {children}
    , 34 | table_head: ({ children }) => {children}, 35 | table_body: ({ children }) => {children}, 36 | table_row: ({ children }) => {children}, 37 | table_cell: ({ children }) => {children}, 38 | table_header_cell: ({ children }) => {children}, 39 | bold: ({ children }) => {children}, 40 | italic: ({ children }) => {children}, 41 | underline: ({ children }) => {children}, 42 | code: ({ children }) => {children}, 43 | code_block: ({ children }) => ( 44 |
    53 |       {children}
    54 |     
    55 | ), 56 | list_item_child: ({ children }) => <>{children}, 57 | Asset: { 58 | audio: props =>