├── .husky
├── .gitignore
├── pre-commit
└── commit-msg
├── types
└── global.d.ts
├── examples
├── react
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── main.tsx
│ │ └── App.tsx
│ ├── tsconfig.node.json
│ ├── index.html
│ ├── .gitignore
│ ├── vite.config.ts
│ ├── package.json
│ └── tsconfig.json
├── svelte
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── main.ts
│ │ └── App.svelte
│ ├── README.md
│ ├── tsconfig.node.json
│ ├── svelte.config.js
│ ├── index.html
│ ├── .gitignore
│ ├── vite.config.ts
│ ├── package.json
│ └── tsconfig.json
├── vue
│ ├── src
│ │ ├── main.js
│ │ └── App.vue
│ ├── README.md
│ ├── index.html
│ ├── .gitignore
│ ├── package.json
│ └── vite.config.js
└── content-example.ts
├── commitlint.config.js
├── packages
├── react-renderer
│ ├── src
│ │ ├── index.ts
│ │ ├── elements
│ │ │ ├── index.ts
│ │ │ ├── Class.tsx
│ │ │ ├── Audio.tsx
│ │ │ ├── Video.tsx
│ │ │ ├── Link.tsx
│ │ │ ├── Image.tsx
│ │ │ └── IFrame.tsx
│ │ ├── RenderText.tsx
│ │ ├── defaultElements.tsx
│ │ ├── types.ts
│ │ └── RichText.tsx
│ ├── tsconfig.build.json
│ ├── LICENSE.md
│ ├── package.json
│ ├── test
│ │ ├── __snapshots__
│ │ │ └── RichText.test.tsx.snap
│ │ └── content.ts
│ ├── CHANGELOG.md
│ └── README.md
├── types
│ ├── tsconfig.build.json
│ ├── src
│ │ ├── util
│ │ │ ├── isText.ts
│ │ │ ├── isElement.ts
│ │ │ └── isEmpty.ts
│ │ └── index.ts
│ ├── README.md
│ ├── LICENSE.md
│ ├── package.json
│ └── CHANGELOG.md
├── html-renderer
│ ├── src
│ │ ├── elements
│ │ │ ├── index.ts
│ │ │ ├── Class.tsx
│ │ │ ├── Audio.tsx
│ │ │ ├── Video.tsx
│ │ │ ├── Link.tsx
│ │ │ ├── Image.tsx
│ │ │ └── IFrame.tsx
│ │ ├── defaultElements.tsx
│ │ ├── types.ts
│ │ └── index.ts
│ ├── tsconfig.build.json
│ ├── test
│ │ ├── __snapshots__
│ │ │ └── index.test.ts.snap
│ │ └── content.ts
│ ├── LICENSE.md
│ ├── package.json
│ ├── CHANGELOG.md
│ └── README.md
└── html-to-slate-ast
│ ├── tsconfig.build.json
│ ├── test
│ ├── pre.html
│ ├── html_input_table.html
│ ├── image.html
│ ├── html_input.html
│ ├── html_input_iframe.html
│ └── google-docs_input.html
│ ├── tsup.config.ts
│ ├── examples
│ ├── node-script.js
│ └── graphql-request-script.js
│ ├── LICENSE.md
│ ├── package.json
│ ├── README.md
│ ├── CHANGELOG.md
│ └── src
│ └── index.ts
├── .lintstagedrc
├── .prettierrc
├── .gitignore
├── lerna.json
├── .eslintrc.js
├── .editorconfig
├── .github
├── label.yml
├── workflows
│ ├── lint.yml
│ ├── release.yml
│ └── main.yml
└── CONTRIBUTING.md
├── .changeset
├── config.json
└── README.md
├── tsconfig.build.json
├── tsconfig.json
├── LICENSE.md
├── README.md
└── package.json
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/types/global.d.ts:
--------------------------------------------------------------------------------
1 | declare var __DEV__: boolean;
2 |
--------------------------------------------------------------------------------
/examples/react/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/packages/react-renderer/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './RichText';
2 | export * from './types';
3 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "**/*.{ts,tsx}": [
3 | "yarn lint --fix",
4 | "yarn format"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/examples/svelte/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "semi": true,
4 | "singleQuote": true,
5 | "trailingComma": "es5"
6 | }
7 |
--------------------------------------------------------------------------------
/examples/vue/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 |
4 | createApp(App).mount('#app')
5 |
--------------------------------------------------------------------------------
/packages/types/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.build.json",
3 | "include": ["src", "types", "../../types"]
4 | }
5 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/types/src/util/isText.ts:
--------------------------------------------------------------------------------
1 | import { Node, Text } from '../';
2 |
3 | export function isText(node: Node): node is Text {
4 | return (node as Text).text !== undefined;
5 | }
6 |
--------------------------------------------------------------------------------
/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/svelte/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "esnext",
5 | "moduleResolution": "node"
6 | },
7 | "include": ["vite.config.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/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/types/src/util/isElement.ts:
--------------------------------------------------------------------------------
1 | import { ElementNode, Node } from '../';
2 |
3 | export function isElement(node: Node): node is ElementNode {
4 | return (node as ElementNode).children !== undefined;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/react-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/types/README.md:
--------------------------------------------------------------------------------
1 | # @graphcms/rich-text-types
2 |
3 | TypeScript definitions for the Hygraph Rich Text field type.
4 |
5 | ---
6 |
7 | Made with 💜 by Hygraph 👋 [join our community](https://slack.hygraph.com)!
8 |
--------------------------------------------------------------------------------
/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/react-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/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-renderer/src/elements/Class.tsx:
--------------------------------------------------------------------------------
1 | import { ClassRendererProps } from '../types';
2 |
3 | export function Class({ className, children }: ClassRendererProps) {
4 | return `
${children}
`;
5 | }
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/react-renderer/src/elements/Class.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ClassRendererProps } from '../types';
3 |
4 | export function Class({ className, children }: ClassRendererProps) {
5 | return {children}
;
6 | }
7 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/examples/vue/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vue Example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/examples/react/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/svelte/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Svelte Example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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/html-renderer/src/elements/Audio.tsx:
--------------------------------------------------------------------------------
1 | export type AudioProps = {
2 | url: string;
3 | };
4 |
5 | export function Audio({ url }: AudioProps) {
6 | return `
7 |
12 |
13 | Your browser doesn't support HTML5 audio. Here is a
14 | link to the audio
15 | instead.
16 |
17 |
18 | `;
19 | }
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/react-renderer/src/elements/Audio.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export type AudioProps = {
4 | url: string;
5 | };
6 |
7 | export function Audio({ url }: AudioProps) {
8 | return (
9 |
14 |
15 | Your browser doesn't support HTML5 audio. Here is a{' '}
16 | link to the audio instead.
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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-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 |
8 |
9 | Your browser doesn't support HTML5 video. Here is a
10 | link to the video instead.
11 |
12 |
13 | `;
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "lib": ["dom", "esnext"],
5 | "importHelpers": true,
6 | "declaration": true,
7 | "sourceMap": true,
8 | "strict": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "moduleResolution": "node",
14 | "jsx": "react",
15 | "esModuleInterop": true,
16 | "skipLibCheck": true,
17 | "forceConsistentCasingInFileNames": true,
18 | "noEmit": true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.build.json",
3 | "include": ["packages", "types", "examples"],
4 | "compilerOptions": {
5 | "allowJs": false,
6 | "baseUrl": ".",
7 | "typeRoots": ["./node_modules/@types", "./types"],
8 | "paths": {
9 | "@graphcms/rich-text-types": ["packages/types/src"],
10 | "@graphcms/rich-text-react-renderer": ["packages/react-renderer/src"],
11 | "@graphcms/rich-text-html-renderer": ["packages/html-renderer/src"],
12 | "@graphcms/html-to-slate-ast": ["packages/html-to-slate-ast/src"],
13 | "$test/*": ["test/*"]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/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/react-renderer/src/elements/Video.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import escapeHtml from 'escape-html';
3 | import { VideoProps } from '@graphcms/rich-text-types';
4 |
5 | export function Video({ src, width, height, title }: Partial) {
6 | return (
7 |
14 |
15 | Your browser doesn't support HTML5 video. Here is a{' '}
16 | link to the video instead.
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/html-to-slate-ast/test/html_input_table.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | R1C1 - BOLD TEXT
7 |
8 |
9 | R1C2
10 |
11 | R1C3
12 |
13 |
14 |
15 | R2C1
16 |
17 |
18 | R2C2 - ITALIC TEXT
19 |
20 |
21 |
22 | R2C3 - BOLD TEXT
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/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/vue/src/App.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
Vue Example
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/react-renderer/src/elements/Link.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import escapeHtml from 'escape-html';
3 | import { LinkElement } from '@graphcms/rich-text-types';
4 | import { LinkRendererProps } from '../types';
5 |
6 | export function Link({ children, ...rest }: LinkRendererProps) {
7 | const { href, rel, id, title, openInNewTab, className } = rest;
8 |
9 | const props: Pick & {
10 | target?: string;
11 | } = {};
12 |
13 | if (rel) props.rel = rel;
14 | if (id) props.id = id;
15 | if (title) props.title = title;
16 | if (className) props.className = className;
17 | if (openInNewTab) props.target = '_blank';
18 |
19 | return (
20 |
21 | {children}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/packages/html-to-slate-ast/test/image.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/react-renderer/src/elements/Image.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import escapeHtml from 'escape-html';
3 | import { ImageProps } from '@graphcms/rich-text-types';
4 |
5 | export function Image({
6 | src,
7 | width,
8 | height,
9 | altText,
10 | title,
11 | }: Partial) {
12 | if (__DEV__ && !src) {
13 | console.warn(
14 | `[@graphcms/rich-text-react-renderer]: src is required. You need to include a \`url\` in your query`
15 | );
16 | }
17 |
18 | const shouldIncludeWidth = width && width > 0;
19 | const shouldIncludeHeight = height && height > 0;
20 |
21 | return (
22 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/packages/types/src/util/isEmpty.ts:
--------------------------------------------------------------------------------
1 | import { ElementNode, Text } from '../';
2 | import { isElement } from './isElement';
3 | import { isText } from './isText';
4 |
5 | export function isEmpty({
6 | children,
7 | }: {
8 | children: (ElementNode | Text)[];
9 | }): boolean {
10 | // Checks if the children array has more than one element.
11 | // It may have a link inside, that's why we need to check this condition.
12 | if (children.length > 1) {
13 | const hasText = children.filter(function f(child): boolean | number {
14 | if (isText(child) && child.text !== '') {
15 | return true;
16 | }
17 |
18 | if (isElement(child)) {
19 | return (child.children = child.children.filter(f)).length;
20 | }
21 |
22 | return false;
23 | });
24 |
25 | return hasText.length > 0 ? false : true;
26 | } else if (children[0].text === '') return true;
27 |
28 | return false;
29 | }
30 |
--------------------------------------------------------------------------------
/packages/react-renderer/src/elements/IFrame.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/iframe-has-title */
2 | import React from 'react';
3 | import escapeHtml from 'escape-html';
4 | import { IFrameProps } from '@graphcms/rich-text-types';
5 |
6 | export function IFrame({ url }: Partial) {
7 | return (
8 |
16 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/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 |
8 |
9 | Your browser doesn't support HTML5 video. Here is a
10 | link to the video instead.
11 |
12 |
13 |
14 |
19 |
20 | Your browser doesn't support HTML5 audio. Here is a
21 | link to the audio
22 | instead.
23 |
24 |
25 | "
26 | `;
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/types/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/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/types/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@graphcms/rich-text-types",
3 | "description": "TypeScript definitions for the Hygraph Rich Text field type.",
4 | "version": "0.5.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",
11 | "lint": "tsdx lint",
12 | "prepublish": "npm run build"
13 | },
14 | "dependencies": {
15 | "tslib": "^2.0.0"
16 | },
17 | "publishConfig": {
18 | "access": "public"
19 | },
20 | "keywords": [
21 | "rich-text",
22 | "types",
23 | "typescript",
24 | "hygraph"
25 | ],
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/hygraph/rich-text.git",
29 | "directory": "packages/types"
30 | },
31 | "main": "dist/index.js",
32 | "module": "dist/rich-text-types.esm.js",
33 | "types": "dist/index.d.ts",
34 | "files": [
35 | "README.md",
36 | "LICENSE.md",
37 | "dist"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-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 | VIDEO
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-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/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/html-to-slate-ast/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@graphcms/html-to-slate-ast",
3 | "version": "0.14.2",
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/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/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 |
--------------------------------------------------------------------------------
/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 |
63 | ),
64 | text: () => (
65 |
68 | ),
69 | },
70 | }}
71 | />
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/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 }) => ``,
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 }) => ``,
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/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/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 }) => ,
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 }) => ,
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 => ,
59 | image: props => ,
60 | video: props => ,
61 | font: FallbackForCustomAsset,
62 | application: FallbackForCustomAsset,
63 | model: FallbackForCustomAsset,
64 | text: FallbackForCustomAsset,
65 | },
66 | embed: {},
67 | link: {},
68 | };
69 |
--------------------------------------------------------------------------------
/packages/react-renderer/src/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import {
3 | EmbedReferences,
4 | IFrameProps,
5 | ImageProps,
6 | RichTextContent,
7 | VideoProps,
8 | ClassProps,
9 | LinkProps,
10 | } from '@graphcms/rich-text-types';
11 |
12 | export interface DefaultElementProps {
13 | children: ReactNode;
14 | }
15 |
16 | export interface ClassRendererProps
17 | extends DefaultElementProps,
18 | Partial {}
19 |
20 | export interface LinkRendererProps
21 | extends DefaultElementProps,
22 | Partial {}
23 |
24 | type DefaultNodeRenderer = (props: DefaultElementProps) => JSX.Element;
25 | type LinkNodeRenderer = (props: LinkRendererProps) => JSX.Element;
26 | type ClassNodeRenderer = (props: ClassRendererProps) => JSX.Element;
27 | type ImageNodeRenderer = (props: Partial) => JSX.Element;
28 | type VideoNodeRenderer = (props: Partial) => JSX.Element;
29 | type IFrameNodeRenderer = (props: Partial) => JSX.Element;
30 | type EmbedNodeRenderer = (props: any) => JSX.Element;
31 |
32 | export type NodeRendererType = {
33 | a?: LinkNodeRenderer;
34 | class?: ClassNodeRenderer;
35 | img?: ImageNodeRenderer;
36 | video?: VideoNodeRenderer;
37 | iframe?: IFrameNodeRenderer;
38 | h1?: DefaultNodeRenderer;
39 | h2?: DefaultNodeRenderer;
40 | h3?: DefaultNodeRenderer;
41 | h4?: DefaultNodeRenderer;
42 | h5?: DefaultNodeRenderer;
43 | h6?: DefaultNodeRenderer;
44 | p?: DefaultNodeRenderer;
45 | ul?: DefaultNodeRenderer;
46 | ol?: DefaultNodeRenderer;
47 | li?: DefaultNodeRenderer;
48 | list_item_child?: DefaultNodeRenderer;
49 | table?: DefaultNodeRenderer;
50 | table_head?: DefaultNodeRenderer;
51 | table_body?: DefaultNodeRenderer;
52 | table_row?: DefaultNodeRenderer;
53 | table_cell?: DefaultNodeRenderer;
54 | table_header_cell?: DefaultNodeRenderer;
55 | blockquote?: DefaultNodeRenderer;
56 | bold?: DefaultNodeRenderer;
57 | italic?: DefaultNodeRenderer;
58 | underline?: DefaultNodeRenderer;
59 | code?: DefaultNodeRenderer;
60 | code_block?: DefaultNodeRenderer;
61 | Asset?: {
62 | application?: EmbedNodeRenderer;
63 | audio?: EmbedNodeRenderer;
64 | font?: EmbedNodeRenderer;
65 | image?: EmbedNodeRenderer;
66 | model?: EmbedNodeRenderer;
67 | text?: EmbedNodeRenderer;
68 | video?: EmbedNodeRenderer;
69 | [key: string]: EmbedNodeRenderer | undefined;
70 | };
71 | embed?: {
72 | [key: string]: EmbedNodeRenderer | undefined;
73 | };
74 | link?: {
75 | [key: string]: EmbedNodeRenderer | undefined;
76 | };
77 | };
78 |
79 | export type RichTextProps = {
80 | content: RichTextContent;
81 | references?: EmbedReferences;
82 | renderers?: NodeRendererType;
83 | };
84 |
--------------------------------------------------------------------------------
/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 = ''; // 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/react-renderer/test/__snapshots__/RichText.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`@graphcms/rich-text-react-renderer renders iframe 1`] = `
4 |
18 | `;
19 |
20 | exports[`@graphcms/rich-text-react-renderer renders inline content 1`] = `
21 |
22 |
23 |
24 | Hey,
25 |
26 |
27 | how
28 |
29 |
30 | are
31 |
32 |
33 | you?
34 |
35 |
36 |
37 | `;
38 |
39 | exports[`@graphcms/rich-text-react-renderer renders inline content with custom renderers 1`] = `
40 |
41 |
42 |
43 | Hey,
44 |
45 |
49 | how
50 |
51 |
54 | are
55 | test
56 |
57 |
60 | you?
61 |
62 |
63 |
64 | `;
65 |
66 | exports[`@graphcms/rich-text-react-renderer renders video 1`] = `
67 |
87 | `;
88 |
89 | exports[`@graphcms/rich-text-react-renderer should render empty text spaces 1`] = `
90 |
91 |
92 | Sweet black
93 |
94 | cap
95 |
96 |
97 |
98 | with
99 |
100 |
101 |
102 | embroidered
103 |
104 |
105 |
106 | Hygraph
107 |
108 | logo.
109 |
110 |
111 | `;
112 |
113 | exports[`@graphcms/rich-text-react-renderer should replace all
114 | in a string with elements 1`] = `
115 |
116 |
117 | Hello,
118 |
119 | My name is joão pedro,
120 |
121 | I'm testing a bug
122 |
123 |
124 | `;
125 |
126 | exports[`custom embeds and assets should render video, image, and audio assets 1`] = `
127 |
166 | `;
167 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/packages/types/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @graphcms/rich-text-types
2 |
3 | ## 0.5.1
4 |
5 | ### Patch Changes
6 |
7 | - [`86411d2`](https://github.com/hygraph/rich-text/commit/86411d2ca48761ad4f0d2dba141abfa3b77f050c) [#117](https://github.com/hygraph/rich-text/pull/117) Thanks [@joelpierre](https://github.com/joelpierre)! - Fix/video prop types
8 |
9 | ## 0.5.0
10 |
11 | ### Minor Changes
12 |
13 | - [`b272253`](https://github.com/hygraph/rich-text/commit/b2722534275efd2c5e473d549d0f0e5a28100025) [#84](https://github.com/hygraph/rich-text/pull/84) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - - Add type `LinkEmbedProps`
14 | - Update `EmbedElement`
15 | - Add `id` to `EmbedProps`
16 |
17 | ## 0.4.0
18 |
19 | ### Minor Changes
20 |
21 | - [`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 release has breaking changes!
22 |
23 | ⚡️ New
24 |
25 | - Add `isEmpty` utility function
26 | - Add `EmptyElementsToRemove` enum as a replacement for `RemoveEmptyElementType`
27 | - Add `elementTypeKeys`
28 |
29 | ⚠️ Breaking Changes
30 |
31 | > The `RichTextProps`, `ClassRendererProps`, `LinkRendererProps`, `DefaultElementProps` and the `NodeRendererType` type are now available on the `@graphcms/rich-text-react-renderer` package.
32 |
33 | - Remove `RichTextProps` type
34 | - Remove `NodeRendererType` type
35 | - Remove `RemoveEmptyElementType` type
36 | - Remove `ClassRendererProps` type
37 | - Remove `LinkRendererProps` type
38 | - Remove `DefaultElementProps` type
39 |
40 | ## 0.3.1
41 |
42 | ### Patch Changes
43 |
44 | - [`c2e0a75`](https://github.com/hygraph/rich-text/commit/c2e0a75e995591bb299250f4d14092b1843b1183) [#53](https://github.com/hygraph/rich-text/pull/53) Thanks [@anmolarora1](https://github.com/anmolarora1)! - Add `table_header_cell` in `Element` and `NodeRendererType`
45 |
46 | ## 0.3.0
47 |
48 | ### Minor Changes
49 |
50 | - [`bc9e612`](https://github.com/hygraph/rich-text/commit/bc9e61293ec0535328541c95c33e71f51ec09c43) [#52](https://github.com/hygraph/rich-text/pull/52) Thanks [@larisachristie](https://github.com/larisachristie)! - Add isInline to EmbedProps type
51 |
52 | ## 0.2.1
53 |
54 | ### Patch Changes
55 |
56 | - [`91495b9`](https://github.com/hygraph/rich-text/commit/91495b9f3649c0bf92326d52365473d376ad598f) [#29](https://github.com/hygraph/rich-text/pull/29) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - Include `code-block` in the list of types in `Element`
57 |
58 | ## 0.2.0
59 |
60 | ### Minor Changes
61 |
62 | - [`768492a`](https://github.com/hygraph/rich-text/commit/768492a5dd5e642cc639b82cd7e13f2ce7f2dc96) [#25](https://github.com/hygraph/rich-text/pull/25) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)! - - Include `embed` in the list of types in `Element`
63 | - Add new type `VideoMimeTypes`
64 | - Add new type `AssetMimeTypes`
65 | - Add new type `EmbedProps`
66 | - Add new type `EmbedElement`
67 | - Add new type `AssetReference`
68 | - Add new type `Reference`
69 | - Add new type `EmbedReferences`
70 | - Add `EmbedElement` to `ElementNode`
71 | - Add `references` to `RichTextProps`
72 | - Add `Asset` and `embed` to `NodeRendererType`
73 |
74 | ## 0.1.4
75 |
76 | ### Patch Changes
77 |
78 | - [`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
79 |
80 | ## 0.1.3
81 |
82 | ### Patch Changes
83 |
84 | - [`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.
85 |
86 | ## 0.1.2
87 |
88 | ### Patch Changes
89 |
90 | - [`23b87f6`](https://github.com/hygraph/rich-text/commit/23b87f6218040df283d112307c3720645a5936aa) [#6](https://github.com/hygraph/rich-text/pull/6) Thanks [@KaterBasilisk6](https://github.com/KaterBasilisk6)!
91 |
92 | - Add `RemoveEmptyElementType`
93 |
94 | ## 0.1.1
95 |
96 | ### Patch Changes
97 |
98 | - [`d831c93`](https://github.com/hygraph/rich-text/commit/d831c93be2f1a07aea2377e0d5842e130e104bfd) [#2](https://github.com/hygraph/rich-text/pull/2) Thanks [@jpedroschmitz](https://github.com/jpedroschmitz)!
99 |
100 | - Add `list_item_child` to `NodeRendererType`;
101 | - Accept array of `ElementNode` and object with `children` on `RichTextContent`.
102 |
103 | ## 0.1.1-canary.0
104 |
105 | ### Patch Changes
106 |
107 | - Add `list_item_child` to `NodeRendererType`;
108 | - Accept array of `ElementNode` and object with `children` on `RichTextContent`.
109 |
--------------------------------------------------------------------------------
/packages/types/src/index.ts:
--------------------------------------------------------------------------------
1 | export interface Element {
2 | children: Array;
3 | type:
4 | | 'bulleted-list'
5 | | 'numbered-list'
6 | | 'list-item'
7 | | 'list-item-child'
8 | | 'table'
9 | | 'table_head'
10 | | 'table_body'
11 | | 'table_row'
12 | | 'table_cell'
13 | | 'table_header_cell'
14 | | 'block-quote'
15 | | 'paragraph'
16 | | 'heading-one'
17 | | 'heading-two'
18 | | 'heading-three'
19 | | 'heading-four'
20 | | 'heading-five'
21 | | 'heading-six'
22 | | 'class'
23 | | 'link'
24 | | 'image'
25 | | 'video'
26 | | 'iframe'
27 | | 'embed'
28 | | 'code-block';
29 |
30 | [key: string]: unknown;
31 | }
32 |
33 | export type ImageMimeTypes =
34 | | 'image/webp'
35 | | 'image/jpeg'
36 | | 'image/bmp'
37 | | 'image/gif'
38 | | 'image/png';
39 |
40 | export type VideoMimeTypes =
41 | | 'video/quicktime'
42 | | 'video/mp4'
43 | | 'video/ogg'
44 | | 'video/webm'
45 | | 'video/x-msvideo';
46 |
47 | export type AssetMimeTypes = ImageMimeTypes | VideoMimeTypes | string;
48 |
49 | export interface Text extends Mark {
50 | text: string;
51 | }
52 |
53 | export type Mark = {
54 | bold?: boolean;
55 | italic?: boolean;
56 | underline?: boolean;
57 | code?: boolean;
58 | };
59 |
60 | export interface ClassProps {
61 | className: string;
62 | }
63 |
64 | export interface ClassElement extends ClassProps, Element {
65 | type: 'class';
66 | }
67 |
68 | export interface LinkProps {
69 | href: string;
70 | className?: string;
71 | rel?: string;
72 | id?: string;
73 | title?: string;
74 | openInNewTab?: boolean;
75 | }
76 |
77 | export interface LinkElement extends LinkProps, Element {
78 | type: 'link';
79 | }
80 |
81 | export interface ImageProps {
82 | type: 'image';
83 | src: string;
84 | title?: string;
85 | width?: number;
86 | height?: number;
87 | handle?: string;
88 | mimeType?: ImageMimeTypes;
89 | altText?: string;
90 | }
91 |
92 | export interface ImageElement extends ImageProps, Element {
93 | type: 'image';
94 | }
95 |
96 | export interface VideoProps {
97 | src: string;
98 | type: 'video';
99 | title?: string;
100 | width: number | null;
101 | height: number | null;
102 | handle?: string;
103 | mimeType: VideoMimeTypes;
104 | }
105 |
106 | export interface VideoElement extends VideoProps, Element {
107 | type: 'video';
108 | }
109 |
110 | export interface IFrameProps {
111 | url: string;
112 | width?: number;
113 | height?: number;
114 | }
115 |
116 | export interface IFrameElement extends IFrameProps, Element {
117 | type: 'iframe';
118 | }
119 |
120 | export type EmbedProps = T & {
121 | id: string;
122 | nodeId: string;
123 | nodeType: string;
124 | isInline?: boolean;
125 | };
126 |
127 | export type LinkEmbedProps = T & {
128 | id: string;
129 | nodeId: string;
130 | nodeType: string;
131 | children: any;
132 | };
133 |
134 | export interface EmbedElement extends EmbedProps, Element {
135 | type: 'embed' | 'link';
136 | }
137 |
138 | export type ElementNode =
139 | | Element
140 | | ClassElement
141 | | LinkElement
142 | | ImageElement
143 | | IFrameElement
144 | | VideoElement
145 | | EmbedElement;
146 |
147 | export type Node = ElementNode | Text;
148 |
149 | export type RichTextContent =
150 | | Array
151 | | { children: Array };
152 |
153 | export type AssetReference = {
154 | id: string;
155 | mimeType: AssetMimeTypes;
156 | [key: string]: any;
157 | };
158 |
159 | export type Reference = {
160 | id: string;
161 | [key: string]: any;
162 | };
163 |
164 | export type EmbedReferences = Array;
165 |
166 | export enum EmptyElementsToRemove {
167 | 'heading-one',
168 | 'heading-two',
169 | 'heading-three',
170 | 'heading-four',
171 | 'heading-five',
172 | 'heading-six',
173 | 'table_head',
174 | }
175 |
176 | export const elementTypeKeys: { [key: string]: string } = {
177 | 'heading-one': 'h1',
178 | 'heading-two': 'h2',
179 | 'heading-three': 'h3',
180 | 'heading-four': 'h4',
181 | 'heading-five': 'h5',
182 | 'heading-six': 'h6',
183 | class: 'class',
184 | link: 'a',
185 | image: 'img',
186 | iframe: 'iframe',
187 | video: 'video',
188 | 'bulleted-list': 'ul',
189 | 'numbered-list': 'ol',
190 | 'list-item': 'li',
191 | 'list-item-child': 'list_item_child',
192 | table: 'table',
193 | table_head: 'table_head',
194 | table_body: 'table_body',
195 | table_row: 'table_row',
196 | table_cell: 'table_cell',
197 | table_header_cell: 'table_header_cell',
198 | 'block-quote': 'blockquote',
199 | paragraph: 'p',
200 | bold: 'bold',
201 | italic: 'italic',
202 | underline: 'underline',
203 | code: 'code',
204 | 'code-block': 'code_block',
205 | };
206 |
207 | export * from './util/isElement';
208 | export * from './util/isText';
209 | export * from './util/isEmpty';
210 |
--------------------------------------------------------------------------------
/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/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/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:
Ordered list:
One
Two
Table:
--------------------------------------------------------------------------------
/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/html-to-slate-ast/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @graphcms/html-to-slate-ast
2 |
3 | ## 0.14.2
4 |
5 | ### Patch Changes
6 |
7 | - [`4f506a2`](https://github.com/hygraph/rich-text/commit/4f506a2337ad139fc0df7ef1a29266e776045fc1) [#136](https://github.com/hygraph/rich-text/pull/136) Thanks [@borivojevic](https://github.com/borivojevic)! - fix: preserve graphassets images during HTML import
8 |
9 | ## 0.14.1
10 |
11 | ### Patch Changes
12 |
13 | - [`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
14 |
15 | ## 0.14.0
16 |
17 | ### Minor Changes
18 |
19 | - [`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
20 |
21 | ### Patch Changes
22 |
23 | - [`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
24 |
25 | * [`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
26 |
27 | - [`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
28 |
29 | * [`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
30 |
31 | - [`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
32 |
33 | ## 0.14.0-canary.5
34 |
35 | ### Patch Changes
36 |
37 | - Revert export changes which breaks types
38 |
39 | ## 0.14.0-canary.4
40 |
41 | ### Patch Changes
42 |
43 | - Export htmlToSlateASTSync from index
44 |
45 | ## 0.14.0-canary.3
46 |
47 | ### Patch Changes
48 |
49 | - Correctly export it from /client
50 |
51 | ## 0.14.0-canary.2
52 |
53 | ### Patch Changes
54 |
55 | - Export htmlToSlateASTSync from /client
56 |
57 | ## 0.14.0-canary.1
58 |
59 | ### Patch Changes
60 |
61 | - Fix htmlToSlateASTSync not working
62 |
63 | ## 0.14.0-canary.0
64 |
65 | ### Minor Changes
66 |
67 | - Add htmlToSlateASTSync function
68 |
69 | ## 0.13.3
70 |
71 | ### Patch Changes
72 |
73 | - [`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
74 |
75 | ## 0.13.2
76 |
77 | ### Patch Changes
78 |
79 | - [`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
80 |
81 | ## 0.13.1
82 |
83 | ### Patch Changes
84 |
85 | - [`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
86 |
87 | ## 0.13.0
88 |
89 | ### Minor Changes
90 |
91 | - [`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
92 | - Add bold text support to table cells
93 | - Add nested tags support to table cells
94 |
95 | ## 0.12.1
96 |
97 | ### Patch Changes
98 |
99 | - Updated dependencies [[`b272253`](https://github.com/hygraph/rich-text/commit/b2722534275efd2c5e473d549d0f0e5a28100025)]:
100 | - @graphcms/rich-text-types@0.5.0
101 |
102 | ## 0.12.0
103 |
104 | ### Minor Changes
105 |
106 | - [`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
107 |
108 | ### Patch Changes
109 |
110 | - Updated dependencies [[`b7f16fa`](https://github.com/hygraph/rich-text/commit/b7f16fa76a28ad0f5cdbe6cb1f58d7fafa63df15)]:
111 | - @graphcms/rich-text-types@0.4.0
112 |
113 | ## 0.11.1
114 |
115 | ### Patch Changes
116 |
117 | - [`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
118 |
119 | ## 0.11.0
120 |
121 | ### Minor Changes
122 |
123 | - [`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
124 |
125 | ### Patch Changes
126 |
127 | - Updated dependencies [[`c2e0a75`](https://github.com/hygraph/rich-text/commit/c2e0a75e995591bb299250f4d14092b1843b1183)]:
128 | - @graphcms/rich-text-types@0.3.1
129 |
130 | ## 0.10.1
131 |
132 | ### Patch Changes
133 |
134 | - Updated dependencies [[`bc9e612`](https://github.com/hygraph/rich-text/commit/bc9e61293ec0535328541c95c33e71f51ec09c43)]:
135 | - @graphcms/rich-text-types@0.3.0
136 |
137 | ## 0.10.0
138 |
139 | ### Minor Changes
140 |
141 | - [`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
142 |
143 | ## 0.9.0
144 |
145 | ### Minor Changes
146 |
147 | - [`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
148 |
149 | ## 0.8.1
150 |
151 | ### Patch Changes
152 |
153 | - [`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
154 |
155 | ## 0.8.0
156 |
157 | ### Minor Changes
158 |
159 | - [`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
160 |
161 | ## 0.7.0
162 |
163 | ### Minor Changes
164 |
165 | - [`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
166 |
167 | ## 0.6.0
168 |
169 | ### Minor Changes
170 |
171 | - [`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
172 |
173 | ## 0.5.1
174 |
175 | ### Patch Changes
176 |
177 | - Updated dependencies [[`768492a`](https://github.com/hygraph/rich-text/commit/768492a5dd5e642cc639b82cd7e13f2ce7f2dc96)]:
178 | - @graphcms/rich-text-types@0.2.0
179 |
180 | ## 0.5.0
181 |
182 | ### Minor Changes
183 |
184 | - [`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
185 |
186 | ## 0.4.0
187 |
188 | ### Minor Changes
189 |
190 | - [`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
191 |
192 | ## 0.3.0
193 |
194 | ### Minor Changes
195 |
196 | - [`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
197 |
198 | ## 0.2.0
199 |
200 | ### Minor Changes
201 |
202 | - [`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.
203 |
204 | ## 0.1.2
205 |
206 | ### Patch Changes
207 |
208 | - [`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.
209 |
210 | - Updated dependencies [[`7cb7b7e`](https://github.com/hygraph/rich-text/commit/7cb7b7ef78a465c54982f81c77432d001ea9645b)]:
211 | - @graphcms/rich-text-types@0.1.3
212 |
--------------------------------------------------------------------------------
/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/react-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/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-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 |
58 | // Fix text node when pasting images; sanitize URLs;
59 | // if the image is not hosted on graphassets.com, we convert it to a link
60 | if (href.includes('graphassets.com') === false) {
61 | return {
62 | type: 'link',
63 | href: sanitizeUrl(href),
64 | title,
65 | openInNewTab: true,
66 | };
67 | }
68 |
69 | // if href includes graphassets.com, we convert it to a image
70 | // handle is always the last part of the href
71 | const handle = href.split('/').pop();
72 |
73 | return {
74 | type: 'image',
75 | src: href,
76 | ...(el.hasAttribute('title') && { title: el.getAttribute('title') }),
77 | ...(el.hasAttribute('width') && {
78 | width: Number(el.getAttribute('width')),
79 | }),
80 | ...(el.hasAttribute('height') && {
81 | height: Number(el.getAttribute('height')),
82 | }),
83 | ...(el.hasAttribute('class') && {
84 | className: el.getAttribute('class'),
85 | }),
86 | ...(el.hasAttribute('style') && {
87 | style: el.getAttribute('style'),
88 | }),
89 | ...(handle && { handle }),
90 | };
91 | },
92 | PRE: () => ({ type: 'code-block' }),
93 | IFRAME: el => {
94 | const src = el.getAttribute('src');
95 | if (!src) return {};
96 | const height = el.getAttribute('height');
97 | const width = el.getAttribute('width');
98 | return {
99 | type: 'iframe',
100 | url: src,
101 | // default iframe height is 150
102 | height: Number(height || 150),
103 | // default iframe width is 300
104 | width: Number(width || 300),
105 | children: [
106 | {
107 | text: '',
108 | },
109 | ],
110 | };
111 | },
112 | };
113 |
114 | const TEXT_TAGS: Record<
115 | HTMLElement['nodeName'],
116 | (el?: HTMLElement) => Partial>
117 | > = {
118 | CODE: () => ({ code: true }),
119 | EM: () => ({ italic: true }),
120 | I: () => ({ italic: true }),
121 | STRONG: () => ({ bold: true }),
122 | U: () => ({ underline: true }),
123 | };
124 |
125 | function deserialize<
126 | T extends {
127 | Node: typeof window.Node;
128 | }
129 | >(el: Node, global: T): string | ChildNode[] | BaseElement | Descendant[];
130 |
131 | function deserialize<
132 | T extends {
133 | Node: typeof window.Node;
134 | }
135 | >(el: Node, global: T) {
136 | if (el.nodeType === 3) {
137 | return el.textContent;
138 | } else if (el.nodeType !== 1) {
139 | return null;
140 | } else if (el.nodeName === 'BR') {
141 | // wrap parentless breaks in a paragraph
142 | if (el.parentElement?.nodeName === 'BODY') {
143 | return jsx('element', { type: 'paragraph' }, [{ text: '' }]);
144 | } else {
145 | return '\n';
146 | }
147 | }
148 |
149 | const { nodeName } = el;
150 | let parent = el;
151 |
152 | if (
153 | nodeName === 'PRE' &&
154 | el.childNodes[0] &&
155 | el.childNodes[0].nodeName === 'CODE'
156 | ) {
157 | parent = el.childNodes[0];
158 | }
159 | let children = Array.from(parent.childNodes)
160 | .map(c => deserialize(c, global))
161 | .flat();
162 |
163 | if (children.length === 0) {
164 | if (!['COLGROUP', 'COL', 'CAPTION', 'TFOOT'].includes(nodeName)) {
165 | const textNode = jsx('text', {}, '');
166 | children = [textNode];
167 | }
168 | }
169 | if (el.nodeName === 'BODY') {
170 | return jsx('fragment', {}, children);
171 | }
172 |
173 | if (
174 | isElementNode(el) &&
175 | Array.from(el.attributes).find(
176 | attr => attr.name === 'role' && attr.value === 'heading'
177 | )
178 | ) {
179 | const level = el.attributes.getNamedItem('aria-level')?.value;
180 | switch (level) {
181 | case '1': {
182 | return jsx('element', { type: 'heading-one' }, children);
183 | }
184 | case '2': {
185 | return jsx('element', { type: 'heading-two' }, children);
186 | }
187 | case '3': {
188 | return jsx('element', { type: 'heading-three' }, children);
189 | }
190 | case '4': {
191 | return jsx('element', { type: 'heading-four' }, children);
192 | }
193 | case '5': {
194 | return jsx('element', { type: 'heading-five' }, children);
195 | }
196 | case '6': {
197 | return jsx('element', { type: 'heading-six' }, children);
198 | }
199 |
200 | default:
201 | break;
202 | }
203 | }
204 |
205 | if (ELEMENT_TAGS[nodeName]) {
206 | const attrs = ELEMENT_TAGS[nodeName](el as HTMLElement);
207 | // li children must be rendered in spans, like in list plugin
208 | if (nodeName === 'LI') {
209 | const hasNestedListChild = children.find(
210 | item =>
211 | SlateElement.isElement(item) &&
212 | // if element has a nested list as a child, all children must be wrapped in individual list-item-child nodes
213 | // TODO: sync with GCMS types for Slate elements
214 | // @ts-expect-error
215 | (item.type === 'numbered-list' || item.type === 'bulleted-list')
216 | );
217 | if (hasNestedListChild) {
218 | const wrappedChildren = children.map(item =>
219 | jsx('element', { type: 'list-item-child' }, item)
220 | );
221 | return jsx('element', attrs, wrappedChildren);
222 | }
223 | // in any case we add a single list-item-child containing the children
224 | const child = jsx('element', { type: 'list-item-child' }, children);
225 | return jsx('element', attrs, [child]);
226 | } else if (nodeName === 'TR') {
227 | if (
228 | el.parentElement?.nodeName === 'THEAD' &&
229 | (el as HTMLTableRowElement).cells.length === 0
230 | ) {
231 | return [
232 | {
233 | type: 'table_header_cell',
234 | children: [
235 | {
236 | type: 'paragraph',
237 | children: [{ text: el.textContent ? el.textContent : '' }],
238 | },
239 | ],
240 | },
241 | ];
242 | }
243 | // if TR is empty, insert a cell with a paragraph to ensure selection can be placed inside
244 | const modifiedChildren =
245 | (el as HTMLTableRowElement).cells.length === 0
246 | ? [
247 | {
248 | type:
249 | el.parentElement?.nodeName === 'THEAD'
250 | ? 'table_header_cell'
251 | : 'table_cell',
252 | children: [
253 | {
254 | type: 'paragraph',
255 | children: [{ text: el.textContent ? el.textContent : '' }],
256 | },
257 | ],
258 | },
259 | ]
260 | : children;
261 | return jsx('element', attrs, modifiedChildren);
262 | } else if (nodeName === 'TD' || nodeName === 'TH') {
263 | // if TD or TH is empty, insert a paragraph to ensure selection can be placed inside
264 | const childNodes = Array.from((el as HTMLTableCellElement).childNodes);
265 | if (childNodes.length === 0) {
266 | return jsx('element', attrs, [
267 | {
268 | type: 'paragraph',
269 | children: [{ text: '' }],
270 | },
271 | ]);
272 | } else {
273 | const children = childNodes.map(c => deserialize(c, global)).flat();
274 | return jsx('element', attrs, children);
275 | }
276 | } else if (nodeName === 'IMG') {
277 | return jsx('element', attrs, [attrs.href]);
278 | }
279 | return jsx('element', attrs, children);
280 | }
281 |
282 | if (nodeName === 'DIV') {
283 | if (isElementNode(el)) {
284 | const nodeType = el.getAttribute('data-gcms-embed-type');
285 | const nodeId = el.getAttribute('data-gcms-embed-id');
286 | if (nodeType && nodeId) {
287 | return jsx('element', { type: 'embed', nodeId, nodeType }, children);
288 | }
289 | }
290 |
291 | const childNodes = Array.from(el.childNodes);
292 | const isParagraph = childNodes.every(
293 | child =>
294 | (isElementNode(child) && isInlineElement(child)) || isTextNode(child)
295 | );
296 | if (isParagraph) {
297 | return jsx('element', { type: 'paragraph' }, children);
298 | }
299 | }
300 |
301 | if (nodeName === 'SPAN') {
302 | const parentElement = el.parentElement;
303 | // Handle users copying parts of paragraphs
304 | // When they copy multiple paragraphs we don't need to do anything, because all spans have block parents in that case
305 | if (!parentElement || parentElement.nodeName === 'BODY') {
306 | return jsx('element', { type: 'paragraph' }, children);
307 | }
308 | const element = el as HTMLElement;
309 |
310 | // boolean attribute
311 | const isInlineEmbed = element.getAttribute('data-gcms-embed-inline');
312 |
313 | if (isInlineEmbed !== null && isElementNode(element)) {
314 | const nodeType = element.getAttribute('data-gcms-embed-type');
315 | const nodeId = element.getAttribute('data-gcms-embed-id');
316 | if (nodeId && nodeType) {
317 | return jsx(
318 | 'element',
319 | { type: 'embed', nodeId, nodeType, isInline: true },
320 | children
321 | );
322 | }
323 | }
324 |
325 | // handles italic, bold and undeline that are not expressed as tags
326 | // important for pasting from Google Docs
327 | const attrs = getSpanAttributes(element);
328 |
329 | if (attrs) {
330 | return children.map(child => {
331 | if (typeof child === 'string') {
332 | return jsx('text', attrs, child);
333 | }
334 |
335 | if (isChildNode(child, global)) return child;
336 |
337 | if (SlateElement.isElement(child) && !SlateText.isText(child)) {
338 | child.children = child.children.map(c => ({ ...c, ...attrs }));
339 | return child;
340 | }
341 |
342 | return child;
343 | });
344 | }
345 | }
346 |
347 | if (TEXT_TAGS[nodeName]) {
348 | const attrs = TEXT_TAGS[nodeName](el as HTMLElement);
349 | return children.map(child => {
350 | if (typeof child === 'string') {
351 | return jsx('text', attrs, child);
352 | }
353 |
354 | if (isChildNode(child, global)) return child;
355 |
356 | if (SlateElement.isElement(child) && !SlateText.isText(child)) {
357 | child.children = child.children.map(c => ({ ...c, ...attrs }));
358 | return child;
359 | }
360 |
361 | return child;
362 | });
363 | }
364 |
365 | // general fallback
366 | // skips unsupported tags and prevents block-level element nesting
367 | return children;
368 | }
369 |
370 | /*
371 | CKEditor's Word normalizer functions
372 | 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
373 | */
374 |
375 | // https://github.com/ckeditor/ckeditor5/blob/bce8267e16fccb25448b4c68acc3bf54336aa087/packages/ckeditor5-paste-from-office/src/filters/space.js#L57
376 | function normalizeSafariSpaceSpans(htmlString: string) {
377 | return htmlString.replace(
378 | /(\s+)<\/span>/g,
379 | (_, spaces) => {
380 | return spaces.length === 1
381 | ? ' '
382 | : Array(spaces.length + 1)
383 | .join('\u00A0 ')
384 | .substring(0, spaces.length + 1);
385 | }
386 | );
387 | }
388 |
389 | // https://github.com/ckeditor/ckeditor5/blob/bce8267e16fccb25448b4c68acc3bf54336aa087/packages/ckeditor5-paste-from-office/src/filters/space.js#L19
390 | function normalizeSpacing(htmlString: string) {
391 | // Run normalizeSafariSpaceSpans() two times to cover nested spans.
392 | return (
393 | normalizeSafariSpaceSpans(normalizeSafariSpaceSpans(htmlString))
394 | // Remove all \r\n from "spacerun spans" so the last replace line doesn't strip all whitespaces.
395 | .replace(
396 | /([^\S\r\n]*?)[\r\n]+([^\S\r\n]*<\/span>)/g,
397 | '$1$2'
398 | )
399 | .replace(/<\/span>/g, '')
400 | .replace(/ <\//g, '\u00A0')
401 | .replace(/ <\/o:p>/g, '\u00A0 ')
402 | // Remove block filler from empty paragraph. Safari uses \u00A0 instead of .
403 | .replace(/( |\u00A0)<\/o:p>/g, '')
404 | // Remove all whitespaces when they contain any \r or \n.
405 | .replace(/>([^\S\r\n]*[\r\n]\s*)<')
406 | );
407 | }
408 |
409 | // https://github.com/ckeditor/ckeditor5/blob/bce8267e16fccb25448b4c68acc3bf54336aa087/packages/ckeditor5-paste-from-office/src/filters/parse.js#L102
410 | function cleanContentAfterBody(htmlString: string) {
411 | const bodyCloseTag = '