Repository: contentful/rich-text Branch: master Commit: 1ebea53a99e6 Files: 160 Total size: 520.0 KB Directory structure: gitextract_d499cr83/ ├── .circleci/ │ └── config.yml ├── .contentful/ │ ├── compressed-size.yml │ └── vault-secrets.yaml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github/ │ ├── dependabot.yml │ ├── semantic.yml │ └── workflows/ │ ├── auto-approve.yml │ └── codeql.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .swcrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── baseJestConfig.js ├── catalog-info.yaml ├── commitlint.config.js ├── lerna.json ├── nx.json ├── package.json ├── packages/ │ ├── contentful-slatejs-adapter/ │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── rollup.config.ts │ │ ├── src/ │ │ │ ├── __test__/ │ │ │ │ ├── contentful-helpers.ts │ │ │ │ └── contentful-to-slatejs-adapter.test.ts │ │ │ ├── contentful-to-slatejs-adapter.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── slatejs-to-contentful-adapter.ts │ │ │ └── types/ │ │ │ ├── index.ts │ │ │ └── slate.ts │ │ └── tsconfig.json │ ├── rich-text-from-markdown/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── __test__/ │ │ │ │ ├── helpers.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── real-world.md │ │ │ │ └── real-world.test.ts │ │ │ ├── index.ts │ │ │ └── types/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── rich-text-html-renderer/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── __test__/ │ │ │ │ ├── documents/ │ │ │ │ │ ├── embedded-entry.ts │ │ │ │ │ ├── embedded-resource.ts │ │ │ │ │ ├── heading.ts │ │ │ │ │ ├── hr.ts │ │ │ │ │ ├── hyperlink.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── inline-entity.ts │ │ │ │ │ ├── invalid-marks.ts │ │ │ │ │ ├── invalid-type.ts │ │ │ │ │ ├── mark.ts │ │ │ │ │ ├── ol.ts │ │ │ │ │ ├── paragraph.ts │ │ │ │ │ ├── quote.ts │ │ │ │ │ ├── table-header.ts │ │ │ │ │ ├── table.ts │ │ │ │ │ └── ul.ts │ │ │ │ └── index.test.ts │ │ │ ├── escapeHtml.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── rich-text-links/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── __test__/ │ │ │ │ └── index.test.ts │ │ │ ├── index.ts │ │ │ └── types/ │ │ │ └── utils.ts │ │ └── tsconfig.json │ ├── rich-text-plain-text-renderer/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── bin/ │ │ │ ├── benchmark/ │ │ │ │ └── get-rich-text-entity-links.ts │ │ │ └── tsconfig.json │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── __test__/ │ │ │ │ └── index.test.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── rich-text-react-renderer/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── __test__/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── index.test.tsx.snap │ │ │ │ ├── components/ │ │ │ │ │ ├── Document.tsx │ │ │ │ │ ├── Paragraph.tsx │ │ │ │ │ └── Strong.tsx │ │ │ │ ├── documents/ │ │ │ │ │ ├── embedded-entry.ts │ │ │ │ │ ├── embedded-resource.ts │ │ │ │ │ ├── heading.ts │ │ │ │ │ ├── hr.ts │ │ │ │ │ ├── hyperlink.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── inline-entity.ts │ │ │ │ │ ├── invalid-marks.ts │ │ │ │ │ ├── invalid-type.ts │ │ │ │ │ ├── mark.ts │ │ │ │ │ ├── multi-mark.ts │ │ │ │ │ ├── ol.ts │ │ │ │ │ ├── paragraph.ts │ │ │ │ │ ├── quote.ts │ │ │ │ │ ├── table-header.ts │ │ │ │ │ ├── table.ts │ │ │ │ │ └── ul.ts │ │ │ │ └── index.test.tsx │ │ │ ├── index.tsx │ │ │ └── util/ │ │ │ ├── appendKeyToValidElement.ts │ │ │ └── nodeListToReactComponents.tsx │ │ └── tsconfig.json │ └── rich-text-types/ │ ├── CHANGELOG.md │ ├── README.md │ ├── __mocks__/ │ │ └── @lingui/ │ │ └── core/ │ │ └── macro.js │ ├── __test__/ │ │ ├── helpers.test.ts │ │ ├── schemaConstraints.test.ts │ │ └── validation.test.ts │ ├── jest.config.js │ ├── package.json │ ├── scripts/ │ │ └── fix-esm-import-extensions.mjs │ ├── src/ │ │ ├── blocks.ts │ │ ├── emptyDocument.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── inlines.ts │ │ ├── marks.ts │ │ ├── nodeTypes.ts │ │ ├── schemaConstraints.ts │ │ ├── types.ts │ │ └── validator/ │ │ ├── assert.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── node.ts │ │ ├── path.ts │ │ ├── text.ts │ │ └── types.ts │ └── tsconfig.json ├── pnpm-workspace.yaml ├── renovate.json ├── rollup.config.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2.1 orbs: frontend-tools: contentful/frontend-tools@2 vault: contentful/vault@1.30.0 nx: nrwl/nx@1.6.2 executors: default: docker: - image: cimg/node:24.13.1 cache-key: &cache-key key: v2-npm-cache-{{ arch }}-{{ checksum ".nvmrc" }}-{{ checksum "pnpm-lock.yaml" }}-{{ .Branch }} commands: setup_pnpm: steps: - run: name: Install pnpm package manager command: | sudo corepack enable corepack prepare pnpm --activate pnpm config set store-dir .pnpm-store pnpm_install: steps: - restore_cache: *cache-key - run: node -v - run: pnpm -v - run: pnpm install --frozen-lockfile --prefer-offline - save_cache: <<: *cache-key paths: - .pnpm-store nx_set_shas: steps: - vault/get-secrets: template-preset: nx-read-circleci - nx/set-shas: error-on-no-successful-workflow: true main-branch-name: master jobs: lint-and-test: executor: default steps: - checkout - vault/get-secrets: template-preset: packages-read - setup_pnpm - pnpm_install - nx_set_shas - run: pnpm exec nx affected --target=build --base=$NX_BASE --head=$NX_HEAD - run: pnpm exec nx affected --target=lint --base=$NX_BASE --head=$NX_HEAD - run: pnpm exec nx affected --target=prettier:check --base=$NX_BASE --head=$NX_HEAD - run: pnpm exec nx affected --target=test --base=$NX_BASE --head=$NX_HEAD - store_test_results: path: reports - persist_to_workspace: root: ~/project paths: - . resource_class: medium+ release: executor: default steps: - checkout - vault/get-secrets: template-preset: semantic-release - attach_workspace: at: ~/project - vault/configure-lerna - run: | echo "@contentful:registry=https://npm.pkg.github.com" >> ~/.npmrc echo "//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_WRITE_TOKEN}" >> ~/.npmrc - run: pnpm nx run-many -t build - run: pnpm lerna version --no-private --conventional-commits --create-release github --yes - run: pnpm lerna publish from-git --yes compressed-size: executor: default steps: - checkout - vault/get-secrets: template-preset: packages-read - vault/get-secrets: template-preset: github-comment - run: | echo "@contentful:registry=https://npm.pkg.github.com" >> ~/.npmrc echo "//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_READ_TOKEN}" >> ~/.npmrc - frontend-tools/compressed-size: package-manager: pnpm workflows: version: 2 # run on every commit commit: jobs: - lint-and-test: context: - vault - release: context: - vault filters: branches: only: master requires: - lint-and-test compressed-size: jobs: - compressed-size: context: - vault ================================================ FILE: .contentful/compressed-size.yml ================================================ version: 1 compressed-size: compression: 'gzip' pattern: './packages/*/{dist,build}/**/*.{js,css}' strip-hash: "\\b\\w{8}\\." collapse-unchanged: true minimum-change-threshold: 1024 ================================================ FILE: .contentful/vault-secrets.yaml ================================================ version: 1 services: github-action: policies: - dependabot circleci: policies: - github-comment - semantic-release - packages-read - nx-read-circleci ================================================ FILE: .editorconfig ================================================ #root = true [*] indent_style = space end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true max_line_length = 100 indent_size = 2 [*.md] trim_trailing_whitespace = false ================================================ FILE: .eslintignore ================================================ node_modules/ .coverage/ .cache/ dist/ .eslintrc.js ================================================ FILE: .eslintrc.js ================================================ module.exports = { env: { browser: true, es2021: true, }, extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:@typescript-eslint/recommended', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 'latest', sourceType: 'module', }, plugins: ['react', 'react-hooks', '@typescript-eslint', 'eslint-plugin-import-helpers'], rules: { 'react-hooks/exhaustive-deps': 'error', '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 'import-helpers/order-imports': [ 'warn', { newlinesBetween: 'always', groups: ['/^react/', 'module', ['parent', 'sibling', 'index']], alphabetize: { order: 'asc', ignoreCase: true }, }, ], 'no-restricted-imports': ['warn'], 'react/react-in-jsx-scope': 'off', }, settings: { react: { version: 'detect' }, }, }; ================================================ FILE: .github/dependabot.yml ================================================ version: 2 registries: npm-registry-registry-npmjs-org: type: npm-registry url: https://registry.npmjs.org token: '${{secrets.NPM_REGISTRY_REGISTRY_NPMJS_ORG_TOKEN}}' npm-github: type: npm-registry url: https://npm.pkg.github.com token: ${{secrets.NPM_REGISTRY_REGISTRY_GH_ORG_TOKEN}} updates: - package-ecosystem: npm directory: '/' schedule: interval: weekly time: '00:00' timezone: Etc/UCT open-pull-requests-limit: 10 target-branch: master labels: - dependencies - dependabot commit-message: prefix: chore registries: - npm-registry-registry-npmjs-org - npm-github reviewers: - 'contentful/team-tolkien' cooldown: default-days: 15 ================================================ FILE: .github/semantic.yml ================================================ titleAndCommits: true anyCommit: true allowMergeCommits: false types: - feat - fix - improvement - docs - style - refactor - perf - test - build - ci - chore - revert ================================================ FILE: .github/workflows/auto-approve.yml ================================================ name: 'dependabot approve-and-request-merge' on: pull_request_target jobs: worker: permissions: contents: write id-token: write pull-requests: write runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' steps: - uses: contentful/github-auto-merge@v2 with: VAULT_URL: ${{ secrets.VAULT_URL }} ================================================ FILE: .github/workflows/codeql.yml ================================================ --- name: "CodeQL Scan for GitHub Actions Workflow" on: push: branches: [master] paths: [".github/workflows/**"] pull_request: branches: [master] paths: [".github/workflows/**"] jobs: analyze: name: Analyze GitHub Actions workflows runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write steps: - uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: actions - name: Run CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: actions ================================================ FILE: .gitignore ================================================ # Logs **/logs **/*.log **/npm-debug.log* **/yarn-debug.log* **/yarn-error.log* # Coverage **/coverage # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) **/build/Release # Dependency directories **/node_modules/ **/jspm_packages/ # TypeScript v1 declaration files **/typings/ # Optional npm cache directory **/.npm # Optional eslint cache **/.eslintcache # Optional REPL history **/.node_repl_history # Output of 'npm pack' **/*.tgz # dotenv environment variables file **/.env **/dist **/.rpt2_cache .vscode/settings.json .idea # test reports reports # nx specific .nx/ # swc **/.swc ================================================ FILE: .husky/pre-commit ================================================ npx lint-staged ================================================ FILE: .npmrc ================================================ registry=https://registry.npmjs.org ignore-scripts=true shamefully-hoist=true ================================================ FILE: .nvmrc ================================================ v24 ================================================ FILE: .prettierignore ================================================ **/node_modules **/dist **/coverage ================================================ FILE: .swcrc ================================================ { "$schema": "https://swc.rs/schema.json", "jsc": { "parser": { "syntax": "typescript", "tsx": true }, "target": "es5", "loose": false, "minify": { "compress": false, "mangle": false } }, "module": { "type": "commonjs" }, "minify": false } ================================================ FILE: CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [15.15.1](https://github.com/contentful/rich-text/compare/v15.15.0...v15.15.1) (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # [15.15.0](https://github.com/contentful/rich-text/compare/v15.14.1...v15.15.0) (2022-11-29) ### Bug Fixes - don't run npm install on prePublish ([#417](https://github.com/contentful/rich-text/issues/417)) ([0af2378](https://github.com/contentful/rich-text/commit/0af237897ce093e294833e7e17c3b69e4634e09a)) ### Features - adding v1 marks to rich text types [TOL-786] ([#416](https://github.com/contentful/rich-text/issues/416)) ([8885de8](https://github.com/contentful/rich-text/commit/8885de8ebba7736de46b0b8892a8aa15bb8d7ba4)) ## [15.14.1](https://github.com/contentful/rich-text/compare/v15.14.0...v15.14.1) (2022-11-23) **Note:** Version bump only for package @contentful/rich-text # [15.14.0](https://github.com/contentful/rich-text/compare/v15.13.2...v15.14.0) (2022-11-14) ### Features - add super/sub script types ([#391](https://github.com/contentful/rich-text/issues/391)) ([2562f66](https://github.com/contentful/rich-text/commit/2562f66278f0eff4eeeb367025d4b465773893d1)) ## [15.13.2](https://github.com/contentful/rich-text/compare/v15.13.1...v15.13.2) (2022-09-07) ### Bug Fixes - add --no-package-lock ([#364](https://github.com/contentful/rich-text/issues/364)) ([41643c9](https://github.com/contentful/rich-text/commit/41643c9f61f525536941b54b64e944b70cb4a241)) - add inline sources to packages ([#357](https://github.com/contentful/rich-text/issues/357)) ([bca6abb](https://github.com/contentful/rich-text/commit/bca6abb4328414b5a7a87b9b57481e161d800dcf)) - add missing -- to circleci release commands ([#360](https://github.com/contentful/rich-text/issues/360)) ([233be08](https://github.com/contentful/rich-text/commit/233be0826cc7c76b799e70262b3281f7dbe4cd92)) - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) - circleci update resource class ([#359](https://github.com/contentful/rich-text/issues/359)) ([6bb3710](https://github.com/contentful/rich-text/commit/6bb3710623907a2601c218f3cea3a817790cbf59)) - revert change to monorepo tsconfig, apply to tsconfig in rich-text-types ([#358](https://github.com/contentful/rich-text/issues/358)) ([56126cc](https://github.com/contentful/rich-text/commit/56126cc9ed3704f21b89d2dbded160be0265f153)) - update Lerna, rollup in slatejs-adapter ([#366](https://github.com/contentful/rich-text/issues/366)) ([32448e3](https://github.com/contentful/rich-text/commit/32448e369ae2b76601dd81839e13c15432430d68)) ## [15.13.1](https://github.com/contentful/rich-text/compare/v15.13.0...v15.13.1) (2022-05-10) ### Bug Fixes - **markdown:** handle empty table cells ([#329](https://github.com/contentful/rich-text/issues/329)) ([55a0a16](https://github.com/contentful/rich-text/commit/55a0a16a12700fefbe7e9727a7172043fd126fc5)) # [15.13.0](https://github.com/contentful/rich-text/compare/v15.12.1...v15.13.0) (2022-05-06) ### Features - support converting tables from markdown to rich-text ([0abc0c6](https://github.com/contentful/rich-text/commit/0abc0c60b7e3e2683ebbb427b44847e6242f6e5e)) ## [15.12.1](https://github.com/contentful/rich-text/compare/v15.12.0...v15.12.1) (2022-04-21) ### Bug Fixes - upgrade react peerDependencies to support react^18.0.0 ([#323](https://github.com/contentful/rich-text/issues/323)) ([7a0bfdf](https://github.com/contentful/rich-text/commit/7a0bfdfb687cca608239e072cbc301ba1b1310d1)) # [15.12.0](https://github.com/contentful/rich-text/compare/v15.11.2...v15.12.0) (2022-03-25) ### Features - enforce minItems in table-related node types ([#314](https://github.com/contentful/rich-text/issues/314)) ([1125331](https://github.com/contentful/rich-text/commit/112533100f66ae01cd9944069dc62fc95f1737a5)) ## [15.11.2](https://github.com/contentful/rich-text/compare/v15.11.1...v15.11.2) (2022-02-07) ### Bug Fixes - set "typings" for rich-text-from-markdown ([1b3a15f](https://github.com/contentful/rich-text/commit/1b3a15f1cb15eacb6d1b15f2b79c5747e2d25618)) ## [15.11.1](https://github.com/contentful/rich-text/compare/v15.11.0...v15.11.1) (2022-01-04) ### Bug Fixes - **rich-text-types:** remove RT validation helpers ([#302](https://github.com/contentful/rich-text/issues/302)) ([fcd3a27](https://github.com/contentful/rich-text/commit/fcd3a277952f53eb3ae6ebb559ae6a02f5553c87)), closes [#295](https://github.com/contentful/rich-text/issues/295) [#274](https://github.com/contentful/rich-text/issues/274) # [15.11.0](https://github.com/contentful/rich-text/compare/v15.10.1...v15.11.0) (2021-12-27) ### Bug Fixes - **react-renderer:** wrap table rows in tbody ([#300](https://github.com/contentful/rich-text/issues/300)) ([e23d1f4](https://github.com/contentful/rich-text/commit/e23d1f4f5c63ce7ded84144b271f566327a144d1)) ### Features - **rich-text-types:** expose HEADINGS array ([#301](https://github.com/contentful/rich-text/issues/301)) ([758539d](https://github.com/contentful/rich-text/commit/758539d46f3db13c21ca2f6d74a389a6fef21803)) ## [15.10.1](https://github.com/contentful/rich-text/compare/v15.10.0...v15.10.1) (2021-12-21) ### Bug Fixes - remove support to convert tables from md to rich text ([a9d513c](https://github.com/contentful/rich-text/commit/a9d513c285e8cdedd384b89e195734dd3a8d2136)) # [15.10.0](https://github.com/contentful/rich-text/compare/v15.9.1...v15.10.0) (2021-12-15) ### Features - support custom error transformer ([#296](https://github.com/contentful/rich-text/issues/296)) ([9449b87](https://github.com/contentful/rich-text/commit/9449b87fc063a00f11cfe7b2bc0fdb4d91251c69)) ## [15.9.1](https://github.com/contentful/rich-text/compare/v15.9.0...v15.9.1) (2021-12-10) ### Bug Fixes - **rich-text-types:** resolve generated JSON schemas ([#294](https://github.com/contentful/rich-text/issues/294)) ([1e5b4c4](https://github.com/contentful/rich-text/commit/1e5b4c474e1e27e97df177748c0c8df365a2ab71)) # [15.9.0](https://github.com/contentful/rich-text/compare/v15.8.0...v15.9.0) (2021-12-09) ### Features - **rich-text-types:** expose RT validation helper ([#292](https://github.com/contentful/rich-text/issues/292)) ([fc5a7cc](https://github.com/contentful/rich-text/commit/fc5a7cc27244f293a9a50acd785f7edcdaaa96ea)) # [15.8.0](https://github.com/contentful/rich-text/compare/v15.7.0...v15.8.0) (2021-11-11) ### Features - add toContentfulDocument() and toSlatejsDocument() empty block node handling ([#287](https://github.com/contentful/rich-text/issues/287)) ([fa79626](https://github.com/contentful/rich-text/commit/fa79626e4020d9640a920ca5d0ccb654e89cfa90)) # [15.7.0](https://github.com/contentful/rich-text/compare/v15.6.2...v15.7.0) (2021-11-11) ### Features - **rich-text-types:** Add TEXT_CONTAINERS ([#286](https://github.com/contentful/rich-text/issues/286)) ([3356ea8](https://github.com/contentful/rich-text/commit/3356ea815a46901a6637f177b04bcf1926adc88d)) ## [15.6.2](https://github.com/contentful/rich-text/compare/v15.6.1...v15.6.2) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text ## [15.6.1](https://github.com/contentful/rich-text/compare/v15.6.0...v15.6.1) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text # [15.6.0](https://github.com/contentful/rich-text/compare/v15.5.1...v15.6.0) (2021-11-04) ### Features - add support to convert tables from md to rich text ([#284](https://github.com/contentful/rich-text/issues/284)) ([213a29c](https://github.com/contentful/rich-text/commit/213a29c78d48b3e63088999c4eed4891906d1719)) ## [15.5.1](https://github.com/contentful/rich-text/compare/v15.5.0...v15.5.1) (2021-10-25) ### Bug Fixes - npm 15.5.0 ([#280](https://github.com/contentful/rich-text/issues/280)) ([e7aeba4](https://github.com/contentful/rich-text/commit/e7aeba49a3074fc9eae6aee569db4e30d1acb8b8)) # [15.5.0](https://github.com/contentful/rich-text/compare/v15.4.0...v15.5.0) (2021-10-25) ### Features - add v1 node types constraints ([#279](https://github.com/contentful/rich-text/issues/279)) ([5026023](https://github.com/contentful/rich-text/commit/5026023610ec1439f24fd32df9977c2cd4c13e86)) # [15.4.0](https://github.com/contentful/rich-text/compare/v15.3.6...v15.4.0) (2021-09-16) ### Features - **html+react:** render Table header as
Test 6
Test 7
Test 9| Germany | |
Test 10
andTest 11
| Germany |
|  | Brazil |
| **[Test 12](https://example.com)** | USA |
## Tables with marks
| **Bold Header 1** | **Bold Header 2** |
| ----------------- | ----------------- |
| _Italic_ | `Code` |
## Tables without body
| abc | def |
| --- | --- |
## Table with empty cells
| | |
| ------ | ------ |
| Cell 1 | |
| | Cell 2 |
================================================
FILE: packages/rich-text-from-markdown/src/__test__/real-world.test.ts
================================================
import { BLOCKS, INLINES, MARKS } from '@contentful/rich-text-types';
import { readFileSync } from 'fs';
import path from 'path';
import { richTextFromMarkdown } from '..';
import { block, document, inline, mark, text } from './helpers';
describe('rich-text-from-markdown', () => {
it('should parse md with all formatting options', async () => {
const md = readFileSync(path.resolve(__dirname, './real-world.md'), 'utf8');
const result = await richTextFromMarkdown(md);
expect(result).toEqual(
document(
{},
block(BLOCKS.HEADING_1, {}, text('h1 Heading')),
block(BLOCKS.HEADING_2, {}, text('h2 Heading')),
block(BLOCKS.HEADING_3, {}, text('h3 Heading')),
block(BLOCKS.HEADING_4, {}, text('h4 Heading')),
block(BLOCKS.HEADING_5, {}, text('h5 Heading')),
block(BLOCKS.HEADING_6, {}, text('h6 Heading')),
// Paragraphs
block(BLOCKS.HEADING_2, {}, text('Paragraphs')),
block(
BLOCKS.PARAGRAPH,
{},
text(`This is a paragraph
with a new line.`),
),
block(BLOCKS.PARAGRAPH, {}, text('This is a new paragraph.')),
// TODO: , etc * but they are pretty much filtered out by markdownNodeTypes and nodeContainerTypes variables. * so we ended up receiving only `text` nodes. * We can't have table cells with text nodes directly, we must wrap text nodes inside paragraphs. */ return [ { nodeType: BLOCKS.TABLE_CELL, content, data: {}, } as Block, ]; }; const buildText = async ( node: MarkdownNode, fallback: FallbackResolver, appliedMarksTypes: string[], ): Promise> => { const nodeType = nodeTypeFor(node); const markType = markTypeFor(node); const marks = Array.from(appliedMarksTypes); if (markType) { marks.push(markType); } if (node.type !== 'text' && node.children) { return (await mdToRichTextNodes(node.children, fallback, marks)) as Array ; } if (node.value) { return [ { nodeType: nodeType, value: node.value, marks: marks.map((type) => ({ type })), data: {}, } as Text, ]; } }; const buildFallbackNode = async ( node: MarkdownNode, fallback: FallbackResolver, ): Promise => { const fallbackResult = await fallback(node); if (_.isArray(fallbackResult)) { return fallbackResult; } return [fallbackResult]; }; async function mdToRichTextNode( node: MarkdownNode, fallback: FallbackResolver, appliedMarksTypes: string[] = [], ): Promise { // By default
is parsed as html node, causing it to be stripped out. // We need to convert it manually in order to support it if (node.type === 'html' && /
/gi.test(node.value)) { node.value = '\n'; node.type = 'text'; } const nodeType = nodeTypeFor(node); if (isLink(node)) { return await buildHyperlink(node, fallback, appliedMarksTypes); } if (isTableCell(nodeType)) { return await buildTableCell(node, fallback, appliedMarksTypes); } if (isBlock(nodeType) || isInline(nodeType)) { return await buildGenericBlockOrInline(node, fallback, appliedMarksTypes); } if (isText(nodeType)) { return await buildText(node, fallback, appliedMarksTypes); } return await buildFallbackNode(node, fallback); } async function mdToRichTextNodes( nodes: MarkdownNode[], fallback: FallbackResolver, appliedMarksTypes: string[] = [], ): Promise{ if (!nodes) { return Promise.resolve([]); } const rtNodes = await Promise.all( nodes.map((node) => mdToRichTextNode(node, fallback, appliedMarksTypes)), ); return _.flatten(rtNodes).filter(Boolean); } const astToRichTextDocument = async ( tree: MarkdownTree, fallback: FallbackResolver, ): Promise => { const content = await mdToRichTextNodes(tree.children, fallback); return { nodeType: BLOCKS.DOCUMENT, data: {}, content: content as TopLevelBlock[], }; }; function expandParagraphWithInlineImages(node: MarkdownNode): MarkdownNode[] { if (node.type !== 'paragraph') { return [node]; } const imageNodeIndices = []; for (let i = 0; i < node.children.length; i++) { if (node.children[i].type === 'image') { imageNodeIndices.push(i); } } if (imageNodeIndices.length === 0) { // If no images in children, return. return [node]; } const allNodes: MarkdownNode[] = []; let lastIndex = -1; for (let j = 0; j < imageNodeIndices.length; j++) { const index = imageNodeIndices[j]; // before if (index !== 0) { const nodesBefore: MarkdownNode[] = node.children.slice(lastIndex + 1, index); if (nodesBefore.length > 0) { allNodes.push({ ...node, children: nodesBefore, }); } } // image const imageNode = node.children[index]; allNodes.push(imageNode); // till end let nodesAfter: MarkdownNode[] = []; const rangeEnd = j + 1 < imageNodeIndices.length ? imageNodeIndices[j + 1] : node.children.length; if (index + 1 < rangeEnd && index === imageNodeIndices.slice(-1)[0]) { nodesAfter = node.children.slice(index + 1, rangeEnd); if (nodesAfter.length > 0) { allNodes.push({ ...node, children: nodesAfter, }); } } lastIndex = index; } return allNodes; } // Inline markdown images come in as nested within a MarkdownNode paragraph // so we must hoist them out before transforming to rich text. function prepareMdAST(ast: MarkdownTree): MarkdownNode { function prepareASTNodeChildren(node: MarkdownNode): MarkdownNode { if (!node.children) { return node; } const children = _.flatMap(node.children, (n) => expandParagraphWithInlineImages(n)).map((n) => prepareASTNodeChildren(n), ); return { ...node, children }; } return prepareASTNodeChildren({ depth: '0', type: 'root', value: '', ordered: true, children: ast.children, }); } // COMPAT: can resolve with either Node or an array of Nodes for back compatibility. export type FallbackResolver = (mdNode: MarkdownNode) => Promise ; export async function richTextFromMarkdown( md: string, fallback: FallbackResolver = () => Promise.resolve(null), ): Promise { const processor = unified().use(markdown).use(gfm); const tree = processor.parse(md); // @ts-expect-error children is missing in the return type of processor.parse const ast = prepareMdAST(tree); return await astToRichTextDocument(ast, fallback); } ================================================ FILE: packages/rich-text-from-markdown/src/types/index.ts ================================================ export interface MarkdownNode extends MarkdownTree { depth: string; type: string; ordered: boolean; value: string; } export interface MarkdownTree { children: MarkdownNode[]; } export interface MarkdownLinkNode extends MarkdownNode { url: string; } ================================================ FILE: packages/rich-text-from-markdown/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "declarationDir": "dist/types", "outDir": "dist/lib", "strict": false, "typeRoots": ["../../node_modules/@types", "node_modules/@types", "src/types"] }, "include": ["src"] } ================================================ FILE: packages/rich-text-html-renderer/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [17.2.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.2.1...@contentful/rich-text-html-renderer@17.2.2) (2026-04-09) ### Bug Fixes - **rich-text-types:** improve esm compability [ZEND-7778] ([#1073](https://github.com/contentful/rich-text/issues/1073)) ([204aaec](https://github.com/contentful/rich-text/commit/204aaecc3893633c081986f44896e14272fa376a)) ## [17.2.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.2.0...@contentful/rich-text-html-renderer@17.2.1) (2026-04-08) ### Bug Fixes - **rich-text-html-renderer:** parse and render asset [] ([#854](https://github.com/contentful/rich-text/issues/854)) ([3ca028d](https://github.com/contentful/rich-text/commit/3ca028d828979a22cb208607371ffec8990d01b7)), closes [#853](https://github.com/contentful/rich-text/issues/853) # [17.2.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.1.6...@contentful/rich-text-html-renderer@17.2.0) (2026-04-08) ### Features - add esm support [ZEND-7778] ([#1069](https://github.com/contentful/rich-text/issues/1069)) ([8c320bd](https://github.com/contentful/rich-text/commit/8c320bde7fa313572bdad84b91a6fd12224afc3a)), closes [#1068](https://github.com/contentful/rich-text/issues/1068) ## [17.1.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.1.5...@contentful/rich-text-html-renderer@17.1.6) (2025-11-04) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [17.1.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.1.4...@contentful/rich-text-html-renderer@17.1.5) (2025-09-23) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [17.1.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.1.3...@contentful/rich-text-html-renderer@17.1.4) (2025-09-23) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [17.1.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.1.2...@contentful/rich-text-html-renderer@17.1.3) (2025-09-10) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [17.1.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.1.1...@contentful/rich-text-html-renderer@17.1.2) (2025-09-09) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [17.1.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.1.0...@contentful/rich-text-html-renderer@17.1.1) (2025-09-04) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [17.1.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.0.1...@contentful/rich-text-html-renderer@17.1.0) (2025-07-15) ### Features - add an option to strip empty trailing paragraphs [TOL-3193] ([#892](https://github.com/contentful/rich-text/issues/892)) ([9beff0e](https://github.com/contentful/rich-text/commit/9beff0e4cba3e79dc68e6a0725e843c5d642eb87)) ## [17.0.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@17.0.0...@contentful/rich-text-html-renderer@17.0.1) (2025-06-16) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [17.0.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.10...@contentful/rich-text-html-renderer@17.0.0) (2024-10-29) ### Features - bring rich text validator [TOL-2426] ([#694](https://github.com/contentful/rich-text/issues/694)) ([30893a6](https://github.com/contentful/rich-text/commit/30893a68b171167502135b48258ba4b93c8375c7)) ### BREAKING CHANGES - removed getSchemaWithNodeType in favor of validateRichTextDocument ## [16.6.10](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.9...@contentful/rich-text-html-renderer@16.6.10) (2024-09-09) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.6.9](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.8...@contentful/rich-text-html-renderer@16.6.9) (2024-08-26) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.6.8](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.7...@contentful/rich-text-html-renderer@16.6.8) (2024-07-29) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.6.7](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.6...@contentful/rich-text-html-renderer@16.6.7) (2024-07-24) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.6.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.5...@contentful/rich-text-html-renderer@16.6.6) (2024-07-17) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.6.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.4...@contentful/rich-text-html-renderer@16.6.5) (2024-07-17) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.6.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.3...@contentful/rich-text-html-renderer@16.6.4) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.6.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.2...@contentful/rich-text-html-renderer@16.6.3) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.6.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.1...@contentful/rich-text-html-renderer@16.6.2) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.6.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.6.0...@contentful/rich-text-html-renderer@16.6.1) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [16.6.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.5.5...@contentful/rich-text-html-renderer@16.6.0) (2024-06-25) ### Features - switch from tslint to eslint [TOL-2218][tol-2203] ([#594](https://github.com/contentful/rich-text/issues/594)) ([c077b5a](https://github.com/contentful/rich-text/commit/c077b5af58f94c8dc6af4715d4b82c2771d643c5)) ## [16.5.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.5.4...@contentful/rich-text-html-renderer@16.5.5) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.5.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.5.3...@contentful/rich-text-html-renderer@16.5.4) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.5.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.5.2...@contentful/rich-text-html-renderer@16.5.3) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.5.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.5.1...@contentful/rich-text-html-renderer@16.5.2) (2024-05-28) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.5.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.5.0...@contentful/rich-text-html-renderer@16.5.1) (2024-05-27) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [16.5.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.4.0...@contentful/rich-text-html-renderer@16.5.0) (2024-05-24) ### Features - add strikethrough support ([#562](https://github.com/contentful/rich-text/issues/562)) ([b87d0c3](https://github.com/contentful/rich-text/commit/b87d0c31bccb4012745c0479b2b5c92fc28c1e91)) # [16.4.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.3.5...@contentful/rich-text-html-renderer@16.4.0) (2024-05-22) ### Features - upgrading rollup to latest version [TOL-2097] ([#559](https://github.com/contentful/rich-text/issues/559)) ([f14d197](https://github.com/contentful/rich-text/commit/f14d1974590d58f92bbf4cb56644095dba929ad9)) ## [16.3.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.3.4...@contentful/rich-text-html-renderer@16.3.5) (2024-03-04) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.3.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.3.3...@contentful/rich-text-html-renderer@16.3.4) (2024-01-30) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.3.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.3.2...@contentful/rich-text-html-renderer@16.3.3) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.3.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.3.1...@contentful/rich-text-html-renderer@16.3.2) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.3.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.3.0...@contentful/rich-text-html-renderer@16.3.1) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [16.3.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.2.0...@contentful/rich-text-html-renderer@16.3.0) (2023-10-24) ### Features - add default renderers for new resource nodes ([#504](https://github.com/contentful/rich-text/issues/504)) ([f748e6b](https://github.com/contentful/rich-text/commit/f748e6b0f0e22ce3809ca8881438b84b1baafb8f)) # [16.2.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.1.2...@contentful/rich-text-html-renderer@16.2.0) (2023-10-02) ### Features - preserve formatting linebreak whitespace ([#497](https://github.com/contentful/rich-text/issues/497)) ([e62ab92](https://github.com/contentful/rich-text/commit/e62ab92f6320d970f921e751d1753cd290224224)) ## [16.1.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.1.1...@contentful/rich-text-html-renderer@16.1.2) (2023-09-12) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.1.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.1.0...@contentful/rich-text-html-renderer@16.1.1) (2023-08-04) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [16.1.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.0.5...@contentful/rich-text-html-renderer@16.1.0) (2023-06-09) ### Features - add default renderer for the node ([#472](https://github.com/contentful/rich-text/issues/472)) ([5a53caf](https://github.com/contentful/rich-text/commit/5a53cafc58aa8d009d2aa2d6cb4fa2d6cee6b19d)) ## [16.0.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.0.4...@contentful/rich-text-html-renderer@16.0.5) (2023-05-26) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.0.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.0.3...@contentful/rich-text-html-renderer@16.0.4) (2023-05-04) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.0.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.0.2...@contentful/rich-text-html-renderer@16.0.3) (2023-03-14) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.0.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.0.1...@contentful/rich-text-html-renderer@16.0.2) (2022-12-01) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [16.0.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-html-renderer@16.0.0...@contentful/rich-text-html-renderer@16.0.1) (2022-12-01) **Note:** Version bump only for package @contentful/rich-text-html-renderer # 16.0.0 (2022-12-01) ## 15.15.1 (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # 15.15.0 (2022-11-29) ## 15.14.1 (2022-11-23) # 15.14.0 (2022-11-14) ### Features - add super/sub script types ([#391](https://github.com/contentful/rich-text/issues/391)) ([2562f66](https://github.com/contentful/rich-text/commit/2562f66278f0eff4eeeb367025d4b465773893d1)) ## 15.13.2 (2022-09-07) ### Bug Fixes - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) - update Lerna, rollup in slatejs-adapter ([#366](https://github.com/contentful/rich-text/issues/366)) ([32448e3](https://github.com/contentful/rich-text/commit/32448e369ae2b76601dd81839e13c15432430d68)) ## 15.13.1 (2022-05-10) ## 15.12.1 (2022-04-21) # 15.12.0 (2022-03-25) ## 15.11.1 (2022-01-04) # 15.11.0 (2021-12-27) ## 15.10.1 (2021-12-21) # 15.10.0 (2021-12-15) ### Features - support custom error transformer ([#296](https://github.com/contentful/rich-text/issues/296)) ([9449b87](https://github.com/contentful/rich-text/commit/9449b87fc063a00f11cfe7b2bc0fdb4d91251c69)) ## 15.9.1 (2021-12-10) ### Bug Fixes - **rich-text-types:** resolve generated JSON schemas ([#294](https://github.com/contentful/rich-text/issues/294)) ([1e5b4c4](https://github.com/contentful/rich-text/commit/1e5b4c474e1e27e97df177748c0c8df365a2ab71)) # 15.9.0 (2021-12-09) # 15.8.0 (2021-11-11) ### Features - add toContentfulDocument() and toSlatejsDocument() empty block node handling ([#287](https://github.com/contentful/rich-text/issues/287)) ([fa79626](https://github.com/contentful/rich-text/commit/fa79626e4020d9640a920ca5d0ccb654e89cfa90)) # 15.7.0 (2021-11-11) ## 15.6.2 (2021-11-05) ## 15.6.1 (2021-11-05) # 15.6.0 (2021-11-04) ## 15.5.1 (2021-10-25) # 15.5.0 (2021-10-25) ### Features - add v1 node types constraints ([#279](https://github.com/contentful/rich-text/issues/279)) ([5026023](https://github.com/contentful/rich-text/commit/5026023610ec1439f24fd32df9977c2cd4c13e86)) # 15.4.0 (2021-09-16) ### Features - **html+react:** render Table header as ([#269](https://github.com/contentful/rich-text/issues/269)) ([0f82905](https://github.com/contentful/rich-text/commit/0f829059be6d91e042dfc71698009177ae4ab78d)) ## 15.3.6 (2021-09-15) ## 15.3.5 (2021-09-13) ## 15.3.4 (2021-09-09) ### Bug Fixes - replace import to fix typings for html-renderer ([#265](https://github.com/contentful/rich-text/issues/265)) ([40c2670](https://github.com/contentful/rich-text/commit/40c267069b18454517b0f4283a7e155cffa410b6)) ## 15.3.3 (2021-09-07) ## 15.3.2 (2021-09-07) ## 15.3.1 (2021-09-07) # 15.3.0 (2021-09-06) # 15.2.0 (2021-08-16) # 15.1.0 (2021-08-02) ### Bug Fixes - 🐛 html encode default inlined CF entry/asset links ([41396eb](https://github.com/contentful/rich-text/commit/41396eb800f6d5c65c634c4394a6c60cf3425255)) - 🐛 prevent html injection via `data.uri` link rendering ([ecae89a](https://github.com/contentful/rich-text/commit/ecae89ad0d25175f342833ca989928670c24b8fd)) ### Features - 🎸 add RT html renderer tables support ([ce81375](https://github.com/contentful/rich-text/commit/ce8137577b269c62727dc64b7d47b4951597dbd6)) # 15.0.0 (2021-06-15) ## 14.1.2 (2020-11-02) ## 14.0.1 (2020-01-30) # 14.0.0 (2020-01-28) # 13.4.0 (2019-08-01) # 13.1.0 (2019-03-04) # 13.0.0 (2019-01-22) ## 12.2.1 (2019-01-22) ### Bug Fixes - gracefully handle empty rich text documents ([14870de](https://github.com/contentful/rich-text/commit/14870ded471f69cee84a60739bee06873ed53af9)) ## 12.1.2 (2018-12-14) ### Features - add html escaping ([4b55331](https://github.com/contentful/rich-text/commit/4b55331e86bc62787420f8081228293f1a22e1b7)) # 12.1.0 (2018-12-12) ## 12.0.3 (2018-12-05) ### Bug Fixes - **readme:** mark types in readme examples ([d997b56](https://github.com/contentful/rich-text/commit/d997b56f2b8c32b2e5b478ab5444757203e2c703)) ## 12.0.2 (2018-12-04) ### Bug Fixes - 🐛 update path to typings ([ce5544f](https://github.com/contentful/rich-text/commit/ce5544f58712dc6a18aadda523d0c0357a66c8a5)) ## 12.0.1 (2018-12-04) # 12.0.0 (2018-11-29) # 11.0.0 (2018-11-27) # 10.3.0 (2018-11-26) # 10.2.0 (2018-11-19) ### Features - 🎸 Add rich-text-references ([363b4e5](https://github.com/contentful/rich-text/commit/363b4e509e94af0932fd7cece8e56beafe8d67d2)) # 10.1.0 (2018-11-16) ## 10.0.1 (2018-11-08) # 10.0.0 (2018-11-02) ### Features - 🎸 removes nodeClass from Node interface ([09b8162](https://github.com/contentful/rich-text/commit/09b8162bcba65bc13afa14b2b5ff046c9fed7b3b)) ### BREAKING CHANGES - Removes accidentally added nodeClass ## 9.0.2 (2018-10-31) # 9.0.0 (2018-10-30) ### Features - 🎸 Explicitly declare nodeClass and nodeType in core types ([0749c61](https://github.com/contentful/rich-text/commit/0749c6199bb2681509608539c76bd8149cade564)) ### BREAKING CHANGES - Potentially breaks TypeScript libraries pulling this in as a dependency if they are not explicitly stating nodeClass and nodeType. ## 8.0.3 (2018-10-30) ## 8.0.2 (2018-10-29) ## 8.0.1 (2018-10-26) ### Bug Fixes - 🐛 Revert mock hypenation commits ([bde9432](https://github.com/contentful/rich-text/commit/bde94323fcc02bf5ab3feeef46a9d8fc8b08d59b)) - sync HTML renderer ([769bd95](https://github.com/contentful/rich-text/commit/769bd95add1aecad0e11fab377f99c5cdad99072)) ### Features - 🎸 take all packages out of "demo mode" ([815f18b](https://github.com/contentful/rich-text/commit/815f18be6a914e7e4782790ee46053689712494b)) ### BREAKING CHANGES - renames all packages # 6.0.0 (2018-10-24) ## [15.15.1](https://github.com/contentful/rich-text/compare/v15.15.0...v15.15.1) (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # [15.15.0](https://github.com/contentful/rich-text/compare/v15.14.1...v15.15.0) (2022-11-29) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.14.1](https://github.com/contentful/rich-text/compare/v15.14.0...v15.14.1) (2022-11-23) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [15.14.0](https://github.com/contentful/rich-text/compare/v15.13.2...v15.14.0) (2022-11-14) ### Features - add super/sub script types ([#391](https://github.com/contentful/rich-text/issues/391)) ([2562f66](https://github.com/contentful/rich-text/commit/2562f66278f0eff4eeeb367025d4b465773893d1)) ## [15.13.2](https://github.com/contentful/rich-text/compare/v15.13.1...v15.13.2) (2022-09-07) ### Bug Fixes - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) - update Lerna, rollup in slatejs-adapter ([#366](https://github.com/contentful/rich-text/issues/366)) ([32448e3](https://github.com/contentful/rich-text/commit/32448e369ae2b76601dd81839e13c15432430d68)) ## [15.13.1](https://github.com/contentful/rich-text/compare/v15.13.0...v15.13.1) (2022-05-10) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.12.1](https://github.com/contentful/rich-text/compare/v15.12.0...v15.12.1) (2022-04-21) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [15.12.0](https://github.com/contentful/rich-text/compare/v15.11.2...v15.12.0) (2022-03-25) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.11.1](https://github.com/contentful/rich-text/compare/v15.11.0...v15.11.1) (2022-01-04) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [15.11.0](https://github.com/contentful/rich-text/compare/v15.10.1...v15.11.0) (2021-12-27) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.10.1](https://github.com/contentful/rich-text/compare/v15.10.0...v15.10.1) (2021-12-21) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [15.10.0](https://github.com/contentful/rich-text/compare/v15.9.1...v15.10.0) (2021-12-15) ### Features - support custom error transformer ([#296](https://github.com/contentful/rich-text/issues/296)) ([9449b87](https://github.com/contentful/rich-text/commit/9449b87fc063a00f11cfe7b2bc0fdb4d91251c69)) ## [15.9.1](https://github.com/contentful/rich-text/compare/v15.9.0...v15.9.1) (2021-12-10) ### Bug Fixes - **rich-text-types:** resolve generated JSON schemas ([#294](https://github.com/contentful/rich-text/issues/294)) ([1e5b4c4](https://github.com/contentful/rich-text/commit/1e5b4c474e1e27e97df177748c0c8df365a2ab71)) # [15.9.0](https://github.com/contentful/rich-text/compare/v15.8.0...v15.9.0) (2021-12-09) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [15.8.0](https://github.com/contentful/rich-text/compare/v15.7.0...v15.8.0) (2021-11-11) ### Features - add toContentfulDocument() and toSlatejsDocument() empty block node handling ([#287](https://github.com/contentful/rich-text/issues/287)) ([fa79626](https://github.com/contentful/rich-text/commit/fa79626e4020d9640a920ca5d0ccb654e89cfa90)) # [15.7.0](https://github.com/contentful/rich-text/compare/v15.6.2...v15.7.0) (2021-11-11) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.6.2](https://github.com/contentful/rich-text/compare/v15.6.1...v15.6.2) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.6.1](https://github.com/contentful/rich-text/compare/v15.6.0...v15.6.1) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [15.6.0](https://github.com/contentful/rich-text/compare/v15.5.1...v15.6.0) (2021-11-04) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.5.1](https://github.com/contentful/rich-text/compare/v15.5.0...v15.5.1) (2021-10-25) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [15.5.0](https://github.com/contentful/rich-text/compare/v15.4.0...v15.5.0) (2021-10-25) ### Features - add v1 node types constraints ([#279](https://github.com/contentful/rich-text/issues/279)) ([5026023](https://github.com/contentful/rich-text/commit/5026023610ec1439f24fd32df9977c2cd4c13e86)) # [15.4.0](https://github.com/contentful/rich-text/compare/v15.3.6...v15.4.0) (2021-09-16) ### Features - **html+react:** render Table header as ([#269](https://github.com/contentful/rich-text/issues/269)) ([0f82905](https://github.com/contentful/rich-text/commit/0f829059be6d91e042dfc71698009177ae4ab78d)) ## [15.3.6](https://github.com/contentful/rich-text/compare/v15.3.5...v15.3.6) (2021-09-15) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.3.5](https://github.com/contentful/rich-text/compare/v15.3.4...v15.3.5) (2021-09-13) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.3.4](https://github.com/contentful/rich-text/compare/v15.3.3...v15.3.4) (2021-09-09) ### Bug Fixes - replace import to fix typings for html-renderer ([#265](https://github.com/contentful/rich-text/issues/265)) ([40c2670](https://github.com/contentful/rich-text/commit/40c267069b18454517b0f4283a7e155cffa410b6)) ## [15.3.3](https://github.com/contentful/rich-text/compare/v15.3.2...v15.3.3) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.3.2](https://github.com/contentful/rich-text/compare/v15.3.1...v15.3.2) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-html-renderer ## [15.3.1](https://github.com/contentful/rich-text/compare/v15.3.0...v15.3.1) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [15.3.0](https://github.com/contentful/rich-text/compare/v15.2.0...v15.3.0) (2021-09-06) **Note:** Version bump only for package @contentful/rich-text-html-renderer # [15.2.0](https://github.com/contentful/rich-text/compare/v15.1.0...v15.2.0) (2021-08-16) ### Bug Fixes - 🐛 html encode default inlined CF entry/asset links ([41396eb](https://github.com/contentful/rich-text/commit/41396eb800f6d5c65c634c4394a6c60cf3425255)) - 🐛 prevent html injection via `data.uri` link rendering ([ecae89a](https://github.com/contentful/rich-text/commit/ecae89ad0d25175f342833ca989928670c24b8fd)) # [15.1.0](https://github.com/contentful/rich-text/compare/v15.0.0...v15.1.0) (2021-08-02) ### Features - 🎸 add RT html renderer tables support ([ce81375](https://github.com/contentful/rich-text/commit/ce8137577b269c62727dc64b7d47b4951597dbd6)) # [15.0.0](https://github.com/contentful/rich-text/compare/v14.2.0...v15.0.0) (2021-06-15) ## [14.1.2](https://github.com/contentful/rich-text/compare/v14.0.1...v14.1.2) (2020-11-02) ## [14.0.1](https://github.com/contentful/rich-text/compare/v14.0.0...v14.0.1) (2020-01-30) # [14.0.0](https://github.com/contentful/rich-text/compare/v13.4.0...v14.0.0) (2020-01-28) # [13.4.0](https://github.com/contentful/rich-text/compare/v13.3.0...v13.4.0) (2019-08-01) # [13.1.0](https://github.com/contentful/rich-text/compare/v13.0.1...v13.1.0) (2019-03-04) # [13.0.0](https://github.com/contentful/rich-text/compare/v12.2.1...v13.0.0) (2019-01-22) ## [12.2.1](https://github.com/contentful/rich-text/compare/v12.2.0...v12.2.1) (2019-01-22) ### Bug Fixes - gracefully handle empty rich text documents ([14870de](https://github.com/contentful/rich-text/commit/14870ded471f69cee84a60739bee06873ed53af9)) ## [12.1.2](https://github.com/contentful/rich-text/compare/v12.1.1...v12.1.2) (2018-12-14) ### Features - add html escaping ([4b55331](https://github.com/contentful/rich-text/commit/4b55331e86bc62787420f8081228293f1a22e1b7)) # [12.1.0](https://github.com/contentful/rich-text/compare/v12.0.4...v12.1.0) (2018-12-12) ## [12.0.3](https://github.com/contentful/rich-text/compare/v12.0.2...v12.0.3) (2018-12-05) ### Bug Fixes - **readme:** mark types in readme examples ([d997b56](https://github.com/contentful/rich-text/commit/d997b56f2b8c32b2e5b478ab5444757203e2c703)) ## [12.0.2](https://github.com/contentful/rich-text/compare/v12.0.1...v12.0.2) (2018-12-04) ### Bug Fixes - 🐛 update path to typings ([ce5544f](https://github.com/contentful/rich-text/commit/ce5544f58712dc6a18aadda523d0c0357a66c8a5)) ## [12.0.1](https://github.com/contentful/rich-text/compare/v12.0.0...v12.0.1) (2018-12-04) # [12.0.0](https://github.com/contentful/rich-text/compare/v11.0.0...v12.0.0) (2018-11-29) # [11.0.0](https://github.com/contentful/rich-text/compare/v10.3.0...v11.0.0) (2018-11-27) # [10.3.0](https://github.com/contentful/rich-text/compare/v10.2.0...v10.3.0) (2018-11-26) # [10.2.0](https://github.com/contentful/rich-text/compare/v10.1.0...v10.2.0) (2018-11-19) ### Features - 🎸 Add rich-text-references ([363b4e5](https://github.com/contentful/rich-text/commit/363b4e509e94af0932fd7cece8e56beafe8d67d2)) # [10.1.0](https://github.com/contentful/rich-text/compare/v10.0.5...v10.1.0) (2018-11-16) ## [10.0.1](https://github.com/contentful/rich-text/compare/v10.0.0...v10.0.1) (2018-11-08) # [10.0.0](https://github.com/contentful/rich-text/compare/v9.0.2...v10.0.0) (2018-11-02) ### Features - 🎸 removes nodeClass from Node interface ([09b8162](https://github.com/contentful/rich-text/commit/09b8162bcba65bc13afa14b2b5ff046c9fed7b3b)) ### BREAKING CHANGES - Removes accidentally added nodeClass ## [9.0.2](https://github.com/contentful/rich-text/compare/v9.0.1...v9.0.2) (2018-10-31) # [9.0.0](https://github.com/contentful/rich-text/compare/v8.0.3...v9.0.0) (2018-10-30) ### Features - 🎸 Explicitly declare nodeClass and nodeType in core types ([0749c61](https://github.com/contentful/rich-text/commit/0749c6199bb2681509608539c76bd8149cade564)) ### BREAKING CHANGES - Potentially breaks TypeScript libraries pulling this in as a dependency if they are not explicitly stating nodeClass and nodeType. ## [8.0.3](https://github.com/contentful/rich-text/compare/v8.0.2...v8.0.3) (2018-10-30) ## [8.0.2](https://github.com/contentful/rich-text/compare/v8.0.1...v8.0.2) (2018-10-29) ## [8.0.1](https://github.com/contentful/rich-text/compare/v8.0.0...v8.0.1) (2018-10-26) ### Bug Fixes - 🐛 Revert mock hypenation commits ([bde9432](https://github.com/contentful/rich-text/commit/bde94323fcc02bf5ab3feeef46a9d8fc8b08d59b)) - sync HTML renderer ([769bd95](https://github.com/contentful/rich-text/commit/769bd95add1aecad0e11fab377f99c5cdad99072)) ### Features - 🎸 take all packages out of "demo mode" ([815f18b](https://github.com/contentful/rich-text/commit/815f18be6a914e7e4782790ee46053689712494b)) ### BREAKING CHANGES - renames all packages # 6.0.0 (2018-10-24) ================================================ FILE: packages/rich-text-html-renderer/README.md ================================================ # rich-text-html-renderer HTML renderer for the Contentful rich text field type. ## Installation Using [npm](http://npmjs.org/): ```sh npm install @contentful/rich-text-html-renderer ``` Using [yarn](https://yarnpkg.com/): ```sh yarn add @contentful/rich-text-html-renderer ``` ## Usage ```javascript import { documentToHtmlString } from '@contentful/rich-text-html-renderer'; const document = { nodeType: 'document', content: [ { nodeType: 'paragraph', content: [ { nodeType: 'text', value: 'Hello world!', marks: [], }, ], }, ], }; documentToHtmlString(document); // -> Hello world!
``` ```javascript import { documentToHtmlString } from '@contentful/rich-text-html-renderer'; const document = { nodeType: 'document', content: [ { nodeType: 'paragraph', content: [ { nodeType: 'text', value: 'Hello', marks: [{ type: 'bold' }], }, { nodeType: 'text', value: ' world!', marks: [{ type: 'italic' }], }, ], }, ], }; documentToHtmlString(document); // ->Hello world!
``` You can also pass custom renderers for both marks and nodes as an optional parameter like so: ```javascript import { BLOCKS, MARKS } from '@contentful/rich-text-types'; import { documentToHtmlString } from '@contentful/rich-text-html-renderer'; const document = { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data:{}, content: [ { nodeType: 'text', value: 'Hello', marks: [{ type: 'bold' }], data: {} }, { nodeType: 'text', value: ' world!', marks: [{ type: 'italic' }] data: {} }, ], }, ] }; const options = { renderMark: { [MARKS.BOLD]: text => `${text} ` }, renderNode: { [BLOCKS.PARAGRAPH]: (node, next) => ` ${next(node.content)} ` } } documentToHtmlString(document, options); // ->``` Last, but not least, you can pass a custom rendering component for an embedded entry: ```javascript import { BLOCKS } from '@contentful/rich-text-types'; import { documentToHtmlString } from '@contentful/rich-text-html-renderer'; const document = { nodeType: 'document', data: {}, content: [ { nodeType: 'embedded-entry-block', data: { target: (...)Link<'Entry'>(...); }, }, ] }; const options = { renderNode: { [BLOCKS.EMBEDDED_ENTRY]: (node) => ` Hello world!${customComponentRenderer(node)} ` } } documentToHtmlString(document, options); // ->(...)Link<'Entry'>(...) ``` The `renderNode` keys should be one of the following `BLOCKS` and `INLINES` properties as defined in [`@contentful/rich-text-types`](https://www.npmjs.com/package/@contentful/rich-text-types): - `BLOCKS` - `DOCUMENT` - `PARAGRAPH` - `HEADING_1` - `HEADING_2` - `HEADING_3` - `HEADING_4` - `HEADING_5` - `HEADING_6` - `UL_LIST` - `OL_LIST` - `LIST_ITEM` - `QUOTE` - `HR` - `EMBEDDED_ENTRY` - `EMBEDDED_ASSET` - `EMBEDDED_RESOURCE` - `INLINES` - `EMBEDDED_ENTRY` (this is different from the `BLOCKS.EMBEDDED_ENTRY`) - `EMBEDDED_RESOURCE` - `HYPERLINK` - `ENTRY_HYPERLINK` - `ASSET_HYPERLINK` - `RESOURCE_HYPERLINK` The `renderMark` keys should be one of the following `MARKS` properties as defined in [`@contentful/rich-text-types`](https://www.npmjs.com/package/@contentful/rich-text-types): - `BOLD` - `ITALIC` - `UNDERLINE` - `CODE` - `SUPERSCRIPT` - `SUBSCRIPT` - `STRIKETHROUGH` #### Preserving Whitespace In your HTML rendering options, you can utilize the `preserveWhitespace` boolean flag. When set to `true`, this flag ensures that spaces and line breaks in the Contentful rich text content are preserved in the rendered HTML. Specifically, it replaces consecutive spaces with ` ` entities and retains line breaks using `
` tags. This capability is particularly beneficial for content that has specific formatting requirements involving spaces and line breaks. ```javascript import { documentToHtmlString } from '@contentful/rich-text-html-renderer'; const document = { nodeType: 'document', content: [ { nodeType: 'paragraph', content: [ { nodeType: 'text', value: 'Hello world!', marks: [], }, ], }, ], }; const options = { preserveWhitespace: true, }; documentToHtmlString(document, options); // ->Hello world!
``` With this configuration, the HTML output retains the spaces found between "Hello" and "world!". ================================================ FILE: packages/rich-text-html-renderer/jest.config.js ================================================ const getBaseConfig = require('../../baseJestConfig'); const package = require('./package.json'); const packageName = package.name.split('@contentful/')[1]; module.exports = { ...getBaseConfig(packageName), }; ================================================ FILE: packages/rich-text-html-renderer/package.json ================================================ { "name": "@contentful/rich-text-html-renderer", "version": "17.2.2", "main": "dist/rich-text-html-renderer.es5.js", "module": "dist/rich-text-html-renderer.esm.js", "typings": "dist/types/index.d.ts", "exports": { ".": { "types": "./dist/types/index.d.ts", "import": "./dist/rich-text-html-renderer.esm.js", "require": "./dist/rich-text-html-renderer.es5.js", "default": "./dist/rich-text-html-renderer.es5.js" }, "./package.json": "./package.json" }, "files": [ "dist" ], "repository": { "type": "git", "url": "https://github.com/contentful/rich-text.git" }, "license": "MIT", "engines": { "node": ">=20.0.0" }, "publishConfig": { "access": "public", "registry": "https://npm.pkg.github.com/" }, "scripts": { "prebuild": "rimraf dist", "build": "tsc --module commonjs && rollup -c --bundleConfigAsCjs rollup.config.js", "start": "tsc && rollup -c --bundleConfigAsCjs rollup.config.js -w", "test": "jest" }, "dependencies": { "@contentful/rich-text-types": "^17.2.7" }, "devDependencies": { "ts-jest": "^29.1.2" } } ================================================ FILE: packages/rich-text-html-renderer/rollup.config.js ================================================ import config from '../../rollup.config'; import { main as outputFile, dependencies } from './package.json'; export default config(outputFile, { external: Object.keys(dependencies), }); ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/embedded-entry.ts ================================================ import { BLOCKS, Document } from '@contentful/rich-text-types'; export default function (entry: Record) { return { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_ENTRY, content: [], data: { target: entry, }, }, ], } as Document; } ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/embedded-resource.ts ================================================ import { Document, BLOCKS, ResourceLink } from '@contentful/rich-text-types'; export default function (resourceLink: ResourceLink) { return { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, content: [], data: { target: resourceLink, }, }, ], } as Document; } ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/heading.ts ================================================ import { Document, BLOCKS } from '@contentful/rich-text-types'; export default function (heading: string) { return { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: heading, data: {}, content: [ { nodeType: 'text', value: 'hello world', marks: [], data: {}, }, ], }, ], } as Document; } ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/hr.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { content: [ { content: [ { marks: [], nodeType: 'text', value: 'hello world', data: {}, }, ], data: {}, nodeType: 'paragraph', }, { content: [ { marks: [], nodeType: 'text', value: '', data: {}, }, ], data: {}, nodeType: 'hr', }, { content: [ { marks: [], nodeType: 'text', value: '', data: {}, }, ], data: {}, nodeType: 'paragraph', }, ], data: {}, nodeType: 'document', } as Document; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/hyperlink.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'Some text ', marks: [], data: {}, }, { nodeType: 'hyperlink', content: [ { nodeType: 'text', value: 'link', marks: [], data: {}, }, ], data: { uri: 'https://url.org', }, }, { nodeType: 'text', value: ' text.', marks: [], data: {}, }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/index.ts ================================================ export { default as hrDoc } from './hr'; export { default as hyperlinkDoc } from './hyperlink'; export { default as invalidMarksDoc } from './invalid-marks'; export { default as invalidTypeDoc } from './invalid-type'; export { default as paragraphDoc } from './paragraph'; export { default as headingDoc } from './heading'; export { default as marksDoc } from './mark'; export { default as embeddedEntryDoc } from './embedded-entry'; export { default as embeddedResourceDoc } from './embedded-resource'; export { default as olDoc } from './ol'; export { default as ulDoc } from './ul'; export { default as quoteDoc } from './quote'; export { default as tableDoc } from './table'; export { default as tableWithHeaderDoc } from './table-header'; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/inline-entity.ts ================================================ import { BLOCKS, Document, INLINES } from '@contentful/rich-text-types'; export default function inlineEntity(entry: Record , inlineType: INLINES) { return { content: [ { data: {}, content: [ { marks: [], value: '', nodeType: 'text', data: {}, }, { data: entry, content: [ { marks: [], value: '', nodeType: 'text', data: {}, }, ], nodeType: inlineType, }, { marks: [], value: '', nodeType: 'text', data: {}, }, ], nodeType: BLOCKS.PARAGRAPH, }, ], data: {}, nodeType: BLOCKS.DOCUMENT, } as Document; } ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/invalid-marks.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'Hello world!', marks: [ { type: 'UNRECOGNIZED_MARK', }, ], data: {}, }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/invalid-type.ts ================================================ import { Document, BLOCKS } from '@contentful/rich-text-types'; export default { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: 'UNRECOGNIZED_TYPE' as BLOCKS, data: {}, content: [ { nodeType: 'text', value: 'Hello world!', marks: [], data: {}, }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/mark.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default function (mark: string) { return { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'hello world', marks: [{ type: mark }], data: {}, }, ], }, ], } as Document; } ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/ol.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { data: {}, content: [ { data: {}, content: [ { data: {}, content: [ { data: {}, content: [ { data: {}, marks: [], value: 'Hello', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'list-item', }, { data: {}, content: [ { data: {}, content: [ { data: {}, marks: [], value: 'world', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'list-item', }, ], nodeType: 'ordered-list', }, { data: {}, content: [ { data: {}, marks: [], value: '', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'document', } as Document; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/paragraph.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'hello world', marks: [], data: {}, }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/quote.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { data: {}, content: [ { data: {}, content: [ { data: {}, marks: [], value: 'hello', nodeType: 'text', }, ], nodeType: 'paragraph', }, { data: {}, content: [ { data: {}, marks: [], value: 'world', nodeType: 'text', }, ], nodeType: 'blockquote', }, ], nodeType: 'document', } as Document; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/table-header.ts ================================================ import { Document, BLOCKS } from '@contentful/rich-text-types'; export default { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.TABLE, data: {}, content: [ { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_HEADER_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'A 1', }, ], }, ], }, { nodeType: BLOCKS.TABLE_HEADER_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'B 1', }, ], }, ], }, ], }, { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'A 2', }, ], }, ], }, { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'B 2', }, ], }, ], }, ], }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/table.ts ================================================ import { Document, BLOCKS } from '@contentful/rich-text-types'; export default { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.TABLE, data: {}, content: [ { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'A 1', }, ], }, ], }, { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'B 1', }, ], }, ], }, ], }, { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'A 2', }, ], }, ], }, { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'B 2', }, ], }, ], }, ], }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/documents/ul.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { data: {}, content: [ { data: {}, content: [ { data: {}, content: [ { data: {}, content: [ { data: {}, marks: [], value: 'Hello', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'list-item', }, { data: {}, content: [ { data: {}, content: [ { data: {}, marks: [], value: 'world', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'list-item', }, ], nodeType: 'unordered-list', }, { data: {}, content: [ { data: {}, marks: [], value: '', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'document', } as Document; ================================================ FILE: packages/rich-text-html-renderer/src/__test__/index.test.ts ================================================ import { Block, BLOCKS, Document, INLINES, MARKS, ResourceLink } from '@contentful/rich-text-types'; import cloneDeep from 'lodash/cloneDeep'; import { documentToHtmlString, Options } from '../index'; import { embeddedEntryDoc, headingDoc, hrDoc, hyperlinkDoc, invalidMarksDoc, invalidTypeDoc, marksDoc, olDoc, paragraphDoc, tableDoc, quoteDoc, ulDoc, tableWithHeaderDoc, embeddedResourceDoc, } from './documents'; import inlineEntity from './documents/inline-entity'; describe('documentToHtmlString', () => { it('returns empty string when given an empty document', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [], }; expect(documentToHtmlString(document)).toEqual(''); }); it('renders nodes with default node renderer', () => { const docs: Array<{ doc: Document; expected: string }> = [ { doc: paragraphDoc, expected: ' hello world
', }, { doc: headingDoc(BLOCKS.HEADING_1), expected: 'hello world
', }, { doc: headingDoc(BLOCKS.HEADING_2), expected: 'hello world
', }, { doc: headingDoc(BLOCKS.HEADING_3), expected: 'hello world
', }, { doc: headingDoc(BLOCKS.HEADING_4), expected: 'hello world
', }, { doc: headingDoc(BLOCKS.HEADING_5), expected: 'hello world
', }, { doc: headingDoc(BLOCKS.HEADING_6), expected: 'hello world
', }, ]; docs.forEach(({ doc, expected }) => { expect(documentToHtmlString(doc)).toEqual(expected); }); }); it('renders marks with default mark renderer', () => { const docs: Array<{ doc: Document; expected: string }> = [ { doc: marksDoc(MARKS.ITALIC), expected: 'hello world
', }, { doc: marksDoc(MARKS.BOLD), expected: 'hello world
', }, { doc: marksDoc(MARKS.UNDERLINE), expected: 'hello world
', }, { doc: marksDoc(MARKS.CODE), expected: '', }, { doc: marksDoc(MARKS.SUPERSCRIPT), expected: '
hello worldhello world
', }, { doc: marksDoc(MARKS.SUBSCRIPT), expected: 'hello world
', }, { doc: marksDoc(MARKS.STRIKETHROUGH), expected: '', }, ]; docs.forEach(({ doc, expected }) => { expect(documentToHtmlString(doc)).toEqual(expected); }); }); it('renders nodes with passed custom node renderer', () => { const options: Options = { renderNode: { [BLOCKS.PARAGRAPH]: (node, next) => `
hello world${next(node.content)}
`, }, }; const document: Document = paragraphDoc; const expected = `hello world
`; expect(documentToHtmlString(document, options)).toEqual(expected); }); it('renders marks with the passed custom mark rendered', () => { const options: Options = { renderMark: { [MARKS.UNDERLINE]: (text) => `${text}`, }, }; const document: Document = marksDoc(MARKS.UNDERLINE); const expected = 'hello world
'; expect(documentToHtmlString(document, options)).toEqual(expected); }); it('renders escaped html', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'foo & bar', marks: [], data: {}, }, ], }, ], }; const expected = 'foo & bar
'; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders escaped html with marks', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'foo & bar', marks: [{ type: MARKS.UNDERLINE }, { type: MARKS.BOLD }], data: {}, }, ], }, ], }; const expected = 'foo & bar
'; expect(documentToHtmlString(document)).toEqual(expected); }); it('does not render unrecognized marks', () => { const document: Document = invalidMarksDoc; const expected = 'Hello world!
'; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders empty node if type is not recognized', () => { const document: Document = invalidTypeDoc; const expected = ''; expect(documentToHtmlString(document as Document)).toEqual(expected); }); it('renders default entry link block', () => { const entrySys = { sys: { id: '9mpxT4zsRi6Iwukey8KeM', link: 'Link', linkType: 'Entry', }, }; const document: Document = embeddedEntryDoc(entrySys); const expected = ``; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders default resource link block', () => { const resourceLink: ResourceLink = { sys: { urn: 'crn:contentful:::content:spaces/6fqi4ljzyr0e/environments/master/entries/9mpxT4zsRi6Iwukey8KeM', type: 'ResourceLink', linkType: 'Contentful:Entry', }, }; const document: Document = embeddedResourceDoc(resourceLink); const expected = ``; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders ordered lists', () => { const document: Document = olDoc; const expected = ``; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders unordered lists', () => { const document: Document = ulDoc; const expected = `
Hello
world
`; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders blockquotes', () => { const document: Document = quoteDoc; const expected = `
Hello
world
hello
world`; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders horizontal rule', () => { const document: Document = hrDoc; const expected = 'hello world
'; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders tables', () => { const document: Document = tableDoc; const expected = '' + '
'; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders tables with header', () => { const expected = '' + ' A 1
B 1
' + ' A 2
B 2
' + '
'; expect(documentToHtmlString(tableWithHeaderDoc)).toEqual(expected); }); it('does not crash with inline elements (e.g. hyperlink)', () => { const document: Document = hyperlinkDoc; expect(documentToHtmlString(document)).toBeTruthy(); }); it('renders hyperlink', () => { const document: Document = hyperlinkDoc; const expected = '' + ' A 1
B 1
' + ' A 2
B 2
Some text link text.
'; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders hyperlink without allowing html injection via `data.uri`', () => { const document: Document = cloneDeep(hyperlinkDoc); (document.content[0].content[1] as Block).data.uri = '">no html injection!no html injection!link text.'; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders hyperlink without invalid non-string `data.uri` values', () => { const document: Document = cloneDeep(hyperlinkDoc); (document.content[0].content[1] as Block).data.uri = 42; const expected = 'Some text link text.
'; expect(documentToHtmlString(document)).toEqual(expected); }); it(`renders asset hyperlink`, () => { const asset = { target: { sys: { id: '9mpxT4zsRi6Iwukey8KeM', type: 'Link', linkType: 'Asset', }, }, }; const document: Document = inlineEntity(asset, INLINES.ASSET_HYPERLINK); const expected = `type: ${INLINES.ASSET_HYPERLINK} id: ${asset.target.sys.id}
`; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders entry hyperlink', () => { const entry = { target: { sys: { id: '9mpxT4zsRi6Iwukey8KeM', type: 'Link', linkType: 'Entry', }, }, }; const document: Document = inlineEntity(entry, INLINES.ENTRY_HYPERLINK); const expected = `type: ${INLINES.ENTRY_HYPERLINK} id: ${entry.target.sys.id}
`; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders resource hyperlink', () => { const entry = { target: { sys: { urn: 'crn:contentful:::content:spaces/6fqi4ljzyr0e/environments/master/entries/9mpxT4zsRi6Iwukey8KeM', type: 'ResourceLink', linkType: 'Contentful:Entry', }, }, }; const document: Document = inlineEntity(entry, INLINES.RESOURCE_HYPERLINK); const expected = `type: ${INLINES.RESOURCE_HYPERLINK} urn: ${entry.target.sys.urn}
`; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders embedded entry', () => { const entry = { target: { sys: { id: '9mpxT4zsRi6Iwukey8KeM', type: 'Link', linkType: 'Entry', }, }, }; const document: Document = inlineEntity(entry, INLINES.EMBEDDED_ENTRY); const expected = `type: ${INLINES.EMBEDDED_ENTRY} id: ${entry.target.sys.id}
`; expect(documentToHtmlString(document)).toEqual(expected); }); it('renders embedded resource', () => { const entry = { target: { sys: { urn: 'crn:contentful:::content:spaces/6fqi4ljzyr0e/environments/master/entries/9mpxT4zsRi6Iwukey8KeM', type: 'Link', linkType: 'Contentful:Entry', }, }, }; const document: Document = inlineEntity(entry, INLINES.EMBEDDED_RESOURCE); const expected = `type: ${INLINES.EMBEDDED_RESOURCE} urn: ${entry.target.sys.urn}
`; expect(documentToHtmlString(document)).toEqual(expected); }); it('does not crash with empty documents', () => { expect(documentToHtmlString({} as Document)).toEqual(''); }); it('does not crash with undefined documents', () => { expect(documentToHtmlString(undefined as Document)).toEqual(''); }); it('preserves whitespace with preserveWhitespace option', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'hello world', marks: [], data: {}, }, ], }, ], }; const options: Options = { preserveWhitespace: true, }; const expected = 'hello world
'; expect(documentToHtmlString(document, options)).toEqual(expected); }); it('preserves line breaks with preserveWhitespace option', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'hello\nworld', marks: [], data: {}, }, ], }, ], }; const options: Options = { preserveWhitespace: true, }; const expected = 'hello
'; expect(documentToHtmlString(document, options)).toEqual(expected); }); it('preserves both spaces and line breaks with preserveWhitespace option', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'hello \n world', marks: [], data: {}, }, ], }, ], }; const options: Options = { preserveWhitespace: true, }; const expected = '
worldhello
'; expect(documentToHtmlString(document, options)).toEqual(expected); }); }); describe('stripEmptyTrailingParagraph', () => { it('strips empty trailing paragraph when enabled', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: true, }; const result = documentToHtmlString(document, options); expect(result).toEqual('
worldHello world
'); }); it('does not strip empty trailing paragraph when disabled', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: false, }; const result = documentToHtmlString(document, options); expect(result).toEqual('Hello world
'); }); it('does not strip empty trailing paragraph when it is the only child', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: true, }; const result = documentToHtmlString(document, options); expect(result).toEqual(''); }); it('does not strip non-empty trailing paragraph', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Not empty', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: true, }; const result = documentToHtmlString(document, options); expect(result).toEqual('Hello world
Not empty
'); }); it('does not strip trailing paragraph with multiple text nodes', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: true, }; const result = documentToHtmlString(document, options); expect(result).toEqual('Hello world
'); }); it('does not strip trailing non-paragraph node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.HEADING_1, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: true, }; const result = documentToHtmlString(document, options); expect(result).toEqual('Hello world
'); }); }); ================================================ FILE: packages/rich-text-html-renderer/src/escapeHtml.ts ================================================ const escapeRegExp = /["'&<>]/g; const escapeMap: Record= { '"': '"', '&': '&', "'": ''', '<': '<', '>': '>', }; export function escapeHtml(input: string): string { return input.replace(escapeRegExp, (ch) => escapeMap[ch]); } ================================================ FILE: packages/rich-text-html-renderer/src/index.ts ================================================ import { BLOCKS, Block, Document, INLINES, Inline, MARKS, Mark, Text, helpers, } from '@contentful/rich-text-types'; import { escapeHtml } from './escapeHtml'; const attributeValue = (value: string) => `"${value.replace(/"/g, '"')}"`; const defaultNodeRenderers: RenderNode = { [BLOCKS.PARAGRAPH]: (node, next) => ` ${next(node.content)}
`, [BLOCKS.HEADING_1]: (node, next) => `${next(node.content)}
`, [BLOCKS.HEADING_2]: (node, next) => `${next(node.content)}
`, [BLOCKS.HEADING_3]: (node, next) => `${next(node.content)}
`, [BLOCKS.HEADING_4]: (node, next) => `${next(node.content)}
`, [BLOCKS.HEADING_5]: (node, next) => `${next(node.content)}
`, [BLOCKS.HEADING_6]: (node, next) => `${next(node.content)}
`, [BLOCKS.EMBEDDED_ENTRY]: (node, next) => `${next(node.content)}`, [BLOCKS.EMBEDDED_RESOURCE]: (node, next) => `${next(node.content)}`, [BLOCKS.EMBEDDED_ASSET]: (node) => defaultBlockAsset(node as Block), [BLOCKS.UL_LIST]: (node, next) => `${next(node.content)}
`, [BLOCKS.OL_LIST]: (node, next) => `${next(node.content)}
`, [BLOCKS.LIST_ITEM]: (node, next) => `- ${next(node.content)}
`, [BLOCKS.QUOTE]: (node, next) => `${next(node.content)}`, [BLOCKS.HR]: () => '
', [BLOCKS.TABLE]: (node, next) => `${next(node.content)}
`, [BLOCKS.TABLE_ROW]: (node, next) => `${next(node.content)} `, [BLOCKS.TABLE_HEADER_CELL]: (node, next) => `${next(node.content)} `, [BLOCKS.TABLE_CELL]: (node, next) => `${next(node.content)} `, [INLINES.ASSET_HYPERLINK]: (node) => defaultInline(INLINES.ASSET_HYPERLINK, node as Inline), [INLINES.ENTRY_HYPERLINK]: (node) => defaultInline(INLINES.ENTRY_HYPERLINK, node as Inline), [INLINES.RESOURCE_HYPERLINK]: (node) => defaultInlineResource(INLINES.RESOURCE_HYPERLINK, node as Inline), [INLINES.EMBEDDED_ENTRY]: (node) => defaultInline(INLINES.EMBEDDED_ENTRY, node as Inline), [INLINES.EMBEDDED_RESOURCE]: (node) => defaultInlineResource(INLINES.EMBEDDED_RESOURCE, node as Inline), [INLINES.HYPERLINK]: (node, next) => { const href = typeof node.data.uri === 'string' ? node.data.uri : ''; return `${next(node.content)}`; }, }; const defaultMarkRenderers: RenderMark = { [MARKS.BOLD]: (text) => `${text}`, [MARKS.ITALIC]: (text) => `${text}`, [MARKS.UNDERLINE]: (text) => `${text}`, [MARKS.CODE]: (text) => `${text}`, [MARKS.SUPERSCRIPT]: (text) => `${text}`, [MARKS.SUBSCRIPT]: (text) => `${text}`, [MARKS.STRIKETHROUGH]: (text) => `${text}`, }; const defaultBlockAsset = (node: Block) => { const fileUrl = node.data?.target?.fields?.file?.url ?? ''; const imageUrl = fileUrl.startsWith('//') ? `https:${fileUrl}` : fileUrl; const imgDescription = node.data?.target?.fields?.description ?? ''; return ``; }; const defaultInline = (type: string, node: Inline) => `type: ${escapeHtml(type)} id: ${escapeHtml(node.data.target.sys.id)}`; const defaultInlineResource = (type: string, node: Inline) => `type: ${escapeHtml(type)} urn: ${escapeHtml(node.data?.target?.sys?.urn ?? '')}`; export type CommonNode = Text | Block | Inline; export interface Next { (nodes: CommonNode[]): string; } export interface NodeRenderer { (node: Block | Inline, next: Next): string; } export interface RenderNode { [k: string]: NodeRenderer; } export interface RenderMark { [k: string]: (text: string) => string; } export interface Options { /** * Node renderers */ renderNode?: RenderNode; /** * Mark renderers */ renderMark?: RenderMark; /** * Keep line breaks and multiple spaces */ preserveWhitespace?: boolean; /** * Strip empty trailing paragraph from the document */ stripEmptyTrailingParagraph?: boolean; } /** * Serialize a Contentful Rich Text `document` to an html string. */ export function documentToHtmlString( richTextDocument: Document, options: Partial
= {}, ): string { if (!richTextDocument || !richTextDocument.content) { return ''; } // Strip empty trailing paragraph if enabled let processedDocument = richTextDocument; if (options.stripEmptyTrailingParagraph) { processedDocument = helpers.stripEmptyTrailingParagraphFromDocument(richTextDocument); } return nodeListToHtmlString(processedDocument.content, { renderNode: { ...defaultNodeRenderers, ...options.renderNode, }, renderMark: { ...defaultMarkRenderers, ...options.renderMark, }, preserveWhitespace: options.preserveWhitespace, }); } function nodeListToHtmlString( nodes: CommonNode[], { renderNode, renderMark, preserveWhitespace }: Options, ): string { return nodes .map ((node) => nodeToHtmlString(node, { renderNode, renderMark, preserveWhitespace })) .join(''); } function nodeToHtmlString( node: CommonNode, { renderNode, renderMark, preserveWhitespace }: Options, ): string { if (helpers.isText(node)) { let nodeValue = escapeHtml(node.value); // If preserveWhitespace is true, handle line breaks and spaces. if (preserveWhitespace) { nodeValue = nodeValue .replace(/\n/g, '
') .replace(/ {2,}/g, (match) => ' '.repeat(match.length)); } if (node.marks.length > 0) { return node.marks.reduce((value: string, mark: Mark) => { if (!renderMark[mark.type]) { return value; } return renderMark[mark.type](value); }, nodeValue); } return nodeValue; } else { const nextNode: Next = (nodes) => nodeListToHtmlString(nodes, { renderMark, renderNode, preserveWhitespace }); if (!node.nodeType || !renderNode[node.nodeType]) { // TODO: Figure what to return when passed an unrecognized node. return ''; } return renderNode[node.nodeType](node, nextNode); } } ================================================ FILE: packages/rich-text-html-renderer/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "declarationDir": "dist/types", "outDir": "dist/lib", "typeRoots": ["../../node_modules/@types", "node_modules/@types", "src/typings"] }, "include": ["src"] } ================================================ FILE: packages/rich-text-links/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [17.1.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@17.1.0...@contentful/rich-text-links@17.1.1) (2026-04-09) ### Bug Fixes - **rich-text-types:** improve esm compability [ZEND-7778] ([#1073](https://github.com/contentful/rich-text/issues/1073)) ([204aaec](https://github.com/contentful/rich-text/commit/204aaecc3893633c081986f44896e14272fa376a)) # [17.1.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@17.0.8...@contentful/rich-text-links@17.1.0) (2026-04-08) ### Features - add esm support [ZEND-7778] ([#1069](https://github.com/contentful/rich-text/issues/1069)) ([8c320bd](https://github.com/contentful/rich-text/commit/8c320bde7fa313572bdad84b91a6fd12224afc3a)), closes [#1068](https://github.com/contentful/rich-text/issues/1068) ## [17.0.8](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@17.0.7...@contentful/rich-text-links@17.0.8) (2025-11-04) **Note:** Version bump only for package @contentful/rich-text-links ## [17.0.7](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@17.0.6...@contentful/rich-text-links@17.0.7) (2025-09-23) **Note:** Version bump only for package @contentful/rich-text-links ## [17.0.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@17.0.5...@contentful/rich-text-links@17.0.6) (2025-09-23) **Note:** Version bump only for package @contentful/rich-text-links ## [17.0.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@17.0.4...@contentful/rich-text-links@17.0.5) (2025-09-10) **Note:** Version bump only for package @contentful/rich-text-links ## [17.0.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@17.0.3...@contentful/rich-text-links@17.0.4) (2025-09-09) **Note:** Version bump only for package @contentful/rich-text-links ## [17.0.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@17.0.2...@contentful/rich-text-links@17.0.3) (2025-09-04) **Note:** Version bump only for package @contentful/rich-text-links ## [17.0.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@17.0.1...@contentful/rich-text-links@17.0.2) (2025-07-15) **Note:** Version bump only for package @contentful/rich-text-links ## [17.0.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@17.0.0...@contentful/rich-text-links@17.0.1) (2025-06-16) **Note:** Version bump only for package @contentful/rich-text-links # [17.0.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.10...@contentful/rich-text-links@17.0.0) (2024-10-29) ### Features - bring rich text validator [TOL-2426] ([#694](https://github.com/contentful/rich-text/issues/694)) ([30893a6](https://github.com/contentful/rich-text/commit/30893a68b171167502135b48258ba4b93c8375c7)) ### BREAKING CHANGES - removed getSchemaWithNodeType in favor of validateRichTextDocument ## [16.7.10](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.9...@contentful/rich-text-links@16.7.10) (2024-09-09) **Note:** Version bump only for package @contentful/rich-text-links ## [16.7.9](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.8...@contentful/rich-text-links@16.7.9) (2024-08-26) **Note:** Version bump only for package @contentful/rich-text-links ## [16.7.8](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.7...@contentful/rich-text-links@16.7.8) (2024-07-29) **Note:** Version bump only for package @contentful/rich-text-links ## [16.7.7](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.6...@contentful/rich-text-links@16.7.7) (2024-07-24) **Note:** Version bump only for package @contentful/rich-text-links ## [16.7.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.5...@contentful/rich-text-links@16.7.6) (2024-07-17) **Note:** Version bump only for package @contentful/rich-text-links ## [16.7.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.4...@contentful/rich-text-links@16.7.5) (2024-07-17) **Note:** Version bump only for package @contentful/rich-text-links ## [16.7.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.3...@contentful/rich-text-links@16.7.4) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-links ## [16.7.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.2...@contentful/rich-text-links@16.7.3) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-links ## [16.7.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.1...@contentful/rich-text-links@16.7.2) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-links ## [16.7.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.7.0...@contentful/rich-text-links@16.7.1) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-links # [16.7.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.6.5...@contentful/rich-text-links@16.7.0) (2024-06-25) ### Features - switch from tslint to eslint [TOL-2218][tol-2203] ([#594](https://github.com/contentful/rich-text/issues/594)) ([c077b5a](https://github.com/contentful/rich-text/commit/c077b5af58f94c8dc6af4715d4b82c2771d643c5)) ## [16.6.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.6.4...@contentful/rich-text-links@16.6.5) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-links ## [16.6.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.6.3...@contentful/rich-text-links@16.6.4) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-links ## [16.6.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.6.2...@contentful/rich-text-links@16.6.3) (2024-05-28) **Note:** Version bump only for package @contentful/rich-text-links ## [16.6.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.6.1...@contentful/rich-text-links@16.6.2) (2024-05-27) **Note:** Version bump only for package @contentful/rich-text-links ## [16.6.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.6.0...@contentful/rich-text-links@16.6.1) (2024-05-24) **Note:** Version bump only for package @contentful/rich-text-links # [16.6.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.5.5...@contentful/rich-text-links@16.6.0) (2024-05-22) ### Features - upgrading rollup to latest version [TOL-2097] ([#559](https://github.com/contentful/rich-text/issues/559)) ([f14d197](https://github.com/contentful/rich-text/commit/f14d1974590d58f92bbf4cb56644095dba929ad9)) ## [16.5.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.5.4...@contentful/rich-text-links@16.5.5) (2024-03-04) **Note:** Version bump only for package @contentful/rich-text-links ## [16.5.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.5.3...@contentful/rich-text-links@16.5.4) (2024-01-30) **Note:** Version bump only for package @contentful/rich-text-links ## [16.5.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.5.2...@contentful/rich-text-links@16.5.3) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-links ## [16.5.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.5.1...@contentful/rich-text-links@16.5.2) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-links ## [16.5.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.5.0...@contentful/rich-text-links@16.5.1) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-links # [16.5.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.4.1...@contentful/rich-text-links@16.5.0) (2023-09-12) ### Features - add new nodes [DANTE-1157] ([#494](https://github.com/contentful/rich-text/issues/494)) ([b1fa6df](https://github.com/contentful/rich-text/commit/b1fa6dffc4bd7e1d367e9ce2cfddffe4fe07be47)) ## [16.4.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.4.0...@contentful/rich-text-links@16.4.1) (2023-08-04) **Note:** Version bump only for package @contentful/rich-text-links # [16.4.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.3.0...@contentful/rich-text-links@16.4.0) (2023-07-10) ### Features - allow document parameter to be nullable ([#471](https://github.com/contentful/rich-text/issues/471)) ([e50f8d2](https://github.com/contentful/rich-text/commit/e50f8d24c60a74f43f84f415a5c393038919efd2)) # [16.3.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.2.0...@contentful/rich-text-links@16.3.0) (2023-06-09) ### Features - change visitNodes to visit nodes in depth-first order ([#475](https://github.com/contentful/rich-text/issues/475)) ([ace5225](https://github.com/contentful/rich-text/commit/ace52253af4e83035075a466b7c97a08072e35ad)) # [16.2.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.1.1...@contentful/rich-text-links@16.2.0) (2023-06-07) ### Features - add deduplicate option to getRichTextResourceLinks ([#470](https://github.com/contentful/rich-text/issues/470)) ([1141d5b](https://github.com/contentful/rich-text/commit/1141d5b6848e32efce198ad2d47b6442a9660aa0)) ## [16.1.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.1.0...@contentful/rich-text-links@16.1.1) (2023-05-31) ### Bug Fixes - prevent crash when a null value is passed as document ([#468](https://github.com/contentful/rich-text/issues/468)) ([3c8531c](https://github.com/contentful/rich-text/commit/3c8531cbcdde623c66c4b13d49fff73cad096871)) # [16.1.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.0.4...@contentful/rich-text-links@16.1.0) (2023-05-26) ### Features - 🎸 add `getRichTextResourceLinks` ([#465](https://github.com/contentful/rich-text/issues/465)) ([5746ba6](https://github.com/contentful/rich-text/commit/5746ba652ae5eac2df27d05193e19aef9e85c438)) ## [16.0.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.0.3...@contentful/rich-text-links@16.0.4) (2023-05-04) **Note:** Version bump only for package @contentful/rich-text-links ## [16.0.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.0.2...@contentful/rich-text-links@16.0.3) (2023-03-14) **Note:** Version bump only for package @contentful/rich-text-links ## [16.0.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.0.1...@contentful/rich-text-links@16.0.2) (2022-12-01) **Note:** Version bump only for package @contentful/rich-text-links ## [16.0.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-links@16.0.0...@contentful/rich-text-links@16.0.1) (2022-12-01) **Note:** Version bump only for package @contentful/rich-text-links # 16.0.0 (2022-12-01) ## 15.15.1 (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # 15.15.0 (2022-11-29) ## 15.14.1 (2022-11-23) # 15.14.0 (2022-11-14) ## 15.13.2 (2022-09-07) ### Bug Fixes - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) ## 15.12.1 (2022-04-21) # 15.12.0 (2022-03-25) ## 15.11.1 (2022-01-04) # 15.11.0 (2021-12-27) ## 15.10.1 (2021-12-21) # 15.10.0 (2021-12-15) ## 15.9.1 (2021-12-10) # 15.9.0 (2021-12-09) # 15.7.0 (2021-11-11) ## 15.6.2 (2021-11-05) ## 15.6.1 (2021-11-05) # 15.6.0 (2021-11-04) ## 15.5.1 (2021-10-25) # 15.5.0 (2021-10-25) ## 15.3.6 (2021-09-15) ## 15.3.5 (2021-09-13) ## 15.3.3 (2021-09-07) ## 15.3.2 (2021-09-07) ## 15.3.1 (2021-09-07) # 15.3.0 (2021-09-06) # 15.1.0 (2021-08-02) # 15.0.0 (2021-06-15) ## 14.1.2 (2020-11-02) ## 14.0.1 (2020-01-30) # 14.0.0 (2020-01-28) ### Bug Fixes - 🐛 detect links as children of nodes also having links ([565f423](https://github.com/contentful/rich-text/commit/565f423b2881c7bcfac40e9ee9ecb6a545fce047)) # 13.4.0 (2019-08-01) # 13.3.0 (2019-03-21) # 13.1.0 (2019-03-04) ### Bug Fixes - 🐛 Fix incorrect typings file path ([cca74d5](https://github.com/contentful/rich-text/commit/cca74d5594fb0594fd1117945e087afa2000877b)) ### Features - 🎸 Support filtering by node name in rich-text-links ([5ec0dda](https://github.com/contentful/rich-text/commit/5ec0dda07cdbe3d28027d28520c553b074ca699f)) # 13.0.0 (2019-01-22) ## 12.1.2 (2018-12-14) ## 12.0.3 (2018-12-05) ### Performance Improvements - ⚡️ prefer iterator unrolling to Array.from ([a887a92](https://github.com/contentful/rich-text/commit/a887a9213dee9dd6986cf313e67b94fd020d138b)) ## 12.0.1 (2018-12-04) # 12.0.0 (2018-11-29) ### Features - 🎸 `getRichTextEntityLinks` perf and signature changes ([fcd6d7f](https://github.com/contentful/rich-text/commit/fcd6d7f42e63df87ddb58d651b86a023d45f990e)) ### Performance Improvements - ⚡️ Add benchmark support ([ae278b7](https://github.com/contentful/rich-text/commit/ae278b7be515a52576af0a1abb6a4cefb05f71a7)) ### BREAKING CHANGES - Produces an entirely new return value and obviates the `linkType` parameter (all entities are grouped and returned by default). # 11.0.0 (2018-11-27) # 10.3.0 (2018-11-26) ### Features - 🎸 return all entity links when no linkType is provided ([02970ea](https://github.com/contentful/rich-text/commit/02970ea61b5b13d99c7b629902defe704c146b17)) # 10.2.0 (2018-11-19) ### Features - 🎸 Rename, refactor, docu. & cache r-t-links ([9cf6d14](https://github.com/contentful/rich-text/commit/9cf6d1460833300053cb8dde5a6e29b0ddf89964)) ## [15.15.1](https://github.com/contentful/rich-text/compare/v15.15.0...v15.15.1) (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # [15.15.0](https://github.com/contentful/rich-text/compare/v15.14.1...v15.15.0) (2022-11-29) **Note:** Version bump only for package @contentful/rich-text-links ## [15.14.1](https://github.com/contentful/rich-text/compare/v15.14.0...v15.14.1) (2022-11-23) **Note:** Version bump only for package @contentful/rich-text-links # [15.14.0](https://github.com/contentful/rich-text/compare/v15.13.2...v15.14.0) (2022-11-14) **Note:** Version bump only for package @contentful/rich-text-links ## [15.13.2](https://github.com/contentful/rich-text/compare/v15.13.1...v15.13.2) (2022-09-07) ### Bug Fixes - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) ## [15.12.1](https://github.com/contentful/rich-text/compare/v15.12.0...v15.12.1) (2022-04-21) **Note:** Version bump only for package @contentful/rich-text-links # [15.12.0](https://github.com/contentful/rich-text/compare/v15.11.2...v15.12.0) (2022-03-25) **Note:** Version bump only for package @contentful/rich-text-links ## [15.11.1](https://github.com/contentful/rich-text/compare/v15.11.0...v15.11.1) (2022-01-04) **Note:** Version bump only for package @contentful/rich-text-links # [15.11.0](https://github.com/contentful/rich-text/compare/v15.10.1...v15.11.0) (2021-12-27) **Note:** Version bump only for package @contentful/rich-text-links ## [15.10.1](https://github.com/contentful/rich-text/compare/v15.10.0...v15.10.1) (2021-12-21) **Note:** Version bump only for package @contentful/rich-text-links # [15.10.0](https://github.com/contentful/rich-text/compare/v15.9.1...v15.10.0) (2021-12-15) **Note:** Version bump only for package @contentful/rich-text-links ## [15.9.1](https://github.com/contentful/rich-text/compare/v15.9.0...v15.9.1) (2021-12-10) **Note:** Version bump only for package @contentful/rich-text-links # [15.9.0](https://github.com/contentful/rich-text/compare/v15.8.0...v15.9.0) (2021-12-09) **Note:** Version bump only for package @contentful/rich-text-links # [15.7.0](https://github.com/contentful/rich-text/compare/v15.6.2...v15.7.0) (2021-11-11) **Note:** Version bump only for package @contentful/rich-text-links ## [15.6.2](https://github.com/contentful/rich-text/compare/v15.6.1...v15.6.2) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text-links ## [15.6.1](https://github.com/contentful/rich-text/compare/v15.6.0...v15.6.1) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text-links # [15.6.0](https://github.com/contentful/rich-text/compare/v15.5.1...v15.6.0) (2021-11-04) **Note:** Version bump only for package @contentful/rich-text-links ## [15.5.1](https://github.com/contentful/rich-text/compare/v15.5.0...v15.5.1) (2021-10-25) **Note:** Version bump only for package @contentful/rich-text-links # [15.5.0](https://github.com/contentful/rich-text/compare/v15.4.0...v15.5.0) (2021-10-25) **Note:** Version bump only for package @contentful/rich-text-links ## [15.3.6](https://github.com/contentful/rich-text/compare/v15.3.5...v15.3.6) (2021-09-15) **Note:** Version bump only for package @contentful/rich-text-links ## [15.3.5](https://github.com/contentful/rich-text/compare/v15.3.4...v15.3.5) (2021-09-13) **Note:** Version bump only for package @contentful/rich-text-links ## [15.3.3](https://github.com/contentful/rich-text/compare/v15.3.2...v15.3.3) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-links ## [15.3.2](https://github.com/contentful/rich-text/compare/v15.3.1...v15.3.2) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-links ## [15.3.1](https://github.com/contentful/rich-text/compare/v15.3.0...v15.3.1) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-links # [15.3.0](https://github.com/contentful/rich-text/compare/v15.2.0...v15.3.0) (2021-09-06) **Note:** Version bump only for package @contentful/rich-text-links # [15.2.0](https://github.com/contentful/rich-text/compare/v15.2.0...v15.1.0) (2021-09-06) # [15.1.0](https://github.com/contentful/rich-text/compare/v15.0.0...v15.1.0) (2021-08-02) # [15.0.0](https://github.com/contentful/rich-text/compare/v14.2.0...v15.0.0) (2021-06-15) ## [14.1.2](https://github.com/contentful/rich-text/compare/v14.0.1...v14.1.2) (2020-11-02) ## [14.0.1](https://github.com/contentful/rich-text/compare/v14.0.0...v14.0.1) (2020-01-30) # [14.0.0](https://github.com/contentful/rich-text/compare/v13.4.0...v14.0.0) (2020-01-28) ### Bug Fixes - 🐛 detect links as children of nodes also having links ([565f423](https://github.com/contentful/rich-text/commit/565f423b2881c7bcfac40e9ee9ecb6a545fce047)) # [13.4.0](https://github.com/contentful/rich-text/compare/v13.3.0...v13.4.0) (2019-08-01) # [13.3.0](https://github.com/contentful/rich-text/compare/v13.2.0...v13.3.0) (2019-03-21) # [13.1.0](https://github.com/contentful/rich-text/compare/v13.0.1...v13.1.0) (2019-03-04) ### Bug Fixes - 🐛 Fix incorrect typings file path ([cca74d5](https://github.com/contentful/rich-text/commit/cca74d5594fb0594fd1117945e087afa2000877b)) ### Features - 🎸 Support filtering by node name in rich-text-links ([5ec0dda](https://github.com/contentful/rich-text/commit/5ec0dda07cdbe3d28027d28520c553b074ca699f)) # [13.0.0](https://github.com/contentful/rich-text/compare/v12.2.1...v13.0.0) (2019-01-22) ## [12.1.2](https://github.com/contentful/rich-text/compare/v12.1.1...v12.1.2) (2018-12-14) ## [12.0.3](https://github.com/contentful/rich-text/compare/v12.0.2...v12.0.3) (2018-12-05) ### Performance Improvements - ⚡️ prefer iterator unrolling to Array.from ([a887a92](https://github.com/contentful/rich-text/commit/a887a9213dee9dd6986cf313e67b94fd020d138b)) ## [12.0.1](https://github.com/contentful/rich-text/compare/v12.0.0...v12.0.1) (2018-12-04) # [12.0.0](https://github.com/contentful/rich-text/compare/v11.0.0...v12.0.0) (2018-11-29) ### Features - 🎸 `getRichTextEntityLinks` perf and signature changes ([fcd6d7f](https://github.com/contentful/rich-text/commit/fcd6d7f42e63df87ddb58d651b86a023d45f990e)) ### Performance Improvements - ⚡️ Add benchmark support ([ae278b7](https://github.com/contentful/rich-text/commit/ae278b7be515a52576af0a1abb6a4cefb05f71a7)) ### BREAKING CHANGES - Produces an entirely new return value and obviates the `linkType` parameter (all entities are grouped and returned by default). # [11.0.0](https://github.com/contentful/rich-text/compare/v10.3.0...v11.0.0) (2018-11-27) # [10.3.0](https://github.com/contentful/rich-text/compare/v10.2.0...v10.3.0) (2018-11-26) ### Features - 🎸 return all entity links when no linkType is provided ([02970ea](https://github.com/contentful/rich-text/commit/02970ea61b5b13d99c7b629902defe704c146b17)) # [10.2.0](https://github.com/contentful/rich-text/compare/v10.1.0...v10.2.0) (2018-11-19) ### Features - 🎸 Rename, refactor, docu. & cache r-t-links ([9cf6d14](https://github.com/contentful/rich-text/commit/9cf6d1460833300053cb8dde5a6e29b0ddf89964)) ================================================ FILE: packages/rich-text-links/README.md ================================================ # rich-text-links Entity (entry and asset) link extraction utilities for the Contentful rich text field type. ## Installation Using [npm](http://npmjs.org/): ```sh npm install @contentful/rich-text-links ``` Using [yarn](https://yarnpkg.com/): ```sh yarn add @contentful/rich-text-links ``` ## Usage ```javascript import { getRichTextEntityLinks } from '@contentful/rich-text-links'; const document = { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'embedded-entry-block', data: { target: { sys: { linkType: 'Entry', type: 'Link', id: 'yXmVKmaDBm8tRfQMwA0e', }, }, }, content: [], }, { nodeType: 'embedded-asset-block', data: { target: { sys: { linkType: 'Asset', type: 'Link', id: 'jNhaW0aSc6Hu74SHVMtq', }, }, }, content: [], }, ], }, ], }; getRichTextEntityLinks(document); /** * -> * { * Entry: [ * { linkType: 'Entry', type: 'Link', id: 'yXmVKmaDBm8tRfQMwA0e' } * ], * Asset: [ * { linkType: 'Asset', type: 'Link', id: 'jNhaW0aSc6Hu74SHVMtq' } * ] * } */ ``` ================================================ FILE: packages/rich-text-links/jest.config.js ================================================ const getBaseConfig = require('../../baseJestConfig'); const package = require('./package.json'); const packageName = package.name.split('@contentful/')[1]; module.exports = { ...getBaseConfig(packageName), }; ================================================ FILE: packages/rich-text-links/package.json ================================================ { "name": "@contentful/rich-text-links", "version": "17.1.1", "main": "dist/rich-text-links.es5.js", "module": "dist/rich-text-links.esm.js", "typings": "dist/types/index.d.ts", "exports": { ".": { "types": "./dist/types/index.d.ts", "import": "./dist/rich-text-links.esm.js", "require": "./dist/rich-text-links.es5.js", "default": "./dist/rich-text-links.es5.js" }, "./package.json": "./package.json" }, "files": [ "dist" ], "repository": { "type": "git", "url": "https://github.com/contentful/rich-text.git" }, "license": "MIT", "engines": { "node": ">=20.0.0" }, "publishConfig": { "access": "public", "registry": "https://npm.pkg.github.com/" }, "scripts": { "build": "tsc --module commonjs && rollup -c --bundleConfigAsCjs rollup.config.js", "prebuild": "rimraf dist", "start": "tsc && rollup -c --bundleConfigAsCjs rollup.config.js -w", "test": "jest" }, "dependencies": { "@contentful/rich-text-types": "^17.2.7" }, "devDependencies": { "ts-jest": "^29.1.2" } } ================================================ FILE: packages/rich-text-links/rollup.config.js ================================================ import config from '../../rollup.config'; import { main as outputFile, dependencies } from './package.json'; export default config(outputFile, { external: Object.keys(dependencies), }); ================================================ FILE: packages/rich-text-links/src/__test__/index.test.ts ================================================ import { Document, BLOCKS, INLINES } from '@contentful/rich-text-types'; import { getAllRichTextResourceLinks, getRichTextEntityLinks, getRichTextResourceLinks, } from '../index'; import { Maybe } from '../types/utils'; function makeResourceLink(spaceId: string, entryId: string) { return { sys: { type: 'ResourceLink', linkType: 'Contentful:Entry', urn: `crn:contentful:::content:spaces/${spaceId}/entries/${entryId}`, }, }; } describe('getRichTextEntityLinks', () => { describe('returning top-level rich text links', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_ENTRY, data: { target: { sys: { linkType: 'Entry', type: 'Link', id: 'foo', }, }, }, content: [], }, { nodeType: BLOCKS.EMBEDDED_ASSET, data: { target: { sys: { linkType: 'Asset', type: 'Link', id: 'bar', }, }, }, content: [], }, ], }, ], }; it('returns all entity link objects', () => { expect(getRichTextEntityLinks(document)).toEqual({ Entry: [ { linkType: 'Entry', type: 'Link', id: 'foo', }, ], Asset: [ { linkType: 'Asset', type: 'Link', id: 'bar', }, ], }); }); it('returns an empty array if document parameter is `null`', () => { /** * This test is important! Not all consumers of this library have correct typescript types, * we know that not handling `null` gracefully will cause issues in production. */ const documentThatIsNull: Maybe= null; expect(getRichTextEntityLinks(documentThatIsNull)).toEqual({ Asset: [], Entry: [] }); }); it('returns an empty array if document parameter is `undefined`', () => { const documentThatIsUndefined: Maybe = undefined; expect(getRichTextEntityLinks(documentThatIsUndefined)).toEqual({ Asset: [], Entry: [] }); }); }); describe('returning rich text links at an arbitrary level of depth', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_ENTRY, data: { target: { sys: { linkType: 'Entry', type: 'Link', id: 'bar', }, }, }, content: [], }, { nodeType: BLOCKS.OL_LIST, data: {}, content: [ { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_ASSET, data: { target: { sys: { linkType: 'Asset', type: 'Link', id: 'quux', }, }, }, content: [], }, ], }, ], }, ], }, { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: INLINES.ENTRY_HYPERLINK, data: { target: { sys: { linkType: 'Entry', type: 'Link', id: 'baz', }, }, }, content: [], }, ], }, ], }, { nodeType: BLOCKS.EMBEDDED_ASSET, data: { target: { sys: { linkType: 'Asset', type: 'Link', id: 'qux', }, }, }, content: [], }, ], }, { nodeType: BLOCKS.TABLE, data: {}, content: [ { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_HEADER_CELL, data: {}, content: [{ data: {}, content: [], nodeType: BLOCKS.PARAGRAPH }], }, { nodeType: BLOCKS.TABLE_HEADER_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: INLINES.EMBEDDED_ENTRY, data: { target: { sys: { id: 'inline-header-entry', type: 'Link', linkType: 'Entry' }, }, }, content: [], }, ], }, ], }, ], }, { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: INLINES.ENTRY_HYPERLINK, data: { target: { sys: { id: 'hyperlink-cell-entry', type: 'Link', linkType: 'Entry' }, }, }, content: [], }, ], }, ], }, { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: INLINES.ASSET_HYPERLINK, data: { target: { sys: { id: 'hyperlink-cell-asset', type: 'Link', linkType: 'Asset', }, }, }, content: [], }, ], }, ], nodeType: BLOCKS.PARAGRAPH, }, ], }, ], }, ], }, ], }; it('returns all entity link objects in the same order as defined in the document', () => { expect(getRichTextEntityLinks(document)).toEqual({ Entry: [ { linkType: 'Entry', type: 'Link', id: 'bar', }, { linkType: 'Entry', type: 'Link', id: 'baz', }, { linkType: 'Entry', type: 'Link', id: 'inline-header-entry', }, { linkType: 'Entry', type: 'Link', id: 'hyperlink-cell-entry', }, ], Asset: [ { linkType: 'Asset', type: 'Link', id: 'quux', }, { linkType: 'Asset', type: 'Link', id: 'qux', }, { linkType: 'Asset', type: 'Link', id: 'hyperlink-cell-asset', }, ], }); }); }); describe('handling redundant links', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_ENTRY, data: { target: { sys: { linkType: 'Entry', type: 'Link', id: 'foo', }, }, }, content: [], }, { nodeType: BLOCKS.EMBEDDED_ENTRY, data: { target: { sys: { linkType: 'Entry', type: 'Link', id: 'foo', }, }, }, content: [], }, { nodeType: BLOCKS.EMBEDDED_ASSET, data: { target: { sys: { linkType: 'Asset', type: 'Link', id: 'bar', }, }, }, content: [], }, { nodeType: BLOCKS.EMBEDDED_ASSET, data: { target: { sys: { linkType: 'Asset', type: 'Link', id: 'bar', }, }, }, content: [], }, { nodeType: BLOCKS.OL_LIST, data: {}, content: [ { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_ENTRY, data: { target: { sys: { linkType: 'Entry', type: 'Link', id: 'foo', }, }, }, content: [], }, { nodeType: BLOCKS.EMBEDDED_ASSET, data: { target: { sys: { linkType: 'Asset', type: 'Link', id: 'bar', }, }, }, content: [], }, ], }, ], }, ], }, ], }; it('ignores all redundant links', () => { expect(getRichTextEntityLinks(document)).toEqual({ Entry: [ { linkType: 'Entry', type: 'Link', id: 'foo', }, ], Asset: [ { linkType: 'Asset', type: 'Link', id: 'bar', }, ], }); }); }); describe('filtering links of given type', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_ENTRY, data: { target: { sys: { linkType: 'Entry', type: 'Link', id: 'foo', }, }, }, content: [], }, { nodeType: BLOCKS.EMBEDDED_ASSET, data: { target: { sys: { linkType: 'Asset', type: 'Link', id: 'bar', }, }, }, content: [], }, ], }, ], }; it('ignores all links of different types', () => { expect(getRichTextEntityLinks(document, BLOCKS.EMBEDDED_ENTRY)).toEqual({ Entry: [ { linkType: 'Entry', type: 'Link', id: 'foo', }, ], Asset: [], }); }); }); }); describe(`getRichTextResourceLinks`, () => { it('returns top-level rich text resource-links', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('foo', 'bar'), }, content: [], }, ], }, ], }; expect(getRichTextResourceLinks(document, BLOCKS.EMBEDDED_RESOURCE)).toEqual([ makeResourceLink('foo', 'bar'), ]); }); it('returns an empty array if document parameter is `null`', () => { const documentThatIsNull: Maybe = null; expect(getRichTextResourceLinks(documentThatIsNull, BLOCKS.EMBEDDED_RESOURCE)).toEqual([]); }); it('returns an empty array if document parameter is `undefined`', () => { const documentThatIsUndefined: Maybe = undefined; expect(getRichTextResourceLinks(documentThatIsUndefined, BLOCKS.EMBEDDED_RESOURCE)).toEqual([]); }); it(`returns all ResourceLinks from multiple levels in the same order as defined in the document`, () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-1'), }, content: [], }, { nodeType: BLOCKS.OL_LIST, data: {}, content: [ { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-2'), }, content: [], }, ], }, ], }, ], }, { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-2', 'entry-1'), }, content: [], }, ], }, ], }, ], }, { nodeType: BLOCKS.TABLE, data: {}, content: [ { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_HEADER_CELL, data: {}, content: [{ data: {}, content: [], nodeType: BLOCKS.PARAGRAPH }], }, { nodeType: BLOCKS.TABLE_HEADER_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-2', 'entry-2'), }, content: [], }, ], }, ], }, ], }, { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-2', 'entry-3'), }, content: [], }, ], }, ], }, ], }, ], }, ], }; expect(getRichTextResourceLinks(document, BLOCKS.EMBEDDED_RESOURCE)).toEqual([ makeResourceLink('space-1', 'entry-1'), makeResourceLink('space-1', 'entry-2'), makeResourceLink('space-2', 'entry-1'), makeResourceLink('space-2', 'entry-2'), makeResourceLink('space-2', 'entry-3'), ]); }); it(`de-duplicates links based on urn`, () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-1'), }, content: [], }, { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-2'), }, content: [], }, { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-1'), }, content: [], }, ], }, ], }; expect(getRichTextResourceLinks(document, BLOCKS.EMBEDDED_RESOURCE)).toEqual([ makeResourceLink('space-1', 'entry-1'), makeResourceLink('space-1', 'entry-2'), ]); }); it(`should return duplicate links if the deduplicate option value is passed as false`, () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-1'), }, content: [], }, { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-2'), }, content: [], }, { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-1'), }, content: [], }, ], }, ], }; expect( getRichTextResourceLinks(document, BLOCKS.EMBEDDED_RESOURCE, { deduplicate: false }), ).toEqual([ makeResourceLink('space-1', 'entry-1'), makeResourceLink('space-1', 'entry-2'), makeResourceLink('space-1', 'entry-1'), ]); }); }); describe(`getAllRichTextResourceLinks`, () => { it('returns top-level rich text resource-links', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('foo', 'bar'), }, content: [], }, { nodeType: INLINES.RESOURCE_HYPERLINK, data: { target: makeResourceLink('foo2', 'bar'), }, content: [], }, { nodeType: INLINES.EMBEDDED_RESOURCE, data: { target: makeResourceLink('foo3', 'bar'), }, content: [], }, ], }, ], }; expect(getAllRichTextResourceLinks(document)).toEqual([ makeResourceLink('foo', 'bar'), makeResourceLink('foo2', 'bar'), makeResourceLink('foo3', 'bar'), ]); }); it('returns an empty array if document parameter is `null`', () => { const documentThatIsNull: Maybe = null; expect(getAllRichTextResourceLinks(documentThatIsNull)).toEqual([]); }); it('returns an empty array if document parameter is `undefined`', () => { const documentThatIsUndefined: Maybe = undefined; expect(getAllRichTextResourceLinks(documentThatIsUndefined)).toEqual([]); }); it(`returns all ResourceLinks from multiple levels in the same order as defined in the document`, () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-1'), }, content: [], }, { nodeType: BLOCKS.OL_LIST, data: {}, content: [ { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-2'), }, content: [], }, ], }, ], }, ], }, { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-2', 'entry-1'), }, content: [], }, ], }, ], }, ], }, { nodeType: BLOCKS.TABLE, data: {}, content: [ { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_HEADER_CELL, data: {}, content: [{ data: {}, content: [], nodeType: BLOCKS.PARAGRAPH }], }, { nodeType: BLOCKS.TABLE_HEADER_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-2', 'entry-2'), }, content: [], }, { nodeType: INLINES.RESOURCE_HYPERLINK, data: { target: makeResourceLink('space-2', 'entry'), }, content: [], }, { nodeType: INLINES.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-3', 'entry'), }, content: [], }, ], }, ], }, ], }, { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-2', 'entry-3'), }, content: [], }, ], }, ], }, ], }, ], }, ], }; expect(getAllRichTextResourceLinks(document)).toEqual([ makeResourceLink('space-1', 'entry-1'), makeResourceLink('space-1', 'entry-2'), makeResourceLink('space-2', 'entry-1'), makeResourceLink('space-2', 'entry-2'), makeResourceLink('space-2', 'entry'), makeResourceLink('space-3', 'entry'), makeResourceLink('space-2', 'entry-3'), ]); }); it(`de-duplicates links based on urn`, () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-1'), }, content: [], }, { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-2'), }, content: [], }, { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-1'), }, content: [], }, { nodeType: INLINES.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-2', 'entry-1'), }, content: [], }, { nodeType: INLINES.RESOURCE_HYPERLINK, data: { target: makeResourceLink('space-2', 'entry-1'), }, content: [], }, ], }, ], }; expect(getAllRichTextResourceLinks(document)).toEqual([ makeResourceLink('space-1', 'entry-1'), makeResourceLink('space-1', 'entry-2'), makeResourceLink('space-2', 'entry-1'), ]); }); it(`should return duplicate links if the deduplicate option value is passed as false`, () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-1'), }, content: [], }, { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-2'), }, content: [], }, { nodeType: BLOCKS.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-1', 'entry-1'), }, content: [], }, { nodeType: INLINES.EMBEDDED_RESOURCE, data: { target: makeResourceLink('space-2', 'entry-1'), }, content: [], }, { nodeType: INLINES.RESOURCE_HYPERLINK, data: { target: makeResourceLink('space-2', 'entry-1'), }, content: [], }, ], }, ], }; expect(getAllRichTextResourceLinks(document, { deduplicate: false })).toEqual([ makeResourceLink('space-1', 'entry-1'), makeResourceLink('space-1', 'entry-2'), makeResourceLink('space-1', 'entry-1'), makeResourceLink('space-2', 'entry-1'), makeResourceLink('space-2', 'entry-1'), ]); }); }); ================================================ FILE: packages/rich-text-links/src/index.ts ================================================ import { Block, BLOCKS, Document, Inline, INLINES, Link, Node, NodeData, ResourceLink, } from '@contentful/rich-text-types'; import { Maybe } from './types/utils'; export type EntityLinks = { [type in EntityType]: EntityLink[] }; export type EntityLinkMaps = { [type in EntityType]: Map }; export type EntityType = 'Entry' | 'Asset'; export type EntityLink = SysObject['sys']; export type SysObject = Link ; export type EntityLinkNodeData = { target: SysObject; }; // spare upstream dependencies the need to use rich-text-types type AcceptedResourceLinkTypes = | `${BLOCKS.EMBEDDED_RESOURCE}` | `${INLINES.EMBEDDED_RESOURCE}` | `${INLINES.RESOURCE_HYPERLINK}`; /** * Extracts all links no matter the entity they are pointing to. */ export function getRichTextResourceLinks( document: Maybe , nodeType: AcceptedResourceLinkTypes, { deduplicate = true }: { deduplicate?: boolean } = {}, ): ResourceLink[] { const isValidType = (actualNodeType: string, data: NodeData) => actualNodeType === nodeType && data.target?.sys?.type === 'ResourceLink'; return getValidResourceLinks(document, isValidType, deduplicate); } export function getAllRichTextResourceLinks( document: Maybe , { deduplicate = true }: { deduplicate?: boolean } = {}, ): ResourceLink[] { const nodeTypes: string[] = [ BLOCKS.EMBEDDED_RESOURCE, INLINES.EMBEDDED_RESOURCE, INLINES.RESOURCE_HYPERLINK, ]; const isValidType = (actualNodeType: string, data: NodeData) => nodeTypes.includes(actualNodeType) && data.target?.sys?.type === 'ResourceLink'; return getValidResourceLinks(document, isValidType, deduplicate); } function getValidResourceLinks( document: Maybe , isValidType: (actualNodeType: string, data: NodeData) => boolean, deduplicate: boolean, ): ResourceLink[] { const links = new Map (); visitNodes(document, (node) => { if (isValidType(node.nodeType, node.data)) { const key = deduplicate ? node.data.target.sys.urn : links.size; links.set(key, node.data.target); } }); return iteratorToArray(links.values()); } /** * Extracts entity links from a Rich Text document. */ export function getRichTextEntityLinks( /** * An instance of a Rich Text Document. */ document: Maybe , /** * Node type. Only the entity links with given node type will be extracted. */ type?: string, ): EntityLinks { const entityLinks: EntityLinkMaps = { Entry: new Map(), Asset: new Map(), }; // const content = (document && document.content) || ([] as Node[]); const addLink = ({ data, nodeType }: Node) => { const hasRequestedNodeType = !type || nodeType === type; if (hasRequestedNodeType && isLinkObject(data)) { entityLinks[data.target.sys.linkType].set(data.target.sys.id, data.target.sys); } }; visitNodes(document, addLink); return { Entry: iteratorToArray(entityLinks.Entry.values()), Asset: iteratorToArray(entityLinks.Asset.values()), }; } function isContentNode(node: Node): node is Inline | Block { return 'content' in node && Array.isArray(node.content); } function visitNodes(startNode: Maybe , onVisit: (node: Node) => void): void { /** * Do not remove this null check as consumers of this library do not all have good Typescript types */ const toCrawl: Node[] = startNode ? [startNode] : []; while (toCrawl.length > 0) { const node = toCrawl.pop(); onVisit(node); if (isContentNode(node)) { for (let i = node.content.length - 1; i >= 0; i--) { toCrawl.push(node.content[i]); } } } } function isLinkObject(data: NodeData): data is EntityLinkNodeData { const sys = data && (data.target as SysObject) && (data.target.sys as EntityLink); if (!sys) { return false; } const isValidLinkObject = typeof sys.id === 'string' && sys.type === 'Link'; if (!isValidLinkObject) { return false; } const isEntityLink = sys.linkType === 'Entry' || sys.linkType === 'Asset'; return isEntityLink; } /** * Used to convert the EntityLink iterators stored by the EntityLinkMap values * into a client-friendly array form. * * Alternately we could do: * 1) Array.from(EntityLinkMap) * 2) [...EntityLinkMap] * * #1, although idiomatic, is about half as slow as #2. * * #2, while faster than #1, requires transpilation of the iterator protocol[1], * which in turn is still only about half as fast as the approach below. * * [1] See https://blog.mariusschulz.com/2017/06/30/typescript-2-3-downlevel-iteration-for-es3-es5. */ function iteratorToArray (iterator: IterableIterator ): T[] { const result = []; // eslint-disable-next-line no-constant-condition while (true) { const { value, done } = iterator.next(); if (done) { break; } result.push(value); } return result; } ================================================ FILE: packages/rich-text-links/src/types/utils.ts ================================================ export type Maybe = T | null | undefined; ================================================ FILE: packages/rich-text-links/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "declarationDir": "dist/types", "outDir": "dist/lib", "typeRoots": ["../../node_modules/@types", "node_modules/@types", "src/typings"] }, "include": ["src"] } ================================================ FILE: packages/rich-text-plain-text-renderer/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [17.2.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@17.2.0...@contentful/rich-text-plain-text-renderer@17.2.1) (2026-04-09) ### Bug Fixes - **rich-text-types:** improve esm compability [ZEND-7778] ([#1073](https://github.com/contentful/rich-text/issues/1073)) ([204aaec](https://github.com/contentful/rich-text/commit/204aaecc3893633c081986f44896e14272fa376a)) # [17.2.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@17.1.6...@contentful/rich-text-plain-text-renderer@17.2.0) (2026-04-08) ### Features - add esm support [ZEND-7778] ([#1069](https://github.com/contentful/rich-text/issues/1069)) ([8c320bd](https://github.com/contentful/rich-text/commit/8c320bde7fa313572bdad84b91a6fd12224afc3a)), closes [#1068](https://github.com/contentful/rich-text/issues/1068) ## [17.1.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@17.1.5...@contentful/rich-text-plain-text-renderer@17.1.6) (2025-11-04) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [17.1.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@17.1.4...@contentful/rich-text-plain-text-renderer@17.1.5) (2025-09-23) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [17.1.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@17.1.3...@contentful/rich-text-plain-text-renderer@17.1.4) (2025-09-23) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [17.1.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@17.1.2...@contentful/rich-text-plain-text-renderer@17.1.3) (2025-09-10) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [17.1.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@17.1.1...@contentful/rich-text-plain-text-renderer@17.1.2) (2025-09-09) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [17.1.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@17.1.0...@contentful/rich-text-plain-text-renderer@17.1.1) (2025-09-04) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [17.1.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@17.0.1...@contentful/rich-text-plain-text-renderer@17.1.0) (2025-07-15) ### Features - add an option to strip empty trailing paragraphs [TOL-3193] ([#892](https://github.com/contentful/rich-text/issues/892)) ([9beff0e](https://github.com/contentful/rich-text/commit/9beff0e4cba3e79dc68e6a0725e843c5d642eb87)) ## [17.0.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@17.0.0...@contentful/rich-text-plain-text-renderer@17.0.1) (2025-06-16) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [17.0.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.10...@contentful/rich-text-plain-text-renderer@17.0.0) (2024-10-29) ### Features - bring rich text validator [TOL-2426] ([#694](https://github.com/contentful/rich-text/issues/694)) ([30893a6](https://github.com/contentful/rich-text/commit/30893a68b171167502135b48258ba4b93c8375c7)) ### BREAKING CHANGES - removed getSchemaWithNodeType in favor of validateRichTextDocument ## [16.2.10](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.9...@contentful/rich-text-plain-text-renderer@16.2.10) (2024-09-09) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.2.9](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.8...@contentful/rich-text-plain-text-renderer@16.2.9) (2024-08-26) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.2.8](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.7...@contentful/rich-text-plain-text-renderer@16.2.8) (2024-07-29) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.2.7](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.6...@contentful/rich-text-plain-text-renderer@16.2.7) (2024-07-24) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.2.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.5...@contentful/rich-text-plain-text-renderer@16.2.6) (2024-07-17) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.2.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.4...@contentful/rich-text-plain-text-renderer@16.2.5) (2024-07-17) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.2.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.3...@contentful/rich-text-plain-text-renderer@16.2.4) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.2.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.2...@contentful/rich-text-plain-text-renderer@16.2.3) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.2.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.1...@contentful/rich-text-plain-text-renderer@16.2.2) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.2.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.2.0...@contentful/rich-text-plain-text-renderer@16.2.1) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [16.2.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.1.5...@contentful/rich-text-plain-text-renderer@16.2.0) (2024-06-25) ### Features - switch from tslint to eslint [TOL-2218][tol-2203] ([#594](https://github.com/contentful/rich-text/issues/594)) ([c077b5a](https://github.com/contentful/rich-text/commit/c077b5af58f94c8dc6af4715d4b82c2771d643c5)) ## [16.1.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.1.4...@contentful/rich-text-plain-text-renderer@16.1.5) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.1.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.1.3...@contentful/rich-text-plain-text-renderer@16.1.4) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.1.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.1.2...@contentful/rich-text-plain-text-renderer@16.1.3) (2024-05-28) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.1.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.1.1...@contentful/rich-text-plain-text-renderer@16.1.2) (2024-05-27) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.1.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.1.0...@contentful/rich-text-plain-text-renderer@16.1.1) (2024-05-24) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [16.1.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.13...@contentful/rich-text-plain-text-renderer@16.1.0) (2024-05-22) ### Features - upgrading rollup to latest version [TOL-2097] ([#559](https://github.com/contentful/rich-text/issues/559)) ([f14d197](https://github.com/contentful/rich-text/commit/f14d1974590d58f92bbf4cb56644095dba929ad9)) ## [16.0.13](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.12...@contentful/rich-text-plain-text-renderer@16.0.13) (2024-03-06) ### Bug Fixes - correctly handle edge case [ZEND-4744] ([#541](https://github.com/contentful/rich-text/issues/541)) ([819b07d](https://github.com/contentful/rich-text/commit/819b07da3a00e6d8101e49cb0e678e3a21b4cd93)) ## [16.0.12](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.11...@contentful/rich-text-plain-text-renderer@16.0.12) (2024-03-04) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.11](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.10...@contentful/rich-text-plain-text-renderer@16.0.11) (2024-01-30) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.10](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.9...@contentful/rich-text-plain-text-renderer@16.0.10) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.9](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.8...@contentful/rich-text-plain-text-renderer@16.0.9) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.8](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.7...@contentful/rich-text-plain-text-renderer@16.0.8) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.7](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.6...@contentful/rich-text-plain-text-renderer@16.0.7) (2023-09-12) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.5...@contentful/rich-text-plain-text-renderer@16.0.6) (2023-08-04) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.4...@contentful/rich-text-plain-text-renderer@16.0.5) (2023-05-26) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.3...@contentful/rich-text-plain-text-renderer@16.0.4) (2023-05-04) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.2...@contentful/rich-text-plain-text-renderer@16.0.3) (2023-03-14) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.1...@contentful/rich-text-plain-text-renderer@16.0.2) (2022-12-01) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [16.0.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-plain-text-renderer@16.0.0...@contentful/rich-text-plain-text-renderer@16.0.1) (2022-12-01) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # 16.0.0 (2022-12-01) ## 15.15.1 (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # 15.15.0 (2022-11-29) ## 15.14.1 (2022-11-23) # 15.14.0 (2022-11-14) ## 15.13.2 (2022-09-07) ### Bug Fixes - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) ## 15.12.1 (2022-04-21) # 15.12.0 (2022-03-25) ## 15.11.1 (2022-01-04) # 15.11.0 (2021-12-27) ## 15.10.1 (2021-12-21) # 15.10.0 (2021-12-15) ## 15.9.1 (2021-12-10) # 15.9.0 (2021-12-09) # 15.7.0 (2021-11-11) ## 15.6.2 (2021-11-05) ## 15.6.1 (2021-11-05) # 15.6.0 (2021-11-04) ## 15.5.1 (2021-10-25) # 15.5.0 (2021-10-25) ## 15.3.6 (2021-09-15) ## 15.3.5 (2021-09-13) ## 15.3.3 (2021-09-07) ## 15.3.2 (2021-09-07) ## 15.3.1 (2021-09-07) # 15.3.0 (2021-09-06) # 15.1.0 (2021-08-02) # 15.0.0 (2021-06-15) ## 14.1.2 (2020-11-02) ## 14.0.1 (2020-01-30) # 14.0.0 (2020-01-28) # 13.4.0 (2019-08-01) # 13.1.0 (2019-03-04) # 13.0.0 (2019-01-22) ### Bug Fixes - 🐛 use named export instead of default ([57bf365](https://github.com/contentful/rich-text/commit/57bf36510f1021f5208a3e33242cceea97940d68)) ### BREAKING CHANGES - removes default export from 'rich-text-plain-text-renderer' ## 12.1.2 (2018-12-14) ## 12.0.3 (2018-12-05) ### Bug Fixes - **readme:** mark types in readme examples ([d997b56](https://github.com/contentful/rich-text/commit/d997b56f2b8c32b2e5b478ab5444757203e2c703)) ## 12.0.2 (2018-12-04) ### Bug Fixes - 🐛 update path to typings ([ce5544f](https://github.com/contentful/rich-text/commit/ce5544f58712dc6a18aadda523d0c0357a66c8a5)) ## 12.0.1 (2018-12-04) # 12.0.0 (2018-11-29) # 11.0.0 (2018-11-27) # 10.3.0 (2018-11-26) # 10.2.0 (2018-11-19) ### Features - 🎸 Add rich-text-references ([363b4e5](https://github.com/contentful/rich-text/commit/363b4e509e94af0932fd7cece8e56beafe8d67d2)) # 10.1.0 (2018-11-16) ## 10.0.5 (2018-11-12) ### Bug Fixes - 🐛 handle case of missing rootNode / rootNode.content ([e2434c7](https://github.com/contentful/rich-text/commit/e2434c7f5e1401f1188dd778c2aa9a108cea5596)) ## 10.0.1 (2018-11-08) # 10.0.0 (2018-11-02) ### Features - 🎸 removes nodeClass from Node interface ([09b8162](https://github.com/contentful/rich-text/commit/09b8162bcba65bc13afa14b2b5ff046c9fed7b3b)) ### BREAKING CHANGES - Removes accidentally added nodeClass ## 9.0.2 (2018-10-31) ## 9.0.1 (2018-10-31) # 9.0.0 (2018-10-30) ### Bug Fixes - 🐛 Fix block divisor provision ([3aafb7a](https://github.com/contentful/rich-text/commit/3aafb7a756d8b6f96ef733eb299091404b3391aa)) - 🐛 remove exclusive mocha test ([5ff5d0e](https://github.com/contentful/rich-text/commit/5ff5d0e35024b7dccda065f54ae34138d9416525)) ### BREAKING CHANGES - Fundamentally alters the plain text renderer behavior. (For the better!) ## 8.0.3 (2018-10-30) ## 8.0.2 (2018-10-29) ## 8.0.1 (2018-10-26) ### Features - 🎸 take all packages out of "demo mode" ([815f18b](https://github.com/contentful/rich-text/commit/815f18be6a914e7e4782790ee46053689712494b)) ### BREAKING CHANGES - renames all packages # 6.0.0 (2018-10-24) ## [15.15.1](https://github.com/contentful/rich-text/compare/v15.15.0...v15.15.1) (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # [15.15.0](https://github.com/contentful/rich-text/compare/v15.14.1...v15.15.0) (2022-11-29) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.14.1](https://github.com/contentful/rich-text/compare/v15.14.0...v15.14.1) (2022-11-23) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [15.14.0](https://github.com/contentful/rich-text/compare/v15.13.2...v15.14.0) (2022-11-14) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.13.2](https://github.com/contentful/rich-text/compare/v15.13.1...v15.13.2) (2022-09-07) ### Bug Fixes - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) ## [15.12.1](https://github.com/contentful/rich-text/compare/v15.12.0...v15.12.1) (2022-04-21) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [15.12.0](https://github.com/contentful/rich-text/compare/v15.11.2...v15.12.0) (2022-03-25) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.11.1](https://github.com/contentful/rich-text/compare/v15.11.0...v15.11.1) (2022-01-04) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [15.11.0](https://github.com/contentful/rich-text/compare/v15.10.1...v15.11.0) (2021-12-27) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.10.1](https://github.com/contentful/rich-text/compare/v15.10.0...v15.10.1) (2021-12-21) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [15.10.0](https://github.com/contentful/rich-text/compare/v15.9.1...v15.10.0) (2021-12-15) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.9.1](https://github.com/contentful/rich-text/compare/v15.9.0...v15.9.1) (2021-12-10) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [15.9.0](https://github.com/contentful/rich-text/compare/v15.8.0...v15.9.0) (2021-12-09) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [15.7.0](https://github.com/contentful/rich-text/compare/v15.6.2...v15.7.0) (2021-11-11) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.6.2](https://github.com/contentful/rich-text/compare/v15.6.1...v15.6.2) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.6.1](https://github.com/contentful/rich-text/compare/v15.6.0...v15.6.1) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [15.6.0](https://github.com/contentful/rich-text/compare/v15.5.1...v15.6.0) (2021-11-04) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.5.1](https://github.com/contentful/rich-text/compare/v15.5.0...v15.5.1) (2021-10-25) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [15.5.0](https://github.com/contentful/rich-text/compare/v15.4.0...v15.5.0) (2021-10-25) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.3.6](https://github.com/contentful/rich-text/compare/v15.3.5...v15.3.6) (2021-09-15) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.3.5](https://github.com/contentful/rich-text/compare/v15.3.4...v15.3.5) (2021-09-13) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.3.3](https://github.com/contentful/rich-text/compare/v15.3.2...v15.3.3) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.3.2](https://github.com/contentful/rich-text/compare/v15.3.1...v15.3.2) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer ## [15.3.1](https://github.com/contentful/rich-text/compare/v15.3.0...v15.3.1) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [15.3.0](https://github.com/contentful/rich-text/compare/v15.2.0...v15.3.0) (2021-09-06) **Note:** Version bump only for package @contentful/rich-text-plain-text-renderer # [15.2.0](https://github.com/contentful/rich-text/compare/v15.2.0...v15.1.0) (2021-09-06) # [15.1.0](https://github.com/contentful/rich-text/compare/v15.0.0...v15.1.0) (2021-08-02) # [15.0.0](https://github.com/contentful/rich-text/compare/v14.2.0...v15.0.0) (2021-06-15) ## [14.1.2](https://github.com/contentful/rich-text/compare/v14.0.1...v14.1.2) (2020-11-02) ## [14.0.1](https://github.com/contentful/rich-text/compare/v14.0.0...v14.0.1) (2020-01-30) # [14.0.0](https://github.com/contentful/rich-text/compare/v13.4.0...v14.0.0) (2020-01-28) # [13.4.0](https://github.com/contentful/rich-text/compare/v13.3.0...v13.4.0) (2019-08-01) # [13.1.0](https://github.com/contentful/rich-text/compare/v13.0.1...v13.1.0) (2019-03-04) # [13.0.0](https://github.com/contentful/rich-text/compare/v12.2.1...v13.0.0) (2019-01-22) ### Bug Fixes - 🐛 use named export instead of default ([57bf365](https://github.com/contentful/rich-text/commit/57bf36510f1021f5208a3e33242cceea97940d68)) ### BREAKING CHANGES - removes default export from 'rich-text-plain-text-renderer' ## [12.1.2](https://github.com/contentful/rich-text/compare/v12.1.1...v12.1.2) (2018-12-14) ## [12.0.3](https://github.com/contentful/rich-text/compare/v12.0.2...v12.0.3) (2018-12-05) ### Bug Fixes - **readme:** mark types in readme examples ([d997b56](https://github.com/contentful/rich-text/commit/d997b56f2b8c32b2e5b478ab5444757203e2c703)) ## [12.0.2](https://github.com/contentful/rich-text/compare/v12.0.1...v12.0.2) (2018-12-04) ### Bug Fixes - 🐛 update path to typings ([ce5544f](https://github.com/contentful/rich-text/commit/ce5544f58712dc6a18aadda523d0c0357a66c8a5)) ## [12.0.1](https://github.com/contentful/rich-text/compare/v12.0.0...v12.0.1) (2018-12-04) # [12.0.0](https://github.com/contentful/rich-text/compare/v11.0.0...v12.0.0) (2018-11-29) # [11.0.0](https://github.com/contentful/rich-text/compare/v10.3.0...v11.0.0) (2018-11-27) # [10.3.0](https://github.com/contentful/rich-text/compare/v10.2.0...v10.3.0) (2018-11-26) # [10.2.0](https://github.com/contentful/rich-text/compare/v10.1.0...v10.2.0) (2018-11-19) ### Features - 🎸 Add rich-text-references ([363b4e5](https://github.com/contentful/rich-text/commit/363b4e509e94af0932fd7cece8e56beafe8d67d2)) # [10.1.0](https://github.com/contentful/rich-text/compare/v10.0.5...v10.1.0) (2018-11-16) ## [10.0.5](https://github.com/contentful/rich-text/compare/v10.0.4...v10.0.5) (2018-11-12) ### Bug Fixes - 🐛 handle case of missing rootNode / rootNode.content ([e2434c7](https://github.com/contentful/rich-text/commit/e2434c7f5e1401f1188dd778c2aa9a108cea5596)) ## [10.0.1](https://github.com/contentful/rich-text/compare/v10.0.0...v10.0.1) (2018-11-08) # [10.0.0](https://github.com/contentful/rich-text/compare/v9.0.2...v10.0.0) (2018-11-02) ### Features - 🎸 removes nodeClass from Node interface ([09b8162](https://github.com/contentful/rich-text/commit/09b8162bcba65bc13afa14b2b5ff046c9fed7b3b)) ### BREAKING CHANGES - Removes accidentally added nodeClass ## [9.0.2](https://github.com/contentful/rich-text/compare/v9.0.1...v9.0.2) (2018-10-31) ## [9.0.1](https://github.com/contentful/rich-text/compare/v9.0.0...v9.0.1) (2018-10-31) # [9.0.0](https://github.com/contentful/rich-text/compare/v8.0.3...v9.0.0) (2018-10-30) ### Bug Fixes - 🐛 Fix block divisor provision ([3aafb7a](https://github.com/contentful/rich-text/commit/3aafb7a756d8b6f96ef733eb299091404b3391aa)) - 🐛 remove exclusive mocha test ([5ff5d0e](https://github.com/contentful/rich-text/commit/5ff5d0e35024b7dccda065f54ae34138d9416525)) ### BREAKING CHANGES - Fundamentally alters the plain text renderer behavior. (For the better!) ## [8.0.3](https://github.com/contentful/rich-text/compare/v8.0.2...v8.0.3) (2018-10-30) ## [8.0.2](https://github.com/contentful/rich-text/compare/v8.0.1...v8.0.2) (2018-10-29) ## [8.0.1](https://github.com/contentful/rich-text/compare/v8.0.0...v8.0.1) (2018-10-26) ### Features - 🎸 take all packages out of "demo mode" ([815f18b](https://github.com/contentful/rich-text/commit/815f18be6a914e7e4782790ee46053689712494b)) ### BREAKING CHANGES - renames all packages # 6.0.0 (2018-10-24) ================================================ FILE: packages/rich-text-plain-text-renderer/README.md ================================================ # rich-text-plain-text-renderer Plain text renderer for the Rich Text document. ## Installation Using [npm](http://npmjs.org/): ```sh npm install @contentful/rich-text-plain-text-renderer ``` Using [yarn](https://yarnpkg.com/): ```sh yarn add @contentful/rich-text-plain-text-renderer ``` ## Usage ```javascript import { documentToPlainTextString } from '@contentful/rich-text-plain-text-renderer'; const document = { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'Hello', marks: [{ type: 'bold' }], data: {}, }, { nodeType: 'text', value: ' world!', marks: [{ type: 'italic' }], data: {}, }, ], }, ], }; documentToPlainTextString(document); // -> Hello world! ``` ================================================ FILE: packages/rich-text-plain-text-renderer/bin/benchmark/get-rich-text-entity-links.ts ================================================ import { BLOCKS, Document, Node } from '@contentful/rich-text-types'; import { documentToPlainTextString as docToString } from '../../src/index'; /** * Implements just a subset of _.times since all we're really doing here is * cleanly filling the field with a bunch of junk data. */ function times (n: number, node: N): N[] { return new Array(n).fill(node); } const richTextField: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: times(8000, { nodeType: BLOCKS.UL_LIST, data: {}, content: times(5, { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: times(5, { nodeType: 'text', data: {}, marks: [], value: 'x', }), }, ], }), }), }, ], }; export const documentToPlainTextString = () => docToString(richTextField, ''); ================================================ FILE: packages/rich-text-plain-text-renderer/bin/tsconfig.json ================================================ { "extends": "../../../bin/tsconfig.json" } ================================================ FILE: packages/rich-text-plain-text-renderer/jest.config.js ================================================ const getBaseConfig = require('../../baseJestConfig'); const package = require('./package.json'); const packageName = package.name.split('@contentful/')[1]; module.exports = { ...getBaseConfig(packageName), }; ================================================ FILE: packages/rich-text-plain-text-renderer/package.json ================================================ { "name": "@contentful/rich-text-plain-text-renderer", "version": "17.2.1", "main": "dist/rich-text-plain-text-renderer.es5.js", "module": "dist/rich-text-plain-text-renderer.esm.js", "typings": "dist/types/index.d.ts", "exports": { ".": { "types": "./dist/types/index.d.ts", "import": "./dist/rich-text-plain-text-renderer.esm.js", "require": "./dist/rich-text-plain-text-renderer.es5.js", "default": "./dist/rich-text-plain-text-renderer.es5.js" }, "./package.json": "./package.json" }, "files": [ "dist" ], "repository": { "type": "git", "url": "https://github.com/contentful/rich-text.git" }, "license": "MIT", "engines": { "node": ">=20.0.0" }, "publishConfig": { "access": "public", "registry": "https://npm.pkg.github.com/" }, "scripts": { "build": "tsc --module commonjs && rollup -c --bundleConfigAsCjs rollup.config.js", "prebuild": "rimraf dist", "start": "tsc && rollup -c --bundleConfigAsCjs rollup.config.js -w", "test": "jest" }, "dependencies": { "@contentful/rich-text-types": "^17.2.7" }, "devDependencies": { "ts-jest": "^29.1.2" } } ================================================ FILE: packages/rich-text-plain-text-renderer/rollup.config.js ================================================ import config from '../../rollup.config'; import { main as outputFile, dependencies } from './package.json'; export default config(outputFile, { external: Object.keys(dependencies), }); ================================================ FILE: packages/rich-text-plain-text-renderer/src/__test__/index.test.ts ================================================ import { Document, BLOCKS, INLINES } from '@contentful/rich-text-types'; import { documentToPlainTextString } from '../index'; describe('documentToPlainTextString', () => { it('returns empty string when given an empty document', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [], }; expect(documentToPlainTextString(document)).toEqual(''); }); it('handles a simple case', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, value: 'Trout is a', marks: [], }, { nodeType: 'text', data: {}, value: ' seafood d', marks: [{ type: 'italic' }], }, { nodeType: 'text', data: {}, value: 'elicacy.', marks: [], }, ], }, ], }; expect(documentToPlainTextString(document)).toEqual('Trout is a seafood delicacy.'); }); describe('rendering deeply nested documents', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'This is text. ', data: {}, marks: [{ type: 'bold' }], }, { nodeType: 'text', value: '', data: {}, marks: [], }, { nodeType: 'text', value: 'This is text with some marks.', data: {}, marks: [{ type: 'italic' }], }, { nodeType: 'text', value: ' ', data: {}, marks: [], }, { nodeType: INLINES.HYPERLINK, content: [ { nodeType: 'text', value: 'This is text from a bolded hyperlink.', data: {}, marks: [{ type: 'bold' }], }, ], data: { url: 'https://example.com', title: 'qux', }, }, ], }, { nodeType: BLOCKS.UL_LIST, data: {}, content: [ { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'This is a list element in a separate block ', data: {}, marks: [], }, { nodeType: INLINES.HYPERLINK, content: [ { nodeType: 'text', value: 'with a link with marks.', data: {}, marks: [], }, ], data: { url: 'https://google.com', title: 'woo', }, }, ], }, ], }, { nodeType: BLOCKS.LIST_ITEM, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'This is a separate list element in the same block.', data: {}, marks: [], }, ], }, ], }, ], }, ], }; it('handles nested nodes gracefully', () => { expect(documentToPlainTextString(document)).toEqual( [ 'This is text.', 'This is text with some marks.', 'This is text from a bolded hyperlink.', 'This is a list element in a separate block with a link with marks.', 'This is a separate list element in the same block.', ].join(' '), ); }); it('defers to the user-supplied block divisor', () => { expect(documentToPlainTextString(document, '\n\n')).toEqual( [ 'This is text. This is text with some marks. This is text from a bolded hyperlink.', 'This is a list element in a separate block with a link with marks.', 'This is a separate list element in the same block.', ].join('\n\n'), ); }); }); }); describe('stripEmptyTrailingParagraph', () => { it('strips empty trailing paragraph when enabled', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options = { stripEmptyTrailingParagraph: true, }; const result = documentToPlainTextString(document, ' ', options); expect(result).toEqual('Hello world'); }); it('does not strip empty trailing paragraph when disabled', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options = { stripEmptyTrailingParagraph: false, }; const result = documentToPlainTextString(document, ' ', options); expect(result).toEqual('Hello world '); }); it('does not strip empty trailing paragraph when it is the only child', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options = { stripEmptyTrailingParagraph: true, }; const result = documentToPlainTextString(document, ' ', options); expect(result).toEqual(''); }); it('does not strip non-empty trailing paragraph', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Not empty', marks: [], data: {}, }, ], }, ], }; const options = { stripEmptyTrailingParagraph: true, }; const result = documentToPlainTextString(document, ' ', options); expect(result).toEqual('Hello world Not empty'); }); it('does not strip trailing paragraph with multiple text nodes', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options = { stripEmptyTrailingParagraph: true, }; const result = documentToPlainTextString(document, ' ', options); expect(result).toEqual('Hello world '); }); it('does not strip trailing non-paragraph node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.HEADING_1, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options = { stripEmptyTrailingParagraph: true, }; const result = documentToPlainTextString(document, ' ', options); expect(result).toEqual('Hello world '); }); }); ================================================ FILE: packages/rich-text-plain-text-renderer/src/index.ts ================================================ import { Block, Inline, Node, helpers, BLOCKS, Document } from '@contentful/rich-text-types'; export interface Options { /** * Strip empty trailing paragraph from the document */ stripEmptyTrailingParagraph?: boolean; } /** * Returns the text value of a rich text document. * * NB: This can be applied to non text node of a structured text document, * hence the flexible typing. */ export function documentToPlainTextString( rootNode: Block | Inline, blockDivisor: string = ' ', options: Options = {}, ): string { if (!rootNode || !rootNode.content || !Array.isArray(rootNode.content)) { /** * Handles edge cases, such as when the value is not set in the CMA or the * field has not been properly validated, e.g. because of a user extension. * Note that we are nevertheless strictly type-casting `rootNode` as * Block | Inline. Valid rich text documents (and their branch block nodes) * should never lack a Node[] `content` property. */ return ''; } // Strip empty trailing paragraph if enabled and rootNode is a Document let processedRootNode = rootNode; if (rootNode.nodeType === BLOCKS.DOCUMENT && options.stripEmptyTrailingParagraph) { processedRootNode = helpers.stripEmptyTrailingParagraphFromDocument(rootNode as Document); } /** * Algorithm notes: We only want to apply spacing when a node is part of a * sequence. This is tricky because nodes can often be deeply nested within * non-semantic content arrays. For example, to get the text value of an * unordered list, we have to traverse like so: * * { * nodeType: BLOCKS.UL_LIST, * data: {}, * content: [ * { * nodeType: BLOCKS.LIST_ITEM, * data: {}, * content: [{ * nodeType: BLOCKS.PARAGRAPH, * data: {}, * content: [ * { nodeType: 'text', data: {}, value: 'List ', marks: [] }, * { nodeType: 'text', data: {}, value: 'item', marks: [{ type: 'bold' }] } * ] * }] * }, * { * nodeType: BLOCKS.LIST_ITEM, * data: {}, * content: [{ * nodeType: BLOCKS.PARAGRAPH, * data: {}, * content: [ * { nodeType: 'text', data: {}, value: 'Another list item', marks: [] } * ] * }] * }, * { * nodeType: BLOCKS.LIST_ITEM, * data: {}, * content: [{ * nodeType: BLOCKS.HR, * data: {}, * content: [], * }] * }, * { * nodeType: BLOCKS.LIST_ITEM, * data: {}, * content: [{ * nodeType: BLOCKS.PARAGRAPH, * data: * content: [ * { nodeType: 'text', data: {}, value: 'Yet another list item', marks: [] } * ] * }] * }, * }] * } * * We want there to be a space between 'List item' and 'Another list item' (to * denote a visual line break, which conventionally appears between non-text * node sequences) but not a redundant space between 'List ' and 'item'. * Moreover, we want just a _singular_ space between 'Another list item' and * 'Yet another list item' - the non-semantic HR between the two nodes should * not denote an additional space. */ return (processedRootNode as Block).content.reduce( (acc: string, node: Node, i: number): string => { let nodeTextValue: string; if (helpers.isText(node)) { nodeTextValue = node.value; } else if (helpers.isBlock(node) || helpers.isInline(node)) { nodeTextValue = documentToPlainTextString(node, blockDivisor, options); if (!nodeTextValue.length) { return acc; } } const nextNode = processedRootNode.content[i + 1]; const isNextNodeBlock = nextNode && helpers.isBlock(nextNode); const divisor = isNextNodeBlock ? blockDivisor : ''; return acc + nodeTextValue + divisor; }, '', ); } ================================================ FILE: packages/rich-text-plain-text-renderer/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "declarationDir": "dist/types", "outDir": "dist/lib", "typeRoots": ["../../node_modules/@types", "node_modules/@types", "src/typings"] }, "include": ["src"] } ================================================ FILE: packages/rich-text-react-renderer/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [16.2.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.2.0...@contentful/rich-text-react-renderer@16.2.1) (2026-04-09) ### Bug Fixes - **rich-text-types:** improve esm compability [ZEND-7778] ([#1073](https://github.com/contentful/rich-text/issues/1073)) ([204aaec](https://github.com/contentful/rich-text/commit/204aaecc3893633c081986f44896e14272fa376a)) # [16.2.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.1.6...@contentful/rich-text-react-renderer@16.2.0) (2026-04-08) ### Features - add esm support [ZEND-7778] ([#1069](https://github.com/contentful/rich-text/issues/1069)) ([8c320bd](https://github.com/contentful/rich-text/commit/8c320bde7fa313572bdad84b91a6fd12224afc3a)), closes [#1068](https://github.com/contentful/rich-text/issues/1068) ## [16.1.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.1.5...@contentful/rich-text-react-renderer@16.1.6) (2025-11-04) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [16.1.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.1.4...@contentful/rich-text-react-renderer@16.1.5) (2025-09-23) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [16.1.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.1.3...@contentful/rich-text-react-renderer@16.1.4) (2025-09-23) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [16.1.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.1.2...@contentful/rich-text-react-renderer@16.1.3) (2025-09-10) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [16.1.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.1.1...@contentful/rich-text-react-renderer@16.1.2) (2025-09-09) ### Bug Fixes - bundle issues after swc migration [TOL-3396] ([#924](https://github.com/contentful/rich-text/issues/924)) ([22245f0](https://github.com/contentful/rich-text/commit/22245f0648b0164ad29dd0dfe789cd83f78283e8)) ## [16.1.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.1.0...@contentful/rich-text-react-renderer@16.1.1) (2025-09-04) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [16.1.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.0.2...@contentful/rich-text-react-renderer@16.1.0) (2025-07-15) ### Features - add an option to strip empty trailing paragraphs [TOL-3193] ([#892](https://github.com/contentful/rich-text/issues/892)) ([9beff0e](https://github.com/contentful/rich-text/commit/9beff0e4cba3e79dc68e6a0725e843c5d642eb87)) ## [16.0.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.0.1...@contentful/rich-text-react-renderer@16.0.2) (2025-06-16) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [16.0.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@16.0.0...@contentful/rich-text-react-renderer@16.0.1) (2024-12-11) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [16.0.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.11...@contentful/rich-text-react-renderer@16.0.0) (2024-10-29) ### Features - bring rich text validator [TOL-2426] ([#694](https://github.com/contentful/rich-text/issues/694)) ([30893a6](https://github.com/contentful/rich-text/commit/30893a68b171167502135b48258ba4b93c8375c7)) ### BREAKING CHANGES - removed getSchemaWithNodeType in favor of validateRichTextDocument ## [15.22.11](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.10...@contentful/rich-text-react-renderer@15.22.11) (2024-09-09) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.22.10](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.9...@contentful/rich-text-react-renderer@15.22.10) (2024-08-26) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.22.9](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.8...@contentful/rich-text-react-renderer@15.22.9) (2024-07-29) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.22.8](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.7...@contentful/rich-text-react-renderer@15.22.8) (2024-07-24) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.22.7](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.6...@contentful/rich-text-react-renderer@15.22.7) (2024-07-17) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.22.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.5...@contentful/rich-text-react-renderer@15.22.6) (2024-07-17) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.22.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.4...@contentful/rich-text-react-renderer@15.22.5) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.22.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.3...@contentful/rich-text-react-renderer@15.22.4) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.22.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.2...@contentful/rich-text-react-renderer@15.22.3) (2024-07-16) ### Bug Fixes - preserveWhitespace option conflicting with renderText ([#618](https://github.com/contentful/rich-text/issues/618)) ([c146dc4](https://github.com/contentful/rich-text/commit/c146dc4a89fa980f877285ab2b0b9c1114236fc9)) ## [15.22.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.1...@contentful/rich-text-react-renderer@15.22.2) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.22.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.22.0...@contentful/rich-text-react-renderer@15.22.1) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.22.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.21.4...@contentful/rich-text-react-renderer@15.22.0) (2024-06-25) ### Features - switch from tslint to eslint [TOL-2218][tol-2203] ([#594](https://github.com/contentful/rich-text/issues/594)) ([c077b5a](https://github.com/contentful/rich-text/commit/c077b5af58f94c8dc6af4715d4b82c2771d643c5)) ## [15.21.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.21.3...@contentful/rich-text-react-renderer@15.21.4) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.21.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.21.2...@contentful/rich-text-react-renderer@15.21.3) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.21.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.21.1...@contentful/rich-text-react-renderer@15.21.2) (2024-05-28) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.21.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.21.0...@contentful/rich-text-react-renderer@15.21.1) (2024-05-27) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.21.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.20.0...@contentful/rich-text-react-renderer@15.21.0) (2024-05-24) ### Features - add strikethrough support ([#562](https://github.com/contentful/rich-text/issues/562)) ([b87d0c3](https://github.com/contentful/rich-text/commit/b87d0c31bccb4012745c0479b2b5c92fc28c1e91)) # [15.20.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.19.6...@contentful/rich-text-react-renderer@15.20.0) (2024-05-22) ### Features - upgrading rollup to latest version [TOL-2097] ([#559](https://github.com/contentful/rich-text/issues/559)) ([f14d197](https://github.com/contentful/rich-text/commit/f14d1974590d58f92bbf4cb56644095dba929ad9)) ## [15.19.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.19.5...@contentful/rich-text-react-renderer@15.19.6) (2024-03-04) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.19.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.19.4...@contentful/rich-text-react-renderer@15.19.5) (2024-03-04) ### Bug Fixes - 🐛 handle multiple space rendering when using preserveWhitespace ([#531](https://github.com/contentful/rich-text/issues/531)) ([982b21b](https://github.com/contentful/rich-text/commit/982b21b13736d8b1dcdb652b4eb41c7ab279ee33)), closes [#514](https://github.com/contentful/rich-text/issues/514) ## [15.19.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.19.3...@contentful/rich-text-react-renderer@15.19.4) (2024-01-30) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.19.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.19.2...@contentful/rich-text-react-renderer@15.19.3) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.19.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.19.1...@contentful/rich-text-react-renderer@15.19.2) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.19.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.19.0...@contentful/rich-text-react-renderer@15.19.1) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.19.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.18.0...@contentful/rich-text-react-renderer@15.19.0) (2023-10-24) ### Features - add default renderers for new resource nodes ([#504](https://github.com/contentful/rich-text/issues/504)) ([f748e6b](https://github.com/contentful/rich-text/commit/f748e6b0f0e22ce3809ca8881438b84b1baafb8f)) # [15.18.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.17.2...@contentful/rich-text-react-renderer@15.18.0) (2023-10-02) ### Features - preserve formatting linebreak whitespace ([#497](https://github.com/contentful/rich-text/issues/497)) ([e62ab92](https://github.com/contentful/rich-text/commit/e62ab92f6320d970f921e751d1753cd290224224)) ## [15.17.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.17.1...@contentful/rich-text-react-renderer@15.17.2) (2023-09-12) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.17.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.17.0...@contentful/rich-text-react-renderer@15.17.1) (2023-08-04) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.17.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.16.5...@contentful/rich-text-react-renderer@15.17.0) (2023-06-09) ### Features - add default renderer for the embedded-resource-block node ([#474](https://github.com/contentful/rich-text/issues/474)) ([844c011](https://github.com/contentful/rich-text/commit/844c011ef3d6f2887d82a39784c6fb5672c6e065)) ## [15.16.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.16.4...@contentful/rich-text-react-renderer@15.16.5) (2023-05-26) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.16.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.16.3...@contentful/rich-text-react-renderer@15.16.4) (2023-05-04) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.16.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.16.2...@contentful/rich-text-react-renderer@15.16.3) (2023-03-14) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.16.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.16.1...@contentful/rich-text-react-renderer@15.16.2) (2022-12-01) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.16.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-react-renderer@15.16.0...@contentful/rich-text-react-renderer@15.16.1) (2022-12-01) **Note:** Version bump only for package @contentful/rich-text-react-renderer # 15.16.0 (2022-12-01) ## 15.15.1 (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # 15.15.0 (2022-11-29) ## 15.14.1 (2022-11-23) # 15.14.0 (2022-11-14) ### Features - add super/sub script types ([#391](https://github.com/contentful/rich-text/issues/391)) ([2562f66](https://github.com/contentful/rich-text/commit/2562f66278f0eff4eeeb367025d4b465773893d1)) ## 15.13.2 (2022-09-07) ### Bug Fixes - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) ## 15.12.1 (2022-04-21) ### Bug Fixes - upgrade react peerDependencies to support react^18.0.0 ([#323](https://github.com/contentful/rich-text/issues/323)) ([7a0bfdf](https://github.com/contentful/rich-text/commit/7a0bfdfb687cca608239e072cbc301ba1b1310d1)) # 15.12.0 (2022-03-25) ## 15.11.1 (2022-01-04) # 15.11.0 (2021-12-27) ### Bug Fixes - **react-renderer:** wrap table rows in tbody ([#300](https://github.com/contentful/rich-text/issues/300)) ([e23d1f4](https://github.com/contentful/rich-text/commit/e23d1f4f5c63ce7ded84144b271f566327a144d1)) ## 15.10.1 (2021-12-21) # 15.10.0 (2021-12-15) ## 15.9.1 (2021-12-10) # 15.9.0 (2021-12-09) # 15.7.0 (2021-11-11) ## 15.6.2 (2021-11-05) ## 15.6.1 (2021-11-05) # 15.6.0 (2021-11-04) ## 15.5.1 (2021-10-25) # 15.5.0 (2021-10-25) # 15.4.0 (2021-09-16) ### Features - **html+react:** render Table header as ([#269](https://github.com/contentful/rich-text/issues/269)) ([0f82905](https://github.com/contentful/rich-text/commit/0f829059be6d91e042dfc71698009177ae4ab78d)) ## 15.3.6 (2021-09-15) ## 15.3.5 (2021-09-13) ## 15.3.3 (2021-09-07) ## 15.3.2 (2021-09-07) ## 15.3.1 (2021-09-07) # 15.3.0 (2021-09-06) # 15.2.0 (2021-08-16) ### Bug Fixes - audit fix aiming mixing-deep vulnerability ([2b98cbd](https://github.com/contentful/rich-text/commit/2b98cbd8e85435305378dd91d70d55db5e9c0832)) # 15.1.0 (2021-08-02) ### Features - 🎸 add RT react renderer tables support ([fddfb8a](https://github.com/contentful/rich-text/commit/fddfb8a943b9807efe92b749d7ffdeb1308e42bd)) # 15.0.0 (2021-06-15) ## 14.1.3 (2021-04-12) ## 14.1.2 (2020-11-02) ## 14.0.1 (2020-01-30) # 14.0.0 (2020-01-28) # 13.4.0 (2019-08-01) ### Features - 🎸 Adds BLOCKS.DOCUMENT renderer support to react-renderer ([57c922c](https://github.com/contentful/rich-text/commit/57c922cb638c47729f2189815a647ba68859394e)), closes [#104](https://github.com/contentful/rich-text/issues/104) # 13.2.0 (2019-03-18) ### Features - 🎸 renderText option to alter all text ([f684a6e](https://github.com/contentful/rich-text/commit/f684a6e91d81ab0c66c286adaa923bd2edf0f4f1)) # 13.1.0 (2019-03-04) ## 13.0.1 (2019-02-11) ### Bug Fixes - 🐛 Fix wrong import in tests ([bead583](https://github.com/contentful/rich-text/commit/bead583e6e9bb71638694e466980b90e599fa47b)) ### Features - 🎸 Initial rich-text-react-renderer implementation ([06dad9b](https://github.com/contentful/rich-text/commit/06dad9b0359325d8fa433438dac997fc9656d13f)) ## [15.15.1](https://github.com/contentful/rich-text/compare/v15.15.0...v15.15.1) (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # [15.15.0](https://github.com/contentful/rich-text/compare/v15.14.1...v15.15.0) (2022-11-29) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.14.1](https://github.com/contentful/rich-text/compare/v15.14.0...v15.14.1) (2022-11-23) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.14.0](https://github.com/contentful/rich-text/compare/v15.13.2...v15.14.0) (2022-11-14) ### Features - add super/sub script types ([#391](https://github.com/contentful/rich-text/issues/391)) ([2562f66](https://github.com/contentful/rich-text/commit/2562f66278f0eff4eeeb367025d4b465773893d1)) ## [15.13.2](https://github.com/contentful/rich-text/compare/v15.13.1...v15.13.2) (2022-09-07) ### Bug Fixes - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) ## [15.12.1](https://github.com/contentful/rich-text/compare/v15.12.0...v15.12.1) (2022-04-21) ### Bug Fixes - upgrade react peerDependencies to support react^18.0.0 ([#323](https://github.com/contentful/rich-text/issues/323)) ([7a0bfdf](https://github.com/contentful/rich-text/commit/7a0bfdfb687cca608239e072cbc301ba1b1310d1)) # [15.12.0](https://github.com/contentful/rich-text/compare/v15.11.2...v15.12.0) (2022-03-25) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.11.1](https://github.com/contentful/rich-text/compare/v15.11.0...v15.11.1) (2022-01-04) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.11.0](https://github.com/contentful/rich-text/compare/v15.10.1...v15.11.0) (2021-12-27) ### Bug Fixes - **react-renderer:** wrap table rows in tbody ([#300](https://github.com/contentful/rich-text/issues/300)) ([e23d1f4](https://github.com/contentful/rich-text/commit/e23d1f4f5c63ce7ded84144b271f566327a144d1)) ## [15.10.1](https://github.com/contentful/rich-text/compare/v15.10.0...v15.10.1) (2021-12-21) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.10.0](https://github.com/contentful/rich-text/compare/v15.9.1...v15.10.0) (2021-12-15) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.9.1](https://github.com/contentful/rich-text/compare/v15.9.0...v15.9.1) (2021-12-10) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.9.0](https://github.com/contentful/rich-text/compare/v15.8.0...v15.9.0) (2021-12-09) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.7.0](https://github.com/contentful/rich-text/compare/v15.6.2...v15.7.0) (2021-11-11) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.6.2](https://github.com/contentful/rich-text/compare/v15.6.1...v15.6.2) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.6.1](https://github.com/contentful/rich-text/compare/v15.6.0...v15.6.1) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.6.0](https://github.com/contentful/rich-text/compare/v15.5.1...v15.6.0) (2021-11-04) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.5.1](https://github.com/contentful/rich-text/compare/v15.5.0...v15.5.1) (2021-10-25) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.5.0](https://github.com/contentful/rich-text/compare/v15.4.0...v15.5.0) (2021-10-25) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.4.0](https://github.com/contentful/rich-text/compare/v15.3.6...v15.4.0) (2021-09-16) ### Features - **html+react:** render Table header as ([#269](https://github.com/contentful/rich-text/issues/269)) ([0f82905](https://github.com/contentful/rich-text/commit/0f829059be6d91e042dfc71698009177ae4ab78d)) ## [15.3.6](https://github.com/contentful/rich-text/compare/v15.3.5...v15.3.6) (2021-09-15) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.3.5](https://github.com/contentful/rich-text/compare/v15.3.4...v15.3.5) (2021-09-13) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.3.3](https://github.com/contentful/rich-text/compare/v15.3.2...v15.3.3) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.3.2](https://github.com/contentful/rich-text/compare/v15.3.1...v15.3.2) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [15.3.1](https://github.com/contentful/rich-text/compare/v15.3.0...v15.3.1) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-react-renderer # [15.2.0](https://github.com/contentful/rich-text/compare/v15.1.0...v15.2.0) (2021-08-16) ### Bug Fixes - audit fix aiming mixing-deep vulnerability ([2b98cbd](https://github.com/contentful/rich-text/commit/2b98cbd8e85435305378dd91d70d55db5e9c0832)) # [15.1.0](https://github.com/contentful/rich-text/compare/v15.0.0...v15.1.0) (2021-08-02) ### Features - 🎸 add RT react renderer tables support ([fddfb8a](https://github.com/contentful/rich-text/commit/fddfb8a943b9807efe92b749d7ffdeb1308e42bd)) # [15.0.0](https://github.com/contentful/rich-text/compare/v14.2.0...v15.0.0) (2021-06-15) # [15.3.0](https://github.com/contentful/rich-text/compare/v15.2.0...v15.3.0) (2021-09-06) **Note:** Version bump only for package @contentful/rich-text-react-renderer ## [14.1.3](https://github.com/contentful/rich-text/compare/v14.1.2...v14.1.3) (2021-04-12) ## [14.1.2](https://github.com/contentful/rich-text/compare/v14.0.1...v14.1.2) (2020-11-02) ## [14.0.1](https://github.com/contentful/rich-text/compare/v14.0.0...v14.0.1) (2020-01-30) # [14.0.0](https://github.com/contentful/rich-text/compare/v13.4.0...v14.0.0) (2020-01-28) # [13.4.0](https://github.com/contentful/rich-text/compare/v13.3.0...v13.4.0) (2019-08-01) ### Features - 🎸 Adds BLOCKS.DOCUMENT renderer support to react-renderer ([57c922c](https://github.com/contentful/rich-text/commit/57c922cb638c47729f2189815a647ba68859394e)), closes [#104](https://github.com/contentful/rich-text/issues/104) # [13.2.0](https://github.com/contentful/rich-text/compare/v13.1.0...v13.2.0) (2019-03-18) ### Features - 🎸 renderText option to alter all text ([f684a6e](https://github.com/contentful/rich-text/commit/f684a6e91d81ab0c66c286adaa923bd2edf0f4f1)) # [13.1.0](https://github.com/contentful/rich-text/compare/v13.0.1...v13.1.0) (2019-03-04) ## [13.0.1](https://github.com/contentful/rich-text/compare/v13.0.0...v13.0.1) (2019-02-11) ### Bug Fixes - 🐛 Fix wrong import in tests ([bead583](https://github.com/contentful/rich-text/commit/bead583e6e9bb71638694e466980b90e599fa47b)) ### Features - 🎸 Initial rich-text-react-renderer implementation ([06dad9b](https://github.com/contentful/rich-text/commit/06dad9b0359325d8fa433438dac997fc9656d13f)) ================================================ FILE: packages/rich-text-react-renderer/README.md ================================================ # rich-text-react-renderer React renderer for the Contentful rich text field type. ## Installation Using [npm](http://npmjs.org/): ```sh npm install @contentful/rich-text-react-renderer ``` Using [yarn](https://yarnpkg.com/): ```sh yarn add @contentful/rich-text-react-renderer ``` ## Usage ```javascript import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; const document = { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'Hello world!', marks: [], data: {}, }, ], }, ], }; documentToReactComponents(document); // -> Hello world!
``` ```javascript import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; const document = { nodeType: 'document', content: [ { nodeType: 'paragraph', content: [ { nodeType: 'text', value: 'Hello', marks: [{ type: 'bold' }], }, { nodeType: 'text', value: ' world!', marks: [{ type: 'italic' }], }, ], }, ], }; documentToReactComponents(document); // ->Hello world!
``` You can also pass custom renderers for both marks and nodes as an optional parameter like so: ```javascript import { BLOCKS, MARKS } from '@contentful/rich-text-types'; import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; const document = { nodeType: 'document', content: [ { nodeType: 'paragraph', content: [ { nodeType: 'text', value: 'Hello', marks: [{ type: 'bold' }], }, { nodeType: 'text', value: ' world!', marks: [{ type: 'italic' }], }, ], }, ], }; const Bold = ({ children }) =>{children}
; const Text = ({ children }) =>{children}
; const options = { renderMark: { [MARKS.BOLD]: (text) =>{text} , }, renderNode: { [BLOCKS.PARAGRAPH]: (node, children) =>{children} , }, renderText: (text) => text.replace('!', '?'), }; documentToReactComponents(document, options); // ->Hello
world? ``` Last, but not least, you can pass a custom rendering component for an embedded entry: ```javascript import { BLOCKS } from '@contentful/rich-text-types'; import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; const document = { nodeType: 'document', content: [ { nodeType: 'embedded-entry-block', data: { target: (...)Link<'Entry'>(...); }, }, ] }; const CustomComponent = ({ title, description }) => (); const options = { renderNode: { [BLOCKS.EMBEDDED_ENTRY]: (node) => { const { title, description } = node.data.target.fields; return{title}
{description}
} } }; documentToReactComponents(document, options); // -> ``` The `renderNode` keys should be one of the following `BLOCKS` and `INLINES` properties as defined in [`@contentful/rich-text-types`](https://www.npmjs.com/package/@contentful/rich-text-types): - `BLOCKS` - `DOCUMENT` - `PARAGRAPH` - `HEADING_1` - `HEADING_2` - `HEADING_3` - `HEADING_4` - `HEADING_5` - `HEADING_6` - `UL_LIST` - `OL_LIST` - `LIST_ITEM` - `QUOTE` - `HR` - `EMBEDDED_ENTRY` - `EMBEDDED_ASSET` - `EMBEDDED_RESOURCE` - `INLINES` - `EMBEDDED_ENTRY` (this is different from the `BLOCKS.EMBEDDED_ENTRY`) - `EMBEDDED_RESOURCE` - `HYPERLINK` - `ENTRY_HYPERLINK` - `ASSET_HYPERLINK` - `RESOURCE_HYPERLINK` The `renderMark` keys should be one of the following `MARKS` properties as defined in [`@contentful/rich-text-types`](https://www.npmjs.com/package/@contentful/rich-text-types): - `BOLD` - `ITALIC` - `UNDERLINE` - `CODE` The `renderText` callback is a function that has a single string argument and returns a React node. Each text node is evaluated individually by this callback. A possible use case for this is to replace instances of `\n` produced by `Shift + Enter` with `[title]
[description]
` React elements. This could be accomplished in the following way: ```javascript const options = { renderText: (text) => { return text.split('\n').reduce((children, textSegment, index) => { return [...children, index > 0 &&
, textSegment]; }, []); }, }; ``` #### Note on adding a `key` prop in custom renderers: It is possible to pass a `key` prop in the components returned by custom renderers. A good use case for this is in embeded entries using the node's `target.sys.id`. It is important not to pass anything that is index-like (e.g. 1 or "1") as it may clash with the default renderers which automatically inject a `key` prop using their index in the Contentful rich text AST. To work around this limitation, just append any non-numeric character to your custom key. ```javascript const options = { renderMark: { [MARKS.BOLD]: (text) => { return {text}; }, }, }; ``` #### Preserving Whitespace The `options` object can include a `preserveWhitespace` boolean flag. When set to `true`, this flag ensures that multiple spaces in the rich text content are preserved by replacing them with ` `, and line breaks are maintained with `
` tags. This is useful for content that relies on specific formatting using spaces and line breaks. Note that `preserveWhitespace` is not compatible with the `renderText` option. When the functionality of `preserveWhitespace` is desired along with a custom `renderText` callback, it should be implemented within the callback instead. ```javascript import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; const document = { nodeType: 'document', content: [ { nodeType: 'paragraph', content: [ { nodeType: 'text', value: 'Hello world!', marks: [], }, ], }, ], }; const options = { preserveWhitespace: true, }; documentToReactComponents(document, options); // ->Hello world!
``` In this example, the multiple spaces between "Hello" and "world!" are preserved in the rendered output. ================================================ FILE: packages/rich-text-react-renderer/jest.config.js ================================================ const getBaseConfig = require('../../baseJestConfig'); const package = require('./package.json'); const packageName = package.name.split('@contentful/')[1]; module.exports = { ...getBaseConfig(packageName), }; ================================================ FILE: packages/rich-text-react-renderer/package.json ================================================ { "name": "@contentful/rich-text-react-renderer", "version": "16.2.1", "main": "dist/rich-text-react-renderer.es5.js", "module": "dist/rich-text-react-renderer.esm.js", "typings": "dist/types/index.d.ts", "exports": { ".": { "types": "./dist/types/index.d.ts", "import": "./dist/rich-text-react-renderer.esm.js", "require": "./dist/rich-text-react-renderer.es5.js", "default": "./dist/rich-text-react-renderer.es5.js" }, "./package.json": "./package.json" }, "files": [ "dist" ], "repository": { "type": "git", "url": "https://github.com/contentful/rich-text.git" }, "license": "MIT", "engines": { "node": ">=20.0.0" }, "publishConfig": { "access": "public", "registry": "https://npm.pkg.github.com/" }, "scripts": { "prebuild": "rimraf dist", "build": "tsc --module commonjs && rollup -c --bundleConfigAsCjs rollup.config.js", "start": "tsc && rollup -c --bundleConfigAsCjs rollup.config.js -w", "test": "jest" }, "dependencies": { "@contentful/rich-text-types": "^17.2.7" }, "peerDependencies": { "react": "^16.8.6 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.6 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "devDependencies": { "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "react": "^19.2.3", "react-dom": "^19.2.3", "ts-jest": "^29.4.6" } } ================================================ FILE: packages/rich-text-react-renderer/rollup.config.js ================================================ import config from '../../rollup.config'; import { main as outputFile, dependencies } from './package.json'; const overrides = { input: 'src/index.tsx', external: [...Object.keys(dependencies), 'react'], }; export default config(outputFile, overrides); ================================================ FILE: packages/rich-text-react-renderer/src/__test__/__snapshots__/index.test.tsx.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`documentToReactComponents does not render unrecognized marks 1`] = ` [Hello world!
, ] `; exports[`documentToReactComponents renders asset hyperlink 1`] = ` [type: asset-hyperlink id: 9mpxT4zsRi6Iwukey8KeM
, ] `; exports[`documentToReactComponents renders blockquotes 1`] = ` [hello
,world, ] `; exports[`documentToReactComponents renders default entry link block 1`] = ` [ , ] `; exports[`documentToReactComponents renders default resource block 1`] = ` [ , ] `; exports[`documentToReactComponents renders embedded entry 1`] = ` [type: embedded-entry-inline id: 9mpxT4zsRi6Iwukey8KeM
, ] `; exports[`documentToReactComponents renders embedded resource 1`] = ` [type: embedded-resource-inline urn: crn:contentful:::content:spaces/6fqi4ljzyr0e/environments/master/entries/9mpxT4zsRi6Iwukey8KeM
, ] `; exports[`documentToReactComponents renders empty node if type is not recognized 1`] = ` [Hello world! , ] `; exports[`documentToReactComponents renders entry hyperlink 1`] = ` [type: entry-hyperlink id: 9mpxT4zsRi6Iwukey8KeM
, ] `; exports[`documentToReactComponents renders horizontal rule 1`] = ` [hello world
,
, , ] `; exports[`documentToReactComponents renders hyperlink 1`] = ` [Some text link text.
, ] `; exports[`documentToReactComponents renders marks with default mark renderer 1`] = ` [hello world
, ] `; exports[`documentToReactComponents renders marks with default mark renderer 2`] = ` [hello world
, ] `; exports[`documentToReactComponents renders marks with default mark renderer 3`] = ` [hello world
, ] `; exports[`documentToReactComponents renders marks with default mark renderer 4`] = ` [, ] `; exports[`documentToReactComponents renders marks with default mark renderer 5`] = ` [
hello worldhello world
, ] `; exports[`documentToReactComponents renders marks with default mark renderer 6`] = ` [hello world
, ] `; exports[`documentToReactComponents renders marks with default mark renderer 7`] = ` [, ] `; exports[`documentToReactComponents renders marks with the passed custom mark renderer 1`] = ` [
hello worldhello world
, ] `; exports[`documentToReactComponents renders multiple marks with default mark renderer 1`] = ` [hello world
, ] `; exports[`documentToReactComponents renders nodes with default node renderer 1`] = ` [hello world
, ] `; exports[`documentToReactComponents renders nodes with default node renderer 2`] = ` [hello world
, ] `; exports[`documentToReactComponents renders nodes with default node renderer 3`] = ` [hello world
, ] `; exports[`documentToReactComponents renders nodes with default node renderer 4`] = ` [hello world
, ] `; exports[`documentToReactComponents renders nodes with default node renderer 5`] = ` [hello world
, ] `; exports[`documentToReactComponents renders nodes with default node renderer 6`] = ` [hello world
, ] `; exports[`documentToReactComponents renders nodes with default node renderer 7`] = ` [hello world
, ] `; exports[`documentToReactComponents renders nodes with passed custom node renderer 1`] = ``; exports[`documentToReactComponents renders ordered lists 1`] = ` [ hello world, , ] `; exports[`documentToReactComponents renders resource hyperlink 1`] = ` [
Hello
world
type: resource-hyperlink urn: crn:contentful:::content:spaces/6fqi4ljzyr0e/environments/master/entries/9mpxT4zsRi6Iwukey8KeM
, ] `; exports[`documentToReactComponents renders tables 1`] = ` [, ] `; exports[`documentToReactComponents renders tables with header 1`] = ` [
A 1
B 1
A 2
B 2
, ] `; exports[`documentToReactComponents renders text with the passed custom text renderer 1`] = ` [
A 1
B 1
A 2
B 2
hello Earth
, ] `; exports[`documentToReactComponents renders unaltered text with default text renderer 1`] = ` [hello world
, ] `; exports[`documentToReactComponents renders unordered lists 1`] = ` [, , ] `; exports[`documentToReactComponents returns an array of elements when given a populated document 1`] = ` [
Hello
world
hello world
,
, , ] `; exports[`nodeListToReactComponents renders children as an array with keys from its index 1`] = ` [hello
, " ",world
, ] `; exports[`nodeToReactComponent does not add additional tags on invalid marks 1`] = `"hello world"`; exports[`nodeToReactComponent does not render altered text with default text renderer 1`] = `some lines of text
`; exports[`nodeToReactComponent renders altered text with custom text renderer 1`] = `some
`; exports[`nodeToReactComponent renders invalid node types in React fragments 1`] = `
lines
of
texthello world `; exports[`nodeToReactComponent renders valid marks 1`] = ` hello world `; exports[`nodeToReactComponent renders valid nodes 1`] = `hello world
`; exports[`preserveWhitespace preserves new lines 1`] = ` [hello
, ] `; exports[`preserveWhitespace preserves spaces between words 1`] = ` [
worldhello world
, ] `; exports[`stripEmptyTrailingParagraph does not strip empty trailing paragraph when disabled 1`] = ` [Hello world
, , ] `; exports[`stripEmptyTrailingParagraph does not strip empty trailing paragraph when it is the only child 1`] = ` [ , ] `; exports[`stripEmptyTrailingParagraph does not strip non-empty trailing paragraph 1`] = ` [Hello world
,Not empty
, ] `; exports[`stripEmptyTrailingParagraph does not strip trailing non-paragraph node 1`] = ` [Hello world
, , ] `; exports[`stripEmptyTrailingParagraph does not strip trailing paragraph with multiple text nodes 1`] = ` [Hello world
, , ] `; exports[`stripEmptyTrailingParagraph strips empty trailing paragraph when enabled 1`] = ` [Hello world
, ] `; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/components/Document.tsx ================================================ import React, { FunctionComponent, ReactNode } from 'react'; type DocumentProps = { children: ReactNode; }; const Document: FunctionComponent= ({ children }) => { return {children} ; }; export default Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/components/Paragraph.tsx ================================================ import React, { FunctionComponent, ReactNode } from 'react'; type ParagraphProps = { children: ReactNode; }; const Paragraph: FunctionComponent= ({ children }) => { return {children}
; }; export default Paragraph; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/components/Strong.tsx ================================================ import React, { FunctionComponent, ReactNode } from 'react'; type StrongProps = { children: ReactNode; }; const Strong: FunctionComponent= ({ children }) => { return {children}; }; export default Strong; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/embedded-entry.ts ================================================ import { BLOCKS, Document } from '@contentful/rich-text-types'; export default function (entry: Record ) { return { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_ENTRY, content: [], data: { target: entry, }, }, ], } as Document; } ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/embedded-resource.ts ================================================ import { Document, BLOCKS, ResourceLink } from '@contentful/rich-text-types'; export default function (resourceLink: ResourceLink) { return { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.EMBEDDED_RESOURCE, content: [], data: { target: resourceLink, }, }, ], } as Document; } ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/heading.ts ================================================ import { Document, BLOCKS } from '@contentful/rich-text-types'; export default function (heading: string) { return { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: heading, data: {}, content: [ { nodeType: 'text', value: 'hello world', marks: [], data: {}, }, ], }, ], } as Document; } ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/hr.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { content: [ { content: [ { marks: [], nodeType: 'text', value: 'hello world', data: {}, }, ], data: {}, nodeType: 'paragraph', }, { content: [ { marks: [], nodeType: 'text', value: '', data: {}, }, ], data: {}, nodeType: 'hr', }, { content: [ { marks: [], nodeType: 'text', value: '', data: {}, }, ], data: {}, nodeType: 'paragraph', }, ], data: {}, nodeType: 'document', } as Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/hyperlink.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'Some text ', marks: [], data: {}, }, { nodeType: 'hyperlink', content: [ { nodeType: 'text', value: 'link', marks: [], data: {}, }, ], data: { uri: 'https://url.org', }, }, { nodeType: 'text', value: ' text.', marks: [], data: {}, }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/index.ts ================================================ export { default as hrDoc } from './hr'; export { default as hyperlinkDoc } from './hyperlink'; export { default as invalidMarksDoc } from './invalid-marks'; export { default as invalidTypeDoc } from './invalid-type'; export { default as paragraphDoc } from './paragraph'; export { default as headingDoc } from './heading'; export { default as marksDoc } from './mark'; export { default as multiMarkDoc } from './multi-mark'; export { default as embeddedEntryDoc } from './embedded-entry'; export { default as embeddedResourceDoc } from './embedded-resource'; export { default as inlineEntityDoc } from './inline-entity'; export { default as olDoc } from './ol'; export { default as ulDoc } from './ul'; export { default as quoteDoc } from './quote'; export { default as tableDoc } from './table'; export { default as tableWithHeaderDoc } from './table-header'; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/inline-entity.ts ================================================ import { BLOCKS, Document, INLINES } from '@contentful/rich-text-types'; export default function inlineEntity(entry: Record , inlineType: INLINES) { return { content: [ { data: {}, content: [ { marks: [], value: '', nodeType: 'text', data: {}, }, { data: entry, content: [ { marks: [], value: '', nodeType: 'text', data: {}, }, ], nodeType: inlineType, }, { marks: [], value: '', nodeType: 'text', data: {}, }, ], nodeType: BLOCKS.PARAGRAPH, }, ], data: {}, nodeType: BLOCKS.DOCUMENT, } as Document; } ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/invalid-marks.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'Hello world!', marks: [ { type: 'UNRECOGNIZED_MARK', }, ], data: {}, }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/invalid-type.ts ================================================ import { Document, BLOCKS } from '@contentful/rich-text-types'; export default { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: 'UNRECOGNIZED_TYPE' as BLOCKS, data: {}, content: [ { nodeType: 'text', value: 'Hello world!', marks: [], data: {}, }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/mark.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default function (mark: string) { return { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'hello world', marks: [{ type: mark }], data: {}, }, ], }, ], } as Document; } ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/multi-mark.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default function () { return { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'hello world', marks: [{ type: 'bold' }, { type: 'italic' }], data: {}, }, ], }, ], } as Document; } ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/ol.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { data: {}, content: [ { data: {}, content: [ { data: {}, content: [ { data: {}, content: [ { data: {}, marks: [], value: 'Hello', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'list-item', }, { data: {}, content: [ { data: {}, content: [ { data: {}, marks: [], value: 'world', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'list-item', }, ], nodeType: 'ordered-list', }, { data: {}, content: [ { data: {}, marks: [], value: '', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'document', } as Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/paragraph.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { nodeType: 'document', data: {}, content: [ { nodeType: 'paragraph', data: {}, content: [ { nodeType: 'text', value: 'hello world', marks: [], data: {}, }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/quote.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { data: {}, content: [ { data: {}, content: [ { data: {}, marks: [], value: 'hello', nodeType: 'text', }, ], nodeType: 'paragraph', }, { data: {}, content: [ { data: {}, marks: [], value: 'world', nodeType: 'text', }, ], nodeType: 'blockquote', }, ], nodeType: 'document', } as Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/table-header.ts ================================================ import { Document, BLOCKS } from '@contentful/rich-text-types'; export default { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.TABLE, data: {}, content: [ { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_HEADER_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'A 1', }, ], }, ], }, { nodeType: BLOCKS.TABLE_HEADER_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'B 1', }, ], }, ], }, ], }, { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'A 2', }, ], }, ], }, { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'B 2', }, ], }, ], }, ], }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/table.ts ================================================ import { Document, BLOCKS } from '@contentful/rich-text-types'; export default { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.TABLE, data: {}, content: [ { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'A 1', }, ], }, ], }, { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'B 1', }, ], }, ], }, ], }, { nodeType: BLOCKS.TABLE_ROW, data: {}, content: [ { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'A 2', }, ], }, ], }, { nodeType: BLOCKS.TABLE_CELL, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', data: {}, marks: [], value: 'B 2', }, ], }, ], }, ], }, ], }, ], } as Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/documents/ul.ts ================================================ import { Document } from '@contentful/rich-text-types'; export default { data: {}, content: [ { data: {}, content: [ { data: {}, content: [ { data: {}, content: [ { data: {}, marks: [], value: 'Hello', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'list-item', }, { data: {}, content: [ { data: {}, content: [ { data: {}, marks: [], value: 'world', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'list-item', }, ], nodeType: 'unordered-list', }, { data: {}, content: [ { data: {}, marks: [], value: '', nodeType: 'text', }, ], nodeType: 'paragraph', }, ], nodeType: 'document', } as Document; ================================================ FILE: packages/rich-text-react-renderer/src/__test__/index.test.tsx ================================================ import React, { ReactNode } from 'react'; import { BLOCKS, Document, INLINES, MARKS, ResourceLink } from '@contentful/rich-text-types'; import { CommonNode, documentToReactComponents, Options } from '..'; import { appendKeyToValidElement } from '../util/appendKeyToValidElement'; import { nodeListToReactComponents, nodeToReactComponent } from '../util/nodeListToReactComponents'; import DocumentWrapper from './components/Document'; import Paragraph from './components/Paragraph'; import Strong from './components/Strong'; import { embeddedEntryDoc, headingDoc, hrDoc, hyperlinkDoc, inlineEntityDoc, invalidMarksDoc, invalidTypeDoc, marksDoc, multiMarkDoc, olDoc, paragraphDoc, quoteDoc, tableDoc, tableWithHeaderDoc, ulDoc, } from './documents'; import embeddedResource from './documents/embedded-resource'; describe('documentToReactComponents', () => { it('returns an empty array when given an empty document', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [], }; expect(documentToReactComponents(document)).toEqual([]); }); it('returns an array of elements when given a populated document', () => { const document: Document = hrDoc; expect(documentToReactComponents(document)).toMatchSnapshot(); expect(documentToReactComponents(document)).toBeInstanceOf(Array); }); it('renders nodes with default node renderer', () => { const docs: Document[] = [ paragraphDoc, headingDoc(BLOCKS.HEADING_1), headingDoc(BLOCKS.HEADING_2), headingDoc(BLOCKS.HEADING_3), headingDoc(BLOCKS.HEADING_4), headingDoc(BLOCKS.HEADING_5), headingDoc(BLOCKS.HEADING_6), ]; docs.forEach((doc) => { expect(documentToReactComponents(doc)).toMatchSnapshot(); }); }); it('renders marks with default mark renderer', () => { const docs: Document[] = [ marksDoc(MARKS.ITALIC), marksDoc(MARKS.BOLD), marksDoc(MARKS.UNDERLINE), marksDoc(MARKS.CODE), marksDoc(MARKS.SUPERSCRIPT), marksDoc(MARKS.SUBSCRIPT), marksDoc(MARKS.STRIKETHROUGH), ]; docs.forEach((doc) => { expect(documentToReactComponents(doc)).toMatchSnapshot(); }); }); it('renders unaltered text with default text renderer', () => { const document: Document = paragraphDoc; expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders multiple marks with default mark renderer', () => { const doc: Document = multiMarkDoc(); expect(documentToReactComponents(doc)).toMatchSnapshot(); }); it('renders nodes with passed custom node renderer', () => { const options: Options = { renderNode: { [BLOCKS.DOCUMENT]: (node, children) => {children} , [BLOCKS.PARAGRAPH]: (node, children) =>{children} , }, }; const document: Document = quoteDoc; expect(documentToReactComponents(document, options)).toMatchSnapshot(); }); it('renders marks with the passed custom mark renderer', () => { const options: Options = { renderMark: { [MARKS.BOLD]: (text) => {text}, }, }; const document: Document = multiMarkDoc(); expect(documentToReactComponents(document, options)).toMatchSnapshot(); }); it('renders text with the passed custom text renderer', () => { const options: Options = { renderText: (text) => text.replace(/world/, 'Earth'), }; const document: Document = paragraphDoc; expect(documentToReactComponents(document, options)).toMatchSnapshot(); }); it('does not render unrecognized marks', () => { const document: Document = invalidMarksDoc; expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders empty node if type is not recognized', () => { const document: Document = invalidTypeDoc; expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders default entry link block', () => { const entrySys = { sys: { id: '9mpxT4zsRi6Iwukey8KeM', link: 'Link', linkType: 'Entry', }, }; const document: Document = embeddedEntryDoc(entrySys); expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders default resource block', () => { const resourceSys: ResourceLink = { sys: { urn: 'crn:contentful:::content:spaces/6fqi4ljzyr0e/environments/master/entries/9mpxT4zsRi6Iwukey8KeM', type: 'ResourceLink', linkType: 'Contentful:Entry', }, }; const document: Document = embeddedResource(resourceSys); expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders ordered lists', () => { const document: Document = olDoc; expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders unordered lists', () => { const document: Document = ulDoc; expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders blockquotes', () => { const document: Document = quoteDoc; expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders horizontal rule', () => { const document: Document = hrDoc; expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders tables', () => { const document: Document = tableDoc; expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders tables with header', () => { expect(documentToReactComponents(tableWithHeaderDoc)).toMatchSnapshot(); }); it('does not crash with inline elements (e.g. hyperlink)', () => { const document: Document = hyperlinkDoc; expect(documentToReactComponents(document)).toBeTruthy(); }); it('renders hyperlink', () => { const document: Document = hyperlinkDoc; expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders asset hyperlink', () => { const asset = { target: { sys: { id: '9mpxT4zsRi6Iwukey8KeM', link: 'Link', type: 'Asset', }, }, }; const document: Document = inlineEntityDoc(asset, INLINES.ASSET_HYPERLINK); expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders entry hyperlink', () => { const entry = { target: { sys: { id: '9mpxT4zsRi6Iwukey8KeM', type: 'Link', linkType: 'Entry', }, }, }; const document: Document = inlineEntityDoc(entry, INLINES.ENTRY_HYPERLINK); expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders resource hyperlink', () => { const entry = { target: { sys: { urn: 'crn:contentful:::content:spaces/6fqi4ljzyr0e/environments/master/entries/9mpxT4zsRi6Iwukey8KeM', type: 'Link', linkType: 'Entry', }, }, }; const document: Document = inlineEntityDoc(entry, INLINES.RESOURCE_HYPERLINK); expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders embedded entry', () => { const entry = { target: { sys: { id: '9mpxT4zsRi6Iwukey8KeM', type: 'Link', linkType: 'Entry', }, }, }; const document: Document = inlineEntityDoc(entry, INLINES.EMBEDDED_ENTRY); expect(documentToReactComponents(document)).toMatchSnapshot(); }); it('renders embedded resource', () => { const entry = { target: { sys: { urn: 'crn:contentful:::content:spaces/6fqi4ljzyr0e/environments/master/entries/9mpxT4zsRi6Iwukey8KeM', type: 'ResourceLink', linkType: 'Contentful:Entry', }, }, }; const document: Document = inlineEntityDoc(entry, INLINES.EMBEDDED_RESOURCE); expect(documentToReactComponents(document)).toMatchSnapshot(); }); }); describe('appendKeyToValidElement', () => { it('appends keys to default React components', () => { expect(appendKeyToValidElement(, 0)).toHaveProperty('key', '0'); }); it('appends keys to custom React components', () => { expect(appendKeyToValidElement(hello world , 0)).toHaveProperty( 'key', '0', ); }); it('does not overwrite user specified keys', () => { expect(appendKeyToValidElement(, 0)).toHaveProperty('key', 'xyz'); }); it('does not add keys to text nodes', () => { expect(appendKeyToValidElement('hello world', 0)).not.toHaveProperty('key'); }); it('does not add keys to node arrays', () => { expect(appendKeyToValidElement([, ], 0)).not.toHaveProperty( 'key', ); }); it('does not add keys to null', () => { expect(appendKeyToValidElement(null, 0)).toBeNull(); }); }); describe('nodeToReactComponent', () => { const options: Options = { renderNode: { [BLOCKS.PARAGRAPH]: (node: CommonNode, children: ReactNode): ReactNode =>{children}
, }, renderMark: { [MARKS.BOLD]: (text: ReactNode): ReactNode => {text}, }, }; const createBlockNode = (nodeType: BLOCKS): CommonNode => ({ nodeType, data: {}, content: [ { nodeType: 'text', value: 'hello world', marks: [], data: {}, }, ], }); const createTextNode = (type: string): CommonNode => ({ nodeType: 'text', value: 'hello world', marks: [{ type }], data: {}, }); it('renders valid nodes', () => { expect(nodeToReactComponent(createBlockNode(BLOCKS.PARAGRAPH), options)).toMatchSnapshot(); }); it('renders invalid node types in React fragments', () => { expect(nodeToReactComponent(createBlockNode(BLOCKS.HEADING_1), options)).toMatchSnapshot(); }); it('renders valid marks', () => { expect(nodeToReactComponent(createTextNode(MARKS.BOLD), options)).toMatchSnapshot(); }); it('does not add additional tags on invalid marks', () => { expect(nodeToReactComponent(createTextNode(MARKS.ITALIC), options)).toMatchSnapshot(); }); const customTextNode: CommonNode = { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'some\nlines\nof\ntext', marks: [{ type: MARKS.BOLD }], data: {}, }, ], }; it('does not render altered text with default text renderer', () => { expect(nodeToReactComponent(customTextNode, options)).toMatchSnapshot(); }); it('renders altered text with custom text renderer', () => { expect( nodeToReactComponent(customTextNode, { ...options, renderText: (text: string): ReactNode => { return text.split('\n').reduce((children, textSegment, index) => { return [...children, index > 0 &&
, textSegment]; }, []); }, }), ).toMatchSnapshot(); }); }); describe('nodeListToReactComponents', () => { const options: Options = { renderNode: { [BLOCKS.PARAGRAPH]: (node: CommonNode, children: ReactNode): ReactNode =>{children}
, }, renderMark: { [MARKS.BOLD]: (text: ReactNode): ReactNode => {text}, }, }; const nodes: CommonNode[] = [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'hello', marks: [{ type: MARKS.BOLD }], data: {}, }, ], }, { nodeType: 'text', value: ' ', marks: [], data: {}, }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'world', marks: [], data: {}, }, ], }, ]; it('renders children as an array with keys from its index', () => { const renderedNodes: ReactNode[] = nodeListToReactComponents(nodes, options) as ReactNode[]; expect(renderedNodes[0]).toHaveProperty('key', '0'); expect(renderedNodes[1]).not.toHaveProperty('key'); expect(renderedNodes[2]).toHaveProperty('key', '2'); expect(renderedNodes).toMatchSnapshot(); }); }); describe('preserveWhitespace', () => { it('preserves spaces between words', () => { const options: Options = { preserveWhitespace: true, }; const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'hello world', marks: [], data: {}, }, ], }, ], }; expect(documentToReactComponents(document, options)).toMatchSnapshot(); }); it('preserves new lines', () => { const options: Options = { preserveWhitespace: true, }; const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'hello\nworld', marks: [], data: {}, }, ], }, ], }; expect(documentToReactComponents(document, options)).toMatchSnapshot(); }); }); describe('stripEmptyTrailingParagraph', () => { it('strips empty trailing paragraph when enabled', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: true, }; const result = documentToReactComponents(document, options); expect(result).toMatchSnapshot(); }); it('does not strip empty trailing paragraph when disabled', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: false, }; const result = documentToReactComponents(document, options); expect(result).toMatchSnapshot(); }); it('does not strip empty trailing paragraph when it is the only child', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: true, }; const result = documentToReactComponents(document, options); expect(result).toMatchSnapshot(); }); it('does not strip non-empty trailing paragraph', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Not empty', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: true, }; const result = documentToReactComponents(document, options); expect(result).toMatchSnapshot(); }); it('does not strip trailing paragraph with multiple text nodes', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: true, }; const result = documentToReactComponents(document, options); expect(result).toMatchSnapshot(); }); it('does not strip trailing non-paragraph node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: 'Hello world', marks: [], data: {}, }, ], }, { nodeType: BLOCKS.HEADING_1, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; const options: Options = { stripEmptyTrailingParagraph: true, }; const result = documentToReactComponents(document, options); expect(result).toMatchSnapshot(); }); }); ================================================ FILE: packages/rich-text-react-renderer/src/index.tsx ================================================ import React, { ReactNode } from 'react'; import { Block, BLOCKS, Document, Inline, INLINES, MARKS, Text, helpers, } from '@contentful/rich-text-types'; import { nodeToReactComponent } from './util/nodeListToReactComponents'; const defaultNodeRenderers: RenderNode = { [BLOCKS.DOCUMENT]: (node, children) => children, [BLOCKS.PARAGRAPH]: (node, children) =>{children}
, [BLOCKS.HEADING_1]: (node, children) =>{children}
, [BLOCKS.HEADING_2]: (node, children) =>{children}
, [BLOCKS.HEADING_3]: (node, children) =>{children}
, [BLOCKS.HEADING_4]: (node, children) =>{children}
, [BLOCKS.HEADING_5]: (node, children) =>{children}
, [BLOCKS.HEADING_6]: (node, children) =>{children}
, [BLOCKS.EMBEDDED_ENTRY]: (node, children) =>{children}, [BLOCKS.EMBEDDED_RESOURCE]: (node, children) =>{children}, [BLOCKS.UL_LIST]: (node, children) =>{children}
, [BLOCKS.OL_LIST]: (node, children) =>{children}
, [BLOCKS.LIST_ITEM]: (node, children) =>- {children}
, [BLOCKS.QUOTE]: (node, children) =>{children}, [BLOCKS.HR]: () =>
, [BLOCKS.TABLE]: (node, children) => ({children}
), [BLOCKS.TABLE_ROW]: (node, children) =>{children} , [BLOCKS.TABLE_HEADER_CELL]: (node, children) =>{children} , [BLOCKS.TABLE_CELL]: (node, children) =>{children} , [INLINES.ASSET_HYPERLINK]: (node) => defaultInline(INLINES.ASSET_HYPERLINK, node as Inline), [INLINES.ENTRY_HYPERLINK]: (node) => defaultInline(INLINES.ENTRY_HYPERLINK, node as Inline), [INLINES.RESOURCE_HYPERLINK]: (node) => defaultInlineResource(INLINES.RESOURCE_HYPERLINK, node as Inline), [INLINES.EMBEDDED_ENTRY]: (node) => defaultInline(INLINES.EMBEDDED_ENTRY, node as Inline), [INLINES.EMBEDDED_RESOURCE]: (node, _children) => defaultInlineResource(INLINES.EMBEDDED_RESOURCE, node as Inline), [INLINES.HYPERLINK]: (node, children) => {children}, }; const defaultMarkRenderers: RenderMark = { [MARKS.BOLD]: (text) => {text}, [MARKS.ITALIC]: (text) => {text}, [MARKS.UNDERLINE]: (text) => {text}, [MARKS.CODE]: (text) =>{text}, [MARKS.SUPERSCRIPT]: (text) => {text}, [MARKS.SUBSCRIPT]: (text) => {text}, [MARKS.STRIKETHROUGH]: (text) =>{text}, }; function defaultInline(type: string, node: Inline): ReactNode { return ( type: {node.nodeType} id: {node.data.target.sys.id} ); } function defaultInlineResource(type: string, node: Inline) { return ( type: {node.nodeType} urn: {node.data.target.sys.urn} ); } export type CommonNode = Text | Block | Inline; export interface NodeRenderer { (node: Block | Inline, children: ReactNode): ReactNode; } export interface RenderNode { [k: string]: NodeRenderer; } export interface RenderMark { [k: string]: (text: ReactNode) => ReactNode; } export interface RenderText { (text: string): ReactNode; } export interface Options { /** * Node renderers */ renderNode?: RenderNode; /** * Mark renderers */ renderMark?: RenderMark; /** * Text renderer */ renderText?: RenderText; /** * Keep line breaks and multiple spaces */ preserveWhitespace?: boolean; /** * Strip empty trailing paragraph from the document */ stripEmptyTrailingParagraph?: boolean; } /** * Serialize a Contentful Rich Text `document` to React tree */ export function documentToReactComponents( richTextDocument: Document, options: Options = {}, ): ReactNode { if (!richTextDocument) { return null; } // Strip empty trailing paragraph if enabled let processedDocument = richTextDocument; if (options.stripEmptyTrailingParagraph) { processedDocument = helpers.stripEmptyTrailingParagraphFromDocument(richTextDocument); } return nodeToReactComponent(processedDocument, { renderNode: { ...defaultNodeRenderers, ...options.renderNode, }, renderMark: { ...defaultMarkRenderers, ...options.renderMark, }, renderText: options.renderText, preserveWhitespace: options.preserveWhitespace, }); } ================================================ FILE: packages/rich-text-react-renderer/src/util/appendKeyToValidElement.ts ================================================ import { cloneElement, isValidElement, ReactNode } from 'react'; export function appendKeyToValidElement(element: ReactNode, key: number): ReactNode { if (isValidElement(element) && element.key === null) { return cloneElement(element, { key }); } return element; } ================================================ FILE: packages/rich-text-react-renderer/src/util/nodeListToReactComponents.tsx ================================================ import React, { ReactNode } from 'react'; import { helpers, Mark } from '@contentful/rich-text-types'; import { CommonNode, Options } from '..'; import { appendKeyToValidElement } from './appendKeyToValidElement'; export function nodeListToReactComponents(nodes: CommonNode[], options: Options): ReactNode { return nodes.map((node: CommonNode, index: number): ReactNode => { return appendKeyToValidElement(nodeToReactComponent(node, options), index); }); } export function nodeToReactComponent(node: CommonNode, options: Options): ReactNode { const { renderNode, renderMark, renderText, preserveWhitespace } = options; if (helpers.isText(node)) { let nodeValue: ReactNode = renderText ? renderText(node.value) : node.value; // Preserving whitespace is only supported with the default transformations. if (preserveWhitespace && !renderText) { // Preserve multiple spaces. nodeValue = (nodeValue as string).replace(/ {2,}/g, (match) => '\u00A0'.repeat(match.length)); // Preserve line breaks. const lines = (nodeValue as string).split('\n'); const jsxLines: (string | React.JSX.Element)[] = []; lines.forEach((line, index) => { jsxLines.push(line); if (index !== lines.length - 1) { jsxLines.push(
); } }); nodeValue = jsxLines; } return node.marks.reduce((value: ReactNode, mark: Mark): ReactNode => { if (!renderMark[mark.type]) { return value; } return renderMark[mark.type](value); }, nodeValue); } else { const children: ReactNode = nodeListToReactComponents(node.content, options); if (!node.nodeType || !renderNode[node.nodeType]) { return <>{children}>; } return renderNode[node.nodeType](node, children); } } ================================================ FILE: packages/rich-text-react-renderer/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "declarationDir": "dist/types", "outDir": "dist/lib", "typeRoots": ["../../node_modules/@types", "node_modules/@types"], "jsx": "react" }, "include": ["src"] } ================================================ FILE: packages/rich-text-types/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [17.2.7](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@17.2.6...@contentful/rich-text-types@17.2.7) (2026-04-09) ### Bug Fixes - **rich-text-types:** improve esm compability [ZEND-7778] ([#1073](https://github.com/contentful/rich-text/issues/1073)) ([204aaec](https://github.com/contentful/rich-text/commit/204aaecc3893633c081986f44896e14272fa376a)) ## [17.2.6](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@17.2.5...@contentful/rich-text-types@17.2.6) (2026-04-08) **Note:** Version bump only for package @contentful/rich-text-types ## [17.2.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@17.2.4...@contentful/rich-text-types@17.2.5) (2025-11-04) **Note:** Version bump only for package @contentful/rich-text-types ## [17.2.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@17.2.3...@contentful/rich-text-types@17.2.4) (2025-09-23) ### Bug Fixes - **build:** resolve ESM import failures in rich-text-types [TOL-3435] ([#940](https://github.com/contentful/rich-text/issues/940)) ([5af12af](https://github.com/contentful/rich-text/commit/5af12af168a6005826216c2090fc989db3f8fb03)) ## [17.2.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@17.2.2...@contentful/rich-text-types@17.2.3) (2025-09-23) ### Bug Fixes - revert lingui [ZEND-6939] ([#937](https://github.com/contentful/rich-text/issues/937)) ([a55a67b](https://github.com/contentful/rich-text/commit/a55a67b1f1b9739801bc5bc3a9885701ff970cc0)) ## [17.2.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@17.2.1...@contentful/rich-text-types@17.2.2) (2025-09-10) ### Bug Fixes - **rich-text-types:** add .js extension for esm [] ([#926](https://github.com/contentful/rich-text/issues/926)) ([b218716](https://github.com/contentful/rich-text/commit/b218716f27a0fe8ad71d0dd8ce551f2ecd2334d6)) ## [17.2.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@17.2.0...@contentful/rich-text-types@17.2.1) (2025-09-09) ### Bug Fixes - bundle issues after swc migration [TOL-3396] ([#924](https://github.com/contentful/rich-text/issues/924)) ([22245f0](https://github.com/contentful/rich-text/commit/22245f0648b0164ad29dd0dfe789cd83f78283e8)) # [17.2.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@17.1.0...@contentful/rich-text-types@17.2.0) (2025-09-04) ### Features - migrate to swc and use lingui for i18n [TOL-3288] ([#914](https://github.com/contentful/rich-text/issues/914)) ([b6782a9](https://github.com/contentful/rich-text/commit/b6782a9658b24944ccce2676f06efb1a527d9936)) # [17.1.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@17.0.1...@contentful/rich-text-types@17.1.0) (2025-07-15) ### Features - add an option to strip empty trailing paragraphs [TOL-3193] ([#892](https://github.com/contentful/rich-text/issues/892)) ([9beff0e](https://github.com/contentful/rich-text/commit/9beff0e4cba3e79dc68e6a0725e843c5d642eb87)) ## [17.0.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@17.0.0...@contentful/rich-text-types@17.0.1) (2025-06-16) **Note:** Version bump only for package @contentful/rich-text-types # [17.0.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.8.5...@contentful/rich-text-types@17.0.0) (2024-10-29) ### Features - bring rich text validator [TOL-2426] ([#694](https://github.com/contentful/rich-text/issues/694)) ([30893a6](https://github.com/contentful/rich-text/commit/30893a68b171167502135b48258ba4b93c8375c7)) ### BREAKING CHANGES - removed getSchemaWithNodeType in favor of validateRichTextDocument ## [16.8.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.8.4...@contentful/rich-text-types@16.8.5) (2024-09-09) **Note:** Version bump only for package @contentful/rich-text-types ## [16.8.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.8.3...@contentful/rich-text-types@16.8.4) (2024-08-26) **Note:** Version bump only for package @contentful/rich-text-types ## [16.8.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.8.2...@contentful/rich-text-types@16.8.3) (2024-07-29) **Note:** Version bump only for package @contentful/rich-text-types ## [16.8.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.8.1...@contentful/rich-text-types@16.8.2) (2024-07-24) ### Bug Fixes - exporting schema function [] ([#632](https://github.com/contentful/rich-text/issues/632)) ([21bf8a6](https://github.com/contentful/rich-text/commit/21bf8a61883abbb2174524df2ec5be826fd429a3)) ## [16.8.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.8.0...@contentful/rich-text-types@16.8.1) (2024-07-17) ### Bug Fixes - revert schema changes [TOL-2249] ([de0157b](https://github.com/contentful/rich-text/commit/de0157bd8b7b1fd00b58e4b753befb1d9eeecbff)) # [16.8.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.7.0...@contentful/rich-text-types@16.8.0) (2024-07-17) ### Features - update inline enum order [] ([#620](https://github.com/contentful/rich-text/issues/620)) ([8643e11](https://github.com/contentful/rich-text/commit/8643e1126ce765842190a866556d7c0fa45a7ba8)) - update schema for rich text types [TOL-2249] ([#619](https://github.com/contentful/rich-text/issues/619)) ([010831e](https://github.com/contentful/rich-text/commit/010831ea1259f478543845ccd70a3aa80f643db0)) # [16.7.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.6.3...@contentful/rich-text-types@16.7.0) (2024-07-16) ### Features - support ol & ul list validation in table cell [TOL-2249] ([#616](https://github.com/contentful/rich-text/issues/616)) ([f116c33](https://github.com/contentful/rich-text/commit/f116c33d7a26401447836d1230277f6914a31ac1)) ## [16.6.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.6.2...@contentful/rich-text-types@16.6.3) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-types ## [16.6.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.6.1...@contentful/rich-text-types@16.6.2) (2024-07-16) **Note:** Version bump only for package @contentful/rich-text-types ## [16.6.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.6.0...@contentful/rich-text-types@16.6.1) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-types # [16.6.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.5.4...@contentful/rich-text-types@16.6.0) (2024-06-25) ### Features - switch from tslint to eslint [TOL-2218][tol-2203] ([#594](https://github.com/contentful/rich-text/issues/594)) ([c077b5a](https://github.com/contentful/rich-text/commit/c077b5af58f94c8dc6af4715d4b82c2771d643c5)) ## [16.5.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.5.3...@contentful/rich-text-types@16.5.4) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-types ## [16.5.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.5.2...@contentful/rich-text-types@16.5.3) (2024-06-25) **Note:** Version bump only for package @contentful/rich-text-types ## [16.5.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.5.1...@contentful/rich-text-types@16.5.2) (2024-05-28) **Note:** Version bump only for package @contentful/rich-text-types ## [16.5.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.5.0...@contentful/rich-text-types@16.5.1) (2024-05-27) ### Bug Fixes - fix breaking change for copying json ([#573](https://github.com/contentful/rich-text/issues/573)) ([e5d6533](https://github.com/contentful/rich-text/commit/e5d653360735efc395ad8920d8b2bfd24eb827c1)) # [16.5.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.4.0...@contentful/rich-text-types@16.5.0) (2024-05-24) ### Features - add strikethrough support ([#562](https://github.com/contentful/rich-text/issues/562)) ([b87d0c3](https://github.com/contentful/rich-text/commit/b87d0c31bccb4012745c0479b2b5c92fc28c1e91)) # [16.4.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.3.5...@contentful/rich-text-types@16.4.0) (2024-05-22) ### Features - upgrading rollup to latest version [TOL-2097] ([#559](https://github.com/contentful/rich-text/issues/559)) ([f14d197](https://github.com/contentful/rich-text/commit/f14d1974590d58f92bbf4cb56644095dba929ad9)) ## [16.3.5](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.3.4...@contentful/rich-text-types@16.3.5) (2024-03-04) ### Bug Fixes - 🐛 export MARKS and EMPTY_DOCUMENT as ESM friendly ([#462](https://github.com/contentful/rich-text/issues/462)) ([7ea8ab3](https://github.com/contentful/rich-text/commit/7ea8ab31bcf82242291ff22640869a23b12dde57)) ## [16.3.4](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.3.3...@contentful/rich-text-types@16.3.4) (2024-01-30) **Note:** Version bump only for package @contentful/rich-text-types ## [16.3.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.3.2...@contentful/rich-text-types@16.3.3) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-types ## [16.3.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.3.1...@contentful/rich-text-types@16.3.2) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-types ## [16.3.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.3.0...@contentful/rich-text-types@16.3.1) (2024-01-23) **Note:** Version bump only for package @contentful/rich-text-types # [16.3.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.2.1...@contentful/rich-text-types@16.3.0) (2023-09-12) ### Features - add new nodes [DANTE-1157] ([#494](https://github.com/contentful/rich-text/issues/494)) ([b1fa6df](https://github.com/contentful/rich-text/commit/b1fa6dffc4bd7e1d367e9ce2cfddffe4fe07be47)) ## [16.2.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.2.0...@contentful/rich-text-types@16.2.1) (2023-08-04) **Note:** Version bump only for package @contentful/rich-text-types # [16.2.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.1.0...@contentful/rich-text-types@16.2.0) (2023-05-26) ### Features - 🎸 add `getRichTextResourceLinks` ([#465](https://github.com/contentful/rich-text/issues/465)) ([5746ba6](https://github.com/contentful/rich-text/commit/5746ba652ae5eac2df27d05193e19aef9e85c438)) # [16.1.0](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.0.3...@contentful/rich-text-types@16.1.0) (2023-05-04) ### Features - add embedded-resource-block to the list of supported block elements ([#461](https://github.com/contentful/rich-text/issues/461)) ([c85fd63](https://github.com/contentful/rich-text/commit/c85fd632d5166bf6a4bd30adbc3ed35668f2e7d5)) ## [16.0.3](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.0.2...@contentful/rich-text-types@16.0.3) (2023-03-14) **Note:** Version bump only for package @contentful/rich-text-types ## [16.0.2](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.0.1...@contentful/rich-text-types@16.0.2) (2022-12-01) **Note:** Version bump only for package @contentful/rich-text-types ## [16.0.1](https://github.com/contentful/rich-text/compare/@contentful/rich-text-types@16.0.0...@contentful/rich-text-types@16.0.1) (2022-12-01) **Note:** Version bump only for package @contentful/rich-text-types # 16.0.0 (2022-12-01) ## 15.15.1 (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # 15.15.0 (2022-11-29) ### Features - adding v1 marks to rich text types [TOL-786] ([#416](https://github.com/contentful/rich-text/issues/416)) ([8885de8](https://github.com/contentful/rich-text/commit/8885de8ebba7736de46b0b8892a8aa15bb8d7ba4)) ## 15.14.1 (2022-11-23) # 15.14.0 (2022-11-14) ### Features - add super/sub script types ([#391](https://github.com/contentful/rich-text/issues/391)) ([2562f66](https://github.com/contentful/rich-text/commit/2562f66278f0eff4eeeb367025d4b465773893d1)) ## 15.13.2 (2022-09-07) ### Bug Fixes - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) - revert change to monorepo tsconfig, apply to tsconfig in rich-text-types ([#358](https://github.com/contentful/rich-text/issues/358)) ([56126cc](https://github.com/contentful/rich-text/commit/56126cc9ed3704f21b89d2dbded160be0265f153)) ## 15.12.1 (2022-04-21) # 15.12.0 (2022-03-25) ### Features - enforce minItems in table-related node types ([#314](https://github.com/contentful/rich-text/issues/314)) ([1125331](https://github.com/contentful/rich-text/commit/112533100f66ae01cd9944069dc62fc95f1737a5)) ## 15.11.1 (2022-01-04) ### Bug Fixes - **rich-text-types:** remove RT validation helpers ([#302](https://github.com/contentful/rich-text/issues/302)) ([fcd3a27](https://github.com/contentful/rich-text/commit/fcd3a277952f53eb3ae6ebb559ae6a02f5553c87)), closes [#295](https://github.com/contentful/rich-text/issues/295) [#274](https://github.com/contentful/rich-text/issues/274) # 15.11.0 (2021-12-27) ### Features - **rich-text-types:** expose HEADINGS array ([#301](https://github.com/contentful/rich-text/issues/301)) ([758539d](https://github.com/contentful/rich-text/commit/758539d46f3db13c21ca2f6d74a389a6fef21803)) ## 15.10.1 (2021-12-21) # 15.10.0 (2021-12-15) ### Features - support custom error transformer ([#296](https://github.com/contentful/rich-text/issues/296)) ([9449b87](https://github.com/contentful/rich-text/commit/9449b87fc063a00f11cfe7b2bc0fdb4d91251c69)) ## 15.9.1 (2021-12-10) ### Bug Fixes - **rich-text-types:** resolve generated JSON schemas ([#294](https://github.com/contentful/rich-text/issues/294)) ([1e5b4c4](https://github.com/contentful/rich-text/commit/1e5b4c474e1e27e97df177748c0c8df365a2ab71)) # 15.9.0 (2021-12-09) ### Features - **rich-text-types:** expose RT validation helper ([#292](https://github.com/contentful/rich-text/issues/292)) ([fc5a7cc](https://github.com/contentful/rich-text/commit/fc5a7cc27244f293a9a50acd785f7edcdaaa96ea)) # 15.7.0 (2021-11-11) ### Features - **rich-text-types:** Add TEXT_CONTAINERS ([#286](https://github.com/contentful/rich-text/issues/286)) ([3356ea8](https://github.com/contentful/rich-text/commit/3356ea815a46901a6637f177b04bcf1926adc88d)) ## 15.6.2 (2021-11-05) ## 15.6.1 (2021-11-05) # 15.6.0 (2021-11-04) ## 15.5.1 (2021-10-25) ### Bug Fixes - npm 15.5.0 ([#280](https://github.com/contentful/rich-text/issues/280)) ([e7aeba4](https://github.com/contentful/rich-text/commit/e7aeba49a3074fc9eae6aee569db4e30d1acb8b8)) # 15.5.0 (2021-10-25) ### Features - add v1 node types constraints ([#279](https://github.com/contentful/rich-text/issues/279)) ([5026023](https://github.com/contentful/rich-text/commit/5026023610ec1439f24fd32df9977c2cd4c13e86)) ## 15.3.6 (2021-09-15) ### Bug Fixes - allow table-row type to have both cells ([#267](https://github.com/contentful/rich-text/issues/267)) ([ec40598](https://github.com/contentful/rich-text/commit/ec405982109cb1c16e7adf71a541a98270d7f45b)) ## 15.3.5 (2021-09-13) ### Bug Fixes - **rich-text-types:** forbid Tables inside ListItem ([#266](https://github.com/contentful/rich-text/issues/266)) ([fc338bf](https://github.com/contentful/rich-text/commit/fc338bf040b8718057717d2681f800d5e26ba59d)) ## 15.3.3 (2021-09-07) ## 15.3.2 (2021-09-07) ## 15.3.1 (2021-09-07) # 15.3.0 (2021-09-06) ### Bug Fixes - allow the first item in Table to be a TableHeaderRow ([9a6de55](https://github.com/contentful/rich-text/commit/9a6de55ed97ca413d3922958850b8215922c06b5)) - don't export the helper TableHeaderRow type ([179c3cb](https://github.com/contentful/rich-text/commit/179c3cb10c0f65725297445afe955f0cca135005)) - typo ([2282665](https://github.com/contentful/rich-text/commit/2282665924404771b3353dd9b380d8863f1d4c41)) ### Features - add TableHeaderCell type ([5e25ac9](https://github.com/contentful/rich-text/commit/5e25ac9f35be2ad6c7ad7857cbde808cbc3437f9)) # 15.1.0 (2021-08-02) ### Features - add rowspan ([e0264fb](https://github.com/contentful/rich-text/commit/e0264fbe4d9467eedf78b7c5c3a9825a584124cf)) # 15.0.0 (2021-06-15) ### Features - add table markup support ([c6ae127](https://github.com/contentful/rich-text/commit/c6ae127aa460f4cd26cd6b671cd43fd2714bc650)) ## 14.1.2 (2020-11-02) ### Bug Fixes - 🐛 configure rollup copy correctly ([3446a33](https://github.com/contentful/rich-text/commit/3446a33fc5711087095a58b088828dfe6066bc7f)) ## 14.0.1 (2020-01-30) ### Features - 🎸 emptyDoc representing an empty RT document ([80315ef](https://github.com/contentful/rich-text/commit/80315ef5130b79336336ba31de8a55e42bafe319)) # 14.0.0 (2020-01-28) # 13.4.0 (2019-08-01) # 13.1.0 (2019-03-04) # 13.0.0 (2019-01-22) ## 12.1.2 (2018-12-14) ## 12.0.1 (2018-12-04) ### Bug Fixes - 🐛 add Object.values and Array.prototype.includes polyfills ([9e2ffb4](https://github.com/contentful/rich-text/commit/9e2ffb46e3564a523c74504270b29ccc8b8249ad)) # 12.0.0 (2018-11-29) # 11.0.0 (2018-11-27) ### Bug Fixes - 🐛 Removes obsolete field `title` from Hyperlinks ([8cb9027](https://github.com/contentful/rich-text/commit/8cb90279f5348a7fb59f211c2ba209a28fd432be)) # 10.3.0 (2018-11-26) # 10.2.0 (2018-11-19) ### Features - 🎸 Add rich-text-references ([363b4e5](https://github.com/contentful/rich-text/commit/363b4e509e94af0932fd7cece8e56beafe8d67d2)) # 10.1.0 (2018-11-16) ### Features - 🎸 adds json schema generation to rich text types ([8916140](https://github.com/contentful/rich-text/commit/89161404eb911f126be23ba2a146ebf748f7489e)) - 🎸 introduces top-level-block type ([a6bf35e](https://github.com/contentful/rich-text/commit/a6bf35e7c9ca35915a512de774b3a3fdc4c76e5d)) ## 10.0.1 (2018-11-08) # 10.0.0 (2018-11-02) ### Features - 🎸 removes nodeClass from Node interface ([09b8162](https://github.com/contentful/rich-text/commit/09b8162bcba65bc13afa14b2b5ff046c9fed7b3b)) ### BREAKING CHANGES - Removes accidentally added nodeClass ## 9.0.2 (2018-10-31) # 9.0.0 (2018-10-30) ### Features - 🎸 Explicitly declare nodeClass and nodeType in core types ([0749c61](https://github.com/contentful/rich-text/commit/0749c6199bb2681509608539c76bd8149cade564)) ### BREAKING CHANGES - Potentially breaks TypeScript libraries pulling this in as a dependency if they are not explicitly stating nodeClass and nodeType. ## 8.0.3 (2018-10-30) ## 8.0.2 (2018-10-29) ## 8.0.1 (2018-10-26) ### Bug Fixes - 🐛 Revert mock hypenation commits ([bde9432](https://github.com/contentful/rich-text/commit/bde94323fcc02bf5ab3feeef46a9d8fc8b08d59b)) ### Features - 🎸 take all packages out of "demo mode" ([815f18b](https://github.com/contentful/rich-text/commit/815f18be6a914e7e4782790ee46053689712494b)) ### BREAKING CHANGES - renames all packages # 6.0.0 (2018-10-24) ## [15.15.1](https://github.com/contentful/rich-text/compare/v15.15.0...v15.15.1) (2022-11-30) ### Bug Fixes - **release:** switch to yarn ([#420](https://github.com/contentful/rich-text/issues/420)) ([0e53501](https://github.com/contentful/rich-text/commit/0e53501eb94b3d1c76ac88ca30943d2675e536c8)) # [15.15.0](https://github.com/contentful/rich-text/compare/v15.14.1...v15.15.0) (2022-11-29) ### Features - adding v1 marks to rich text types [TOL-786] ([#416](https://github.com/contentful/rich-text/issues/416)) ([8885de8](https://github.com/contentful/rich-text/commit/8885de8ebba7736de46b0b8892a8aa15bb8d7ba4)) ## [15.14.1](https://github.com/contentful/rich-text/compare/v15.14.0...v15.14.1) (2022-11-23) **Note:** Version bump only for package @contentful/rich-text-types # [15.14.0](https://github.com/contentful/rich-text/compare/v15.13.2...v15.14.0) (2022-11-14) ### Features - add super/sub script types ([#391](https://github.com/contentful/rich-text/issues/391)) ([2562f66](https://github.com/contentful/rich-text/commit/2562f66278f0eff4eeeb367025d4b465773893d1)) ## [15.13.2](https://github.com/contentful/rich-text/compare/v15.13.1...v15.13.2) (2022-09-07) ### Bug Fixes - add prettier write command ([#345](https://github.com/contentful/rich-text/issues/345)) ([0edad4c](https://github.com/contentful/rich-text/commit/0edad4c3176cea85d56a55fc5f4072419d730c8a)) - revert change to monorepo tsconfig, apply to tsconfig in rich-text-types ([#358](https://github.com/contentful/rich-text/issues/358)) ([56126cc](https://github.com/contentful/rich-text/commit/56126cc9ed3704f21b89d2dbded160be0265f153)) ## [15.12.1](https://github.com/contentful/rich-text/compare/v15.12.0...v15.12.1) (2022-04-21) **Note:** Version bump only for package @contentful/rich-text-types # [15.12.0](https://github.com/contentful/rich-text/compare/v15.11.2...v15.12.0) (2022-03-25) ### Features - enforce minItems in table-related node types ([#314](https://github.com/contentful/rich-text/issues/314)) ([1125331](https://github.com/contentful/rich-text/commit/112533100f66ae01cd9944069dc62fc95f1737a5)) ## [15.11.1](https://github.com/contentful/rich-text/compare/v15.11.0...v15.11.1) (2022-01-04) ### Bug Fixes - **rich-text-types:** remove RT validation helpers ([#302](https://github.com/contentful/rich-text/issues/302)) ([fcd3a27](https://github.com/contentful/rich-text/commit/fcd3a277952f53eb3ae6ebb559ae6a02f5553c87)), closes [#295](https://github.com/contentful/rich-text/issues/295) [#274](https://github.com/contentful/rich-text/issues/274) # [15.11.0](https://github.com/contentful/rich-text/compare/v15.10.1...v15.11.0) (2021-12-27) ### Features - **rich-text-types:** expose HEADINGS array ([#301](https://github.com/contentful/rich-text/issues/301)) ([758539d](https://github.com/contentful/rich-text/commit/758539d46f3db13c21ca2f6d74a389a6fef21803)) ## [15.10.1](https://github.com/contentful/rich-text/compare/v15.10.0...v15.10.1) (2021-12-21) **Note:** Version bump only for package @contentful/rich-text-types # [15.10.0](https://github.com/contentful/rich-text/compare/v15.9.1...v15.10.0) (2021-12-15) ### Features - support custom error transformer ([#296](https://github.com/contentful/rich-text/issues/296)) ([9449b87](https://github.com/contentful/rich-text/commit/9449b87fc063a00f11cfe7b2bc0fdb4d91251c69)) ## [15.9.1](https://github.com/contentful/rich-text/compare/v15.9.0...v15.9.1) (2021-12-10) ### Bug Fixes - **rich-text-types:** resolve generated JSON schemas ([#294](https://github.com/contentful/rich-text/issues/294)) ([1e5b4c4](https://github.com/contentful/rich-text/commit/1e5b4c474e1e27e97df177748c0c8df365a2ab71)) # [15.9.0](https://github.com/contentful/rich-text/compare/v15.8.0...v15.9.0) (2021-12-09) ### Features - **rich-text-types:** expose RT validation helper ([#292](https://github.com/contentful/rich-text/issues/292)) ([fc5a7cc](https://github.com/contentful/rich-text/commit/fc5a7cc27244f293a9a50acd785f7edcdaaa96ea)) # [15.7.0](https://github.com/contentful/rich-text/compare/v15.6.2...v15.7.0) (2021-11-11) ### Features - **rich-text-types:** Add TEXT_CONTAINERS ([#286](https://github.com/contentful/rich-text/issues/286)) ([3356ea8](https://github.com/contentful/rich-text/commit/3356ea815a46901a6637f177b04bcf1926adc88d)) ## [15.6.2](https://github.com/contentful/rich-text/compare/v15.6.1...v15.6.2) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text-types ## [15.6.1](https://github.com/contentful/rich-text/compare/v15.6.0...v15.6.1) (2021-11-05) **Note:** Version bump only for package @contentful/rich-text-types # [15.6.0](https://github.com/contentful/rich-text/compare/v15.5.1...v15.6.0) (2021-11-04) **Note:** Version bump only for package @contentful/rich-text-types ## [15.5.1](https://github.com/contentful/rich-text/compare/v15.5.0...v15.5.1) (2021-10-25) ### Bug Fixes - npm 15.5.0 ([#280](https://github.com/contentful/rich-text/issues/280)) ([e7aeba4](https://github.com/contentful/rich-text/commit/e7aeba49a3074fc9eae6aee569db4e30d1acb8b8)) # [15.5.0](https://github.com/contentful/rich-text/compare/v15.4.0...v15.5.0) (2021-10-25) ### Features - add v1 node types constraints ([#279](https://github.com/contentful/rich-text/issues/279)) ([5026023](https://github.com/contentful/rich-text/commit/5026023610ec1439f24fd32df9977c2cd4c13e86)) ## [15.3.6](https://github.com/contentful/rich-text/compare/v15.3.5...v15.3.6) (2021-09-15) ### Bug Fixes - allow table-row type to have both cells ([#267](https://github.com/contentful/rich-text/issues/267)) ([ec40598](https://github.com/contentful/rich-text/commit/ec405982109cb1c16e7adf71a541a98270d7f45b)) ## [15.3.5](https://github.com/contentful/rich-text/compare/v15.3.4...v15.3.5) (2021-09-13) ### Bug Fixes - **rich-text-types:** forbid Tables inside ListItem ([#266](https://github.com/contentful/rich-text/issues/266)) ([fc338bf](https://github.com/contentful/rich-text/commit/fc338bf040b8718057717d2681f800d5e26ba59d)) ## [15.3.3](https://github.com/contentful/rich-text/compare/v15.3.2...v15.3.3) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-types ## [15.3.2](https://github.com/contentful/rich-text/compare/v15.3.1...v15.3.2) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-types ## [15.3.1](https://github.com/contentful/rich-text/compare/v15.3.0...v15.3.1) (2021-09-07) **Note:** Version bump only for package @contentful/rich-text-types # [15.3.0](https://github.com/contentful/rich-text/compare/v15.2.0...v15.3.0) (2021-09-06) ### Bug Fixes - allow the first item in Table to be a TableHeaderRow ([9a6de55](https://github.com/contentful/rich-text/commit/9a6de55ed97ca413d3922958850b8215922c06b5)) - don't export the helper TableHeaderRow type ([179c3cb](https://github.com/contentful/rich-text/commit/179c3cb10c0f65725297445afe955f0cca135005)) - typo ([2282665](https://github.com/contentful/rich-text/commit/2282665924404771b3353dd9b380d8863f1d4c41)) ### Features - add TableHeaderCell type ([5e25ac9](https://github.com/contentful/rich-text/commit/5e25ac9f35be2ad6c7ad7857cbde808cbc3437f9)) # [15.2.0](https://github.com/contentful/rich-text/compare/v15.2.0...v15.1.0) (2021-09-06) ### Bug Fixes - allow the first item in Table to be a TableHeaderRow ([9a6de55](https://github.com/contentful/rich-text/commit/9a6de55ed97ca413d3922958850b8215922c06b5)) - don't export the helper TableHeaderRow type ([179c3cb](https://github.com/contentful/rich-text/commit/179c3cb10c0f65725297445afe955f0cca135005)) - typo ([2282665](https://github.com/contentful/rich-text/commit/2282665924404771b3353dd9b380d8863f1d4c41)) ### Features - add TableHeaderCell type ([5e25ac9](https://github.com/contentful/rich-text/commit/5e25ac9f35be2ad6c7ad7857cbde808cbc3437f9)) # [15.1.0](https://github.com/contentful/rich-text/compare/v15.0.0...v15.1.0) (2021-08-02) ### Features - add rowspan ([e0264fb](https://github.com/contentful/rich-text/commit/e0264fbe4d9467eedf78b7c5c3a9825a584124cf)) # [15.0.0](https://github.com/contentful/rich-text/compare/v14.2.0...v15.0.0) (2021-06-15) ### Features - add table markup support ([c6ae127](https://github.com/contentful/rich-text/commit/c6ae127aa460f4cd26cd6b671cd43fd2714bc650)) ## [14.1.2](https://github.com/contentful/rich-text/compare/v14.0.1...v14.1.2) (2020-11-02) ### Bug Fixes - 🐛 configure rollup copy correctly ([3446a33](https://github.com/contentful/rich-text/commit/3446a33fc5711087095a58b088828dfe6066bc7f)) ## [14.0.1](https://github.com/contentful/rich-text/compare/v14.0.0...v14.0.1) (2020-01-30) ### Features - 🎸 emptyDoc representing an empty RT document ([80315ef](https://github.com/contentful/rich-text/commit/80315ef5130b79336336ba31de8a55e42bafe319)) # [14.0.0](https://github.com/contentful/rich-text/compare/v13.4.0...v14.0.0) (2020-01-28) # [13.4.0](https://github.com/contentful/rich-text/compare/v13.3.0...v13.4.0) (2019-08-01) # [13.1.0](https://github.com/contentful/rich-text/compare/v13.0.1...v13.1.0) (2019-03-04) # [13.0.0](https://github.com/contentful/rich-text/compare/v12.2.1...v13.0.0) (2019-01-22) ## [12.1.2](https://github.com/contentful/rich-text/compare/v12.1.1...v12.1.2) (2018-12-14) ## [12.0.1](https://github.com/contentful/rich-text/compare/v12.0.0...v12.0.1) (2018-12-04) ### Bug Fixes - 🐛 add Object.values and Array.prototype.includes polyfills ([9e2ffb4](https://github.com/contentful/rich-text/commit/9e2ffb46e3564a523c74504270b29ccc8b8249ad)) # [12.0.0](https://github.com/contentful/rich-text/compare/v11.0.0...v12.0.0) (2018-11-29) # [11.0.0](https://github.com/contentful/rich-text/compare/v10.3.0...v11.0.0) (2018-11-27) ### Bug Fixes - 🐛 Removes obsolete field `title` from Hyperlinks ([8cb9027](https://github.com/contentful/rich-text/commit/8cb90279f5348a7fb59f211c2ba209a28fd432be)) # [10.3.0](https://github.com/contentful/rich-text/compare/v10.2.0...v10.3.0) (2018-11-26) # [10.2.0](https://github.com/contentful/rich-text/compare/v10.1.0...v10.2.0) (2018-11-19) ### Features - 🎸 Add rich-text-references ([363b4e5](https://github.com/contentful/rich-text/commit/363b4e509e94af0932fd7cece8e56beafe8d67d2)) # [10.1.0](https://github.com/contentful/rich-text/compare/v10.0.5...v10.1.0) (2018-11-16) ### Features - 🎸 adds json schema generation to rich text types ([8916140](https://github.com/contentful/rich-text/commit/89161404eb911f126be23ba2a146ebf748f7489e)) - 🎸 introduces top-level-block type ([a6bf35e](https://github.com/contentful/rich-text/commit/a6bf35e7c9ca35915a512de774b3a3fdc4c76e5d)) ## [10.0.1](https://github.com/contentful/rich-text/compare/v10.0.0...v10.0.1) (2018-11-08) # [10.0.0](https://github.com/contentful/rich-text/compare/v9.0.2...v10.0.0) (2018-11-02) ### Features - 🎸 removes nodeClass from Node interface ([09b8162](https://github.com/contentful/rich-text/commit/09b8162bcba65bc13afa14b2b5ff046c9fed7b3b)) ### BREAKING CHANGES - Removes accidentally added nodeClass ## [9.0.2](https://github.com/contentful/rich-text/compare/v9.0.1...v9.0.2) (2018-10-31) # [9.0.0](https://github.com/contentful/rich-text/compare/v8.0.3...v9.0.0) (2018-10-30) ### Features - 🎸 Explicitly declare nodeClass and nodeType in core types ([0749c61](https://github.com/contentful/rich-text/commit/0749c6199bb2681509608539c76bd8149cade564)) ### BREAKING CHANGES - Potentially breaks TypeScript libraries pulling this in as a dependency if they are not explicitly stating nodeClass and nodeType. ## [8.0.3](https://github.com/contentful/rich-text/compare/v8.0.2...v8.0.3) (2018-10-30) ## [8.0.2](https://github.com/contentful/rich-text/compare/v8.0.1...v8.0.2) (2018-10-29) ## [8.0.1](https://github.com/contentful/rich-text/compare/v8.0.0...v8.0.1) (2018-10-26) ### Bug Fixes - 🐛 Revert mock hypenation commits ([bde9432](https://github.com/contentful/rich-text/commit/bde94323fcc02bf5ab3feeef46a9d8fc8b08d59b)) ### Features - 🎸 take all packages out of "demo mode" ([815f18b](https://github.com/contentful/rich-text/commit/815f18be6a914e7e4782790ee46053689712494b)) ### BREAKING CHANGES - renames all packages # 6.0.0 (2018-10-24) ================================================ FILE: packages/rich-text-types/README.md ================================================ # rich-text-types Type definitions and constants for the Contentful rich text field type. ================================================ FILE: packages/rich-text-types/__mocks__/@lingui/core/macro.js ================================================ const t = ({ _id, message }) => message; const plural = (count, options) => { let message = options.other; if (count === 0) { message = options.zero || options[0] || options.other; } if (count === 1) { message = options.one || options.other; } return message.replace('#', count); }; module.exports = { t, plural }; ================================================ FILE: packages/rich-text-types/__test__/helpers.test.ts ================================================ import { BLOCKS } from '../src/blocks'; import { helpers } from '../src/index'; import { Document, Mark } from '../src/types'; describe('helpers', () => { describe('isEmptyParagraph', () => { it('returns true for empty paragraph', () => { const node = { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text' as const, value: '', marks: [] as Mark[], data: {}, }, ], }; expect(helpers.isEmptyParagraph(node)).toBe(true); }); it('returns false for non-empty paragraph', () => { const node = { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text' as const, value: 'Hello world', marks: [] as Mark[], data: {}, }, ], }; expect(helpers.isEmptyParagraph(node)).toBe(false); }); it('returns false for paragraph with multiple text nodes', () => { const node = { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text' as const, value: '', marks: [] as Mark[], data: {}, }, { nodeType: 'text' as const, value: '', marks: [] as Mark[], data: {}, }, ], }; expect(helpers.isEmptyParagraph(node)).toBe(false); }); it('returns false for non-paragraph node', () => { const node = { nodeType: BLOCKS.HEADING_1, data: {}, content: [ { nodeType: 'text' as const, value: '', marks: [] as Mark[], data: {}, }, ], }; expect(helpers.isEmptyParagraph(node)).toBe(false); }); }); describe('stripEmptyTrailingParagraphFromDocument', () => { it('strips empty trailing paragraph', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text' as const, value: 'Hello world', marks: [] as Mark[], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text' as const, value: '', marks: [] as Mark[], data: {}, }, ], }, ], }; const result = helpers.stripEmptyTrailingParagraphFromDocument(document); expect(result.content).toHaveLength(1); expect(result.content[0].nodeType).toBe(BLOCKS.PARAGRAPH); }); it('does not strip empty trailing paragraph when it is the only child', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text' as const, value: '', marks: [] as Mark[], data: {}, }, ], }, ], }; const result = helpers.stripEmptyTrailingParagraphFromDocument(document); expect(result.content).toHaveLength(1); }); it('does not strip non-empty trailing paragraph', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text' as const, value: 'Hello world', marks: [] as Mark[], data: {}, }, ], }, { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text' as const, value: 'Not empty', marks: [] as Mark[], data: {}, }, ], }, ], }; const result = helpers.stripEmptyTrailingParagraphFromDocument(document); expect(result.content).toHaveLength(2); }); }); }); ================================================ FILE: packages/rich-text-types/__test__/schemaConstraints.test.ts ================================================ import { BLOCKS } from '../src/blocks'; import { CONTAINERS, TEXT_CONTAINERS, VOID_BLOCKS } from '../src/schemaConstraints'; const allKnownBlocks = Object.values(BLOCKS); describe('schema constraints', () => { it('all block node types are either considered a container or void', () => { const blocks = [ BLOCKS.DOCUMENT, // Root block could be in CONTAINERS but isn't. ...VOID_BLOCKS, ...TEXT_CONTAINERS, ...Object.keys(CONTAINERS), ]; expect(blocks).toEqual(expect.arrayContaining(allKnownBlocks)); expect(blocks.length).toEqual(allKnownBlocks.length); }); it('should allow UL_LIST and OL_LIST blocks as children of TABLE_CELL', () => { // Get the children of TABLE_CELL const tableCellChildren = CONTAINERS[BLOCKS.TABLE_CELL]; // Check that UL_LIST and OL_LIST are in the children array expect(tableCellChildren).toContain(BLOCKS.UL_LIST); expect(tableCellChildren).toContain(BLOCKS.OL_LIST); }); }); ================================================ FILE: packages/rich-text-types/__test__/validation.test.ts ================================================ import { BLOCKS } from '../src/blocks'; import { INLINES } from '../src/inlines'; import type { Document } from '../src/types'; import { validateRichTextDocument } from '../src/validator/index'; describe('validation', () => { it('fails if it is not document node', () => { // @ts-expect-error we force a wrong node type to check that it fails const document: Document = { nodeType: BLOCKS.PARAGRAPH, content: [], data: {} }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: ['document'], name: 'in', path: ['nodeType'], value: 'paragraph', }, ]); }); it('fails if it has an invalid shape', () => { // @ts-expect-error we force a wrong node type to check that it fails const document: Document = { nodeType: BLOCKS.DOCUMENT }; expect(validateRichTextDocument(document)).toEqual([ { name: 'required', path: ['content'], details: 'The property "content" is required here', }, { name: 'required', path: ['data'], details: 'The property "data" is required here', }, ]); }); it('fails if it has nested documents', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, // @ts-expect-error we force a wrong node type to check that it fails content: [{ nodeType: BLOCKS.DOCUMENT, content: [], data: {} }], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [ 'blockquote', 'embedded-asset-block', 'embedded-entry-block', 'embedded-resource-block', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5', 'heading-6', 'hr', 'ordered-list', 'paragraph', 'table', 'unordered-list', ], name: 'in', path: ['content', 0, 'nodeType'], value: 'document', }, ]); }); it('fails without a nodeType property', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, // @ts-expect-error we force a wrong node type to check that it fails content: [{ content: [], data: {} }], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [ 'blockquote', 'embedded-asset-block', 'embedded-entry-block', 'embedded-resource-block', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5', 'heading-6', 'hr', 'ordered-list', 'paragraph', 'table', 'unordered-list', ], name: 'in', path: ['content', 0, 'nodeType'], value: undefined, }, ]); }); it('fails on custom nodeTypes (unknown nodeType)', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, // @ts-expect-error we force a wrong node type to check that it fails content: [{ nodeType: 'custom-node-type', content: [], data: {} }], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [ 'blockquote', 'embedded-asset-block', 'embedded-entry-block', 'embedded-resource-block', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5', 'heading-6', 'hr', 'ordered-list', 'paragraph', 'table', 'unordered-list', ], name: 'in', path: ['content', 0, 'nodeType'], value: 'custom-node-type', }, ]); }); it('fails without a content property', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, // @ts-expect-error we force a wrong node type to check that it fails content: [{ nodeType: BLOCKS.PARAGRAPH, data: {} }], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The property "content" is required here', name: 'required', path: ['content', 0, 'content'], value: undefined, }, ]); }); it('fails with a invalid content property', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, // @ts-expect-error we force a wrong type to check that it fails content: [{ nodeType: BLOCKS.PARAGRAPH, content: 'Hello World', data: {} }], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The type of "content" is incorrect, expected type: Array', name: 'type', path: ['content', 0, 'content'], type: 'Array', value: 'Hello World', }, ]); }); it('fails without data property', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, // @ts-expect-error we force a wrong node type to check that it fails content: [{ nodeType: BLOCKS.PARAGRAPH, content: [] }], }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The property "data" is required here', name: 'required', path: ['data'], }, ]); }); it('fails with invalid data property', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [{ nodeType: BLOCKS.PARAGRAPH, content: [], data: null }], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The type of "data" is incorrect, expected type: Object', name: 'type', path: ['content', 0, 'data'], type: 'Object', value: null, }, ]); }); it('fails if undefined is in the content list', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [{ nodeType: BLOCKS.PARAGRAPH, content: [], data: {} }, undefined], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The type of "1" is incorrect, expected type: Object', name: 'type', path: ['content', 1], type: 'Object', value: undefined, }, ]); }); it('fails if undefined is in the content list of child nodes', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.UL_LIST, content: [ { nodeType: BLOCKS.LIST_ITEM, content: [{ nodeType: BLOCKS.PARAGRAPH, content: [], data: {} }, undefined], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The type of "1" is incorrect, expected type: Object', name: 'type', path: ['content', 0, 'content', 0, 'content', 1], type: 'Object', value: undefined, }, ]); }); it('fails with unknown properties', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [], data: {}, // @ts-expect-error we force a wrong property to check that it fails myCustomProperty: 'Hello World', }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The property "myCustomProperty" is not expected', name: 'unexpected', path: ['myCustomProperty'], }, ]); }); it.each([BLOCKS.LIST_ITEM, BLOCKS.TABLE_ROW, BLOCKS.TABLE_HEADER_CELL, BLOCKS.TABLE_CELL])( 'fails with a invalid block node as children (nodeType: %s) of the root node', (nodeType) => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { // @ts-expect-error we force a wrong node type to check that it fails nodeType, content: [], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [ 'blockquote', 'embedded-asset-block', 'embedded-entry-block', 'embedded-resource-block', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5', 'heading-6', 'hr', 'ordered-list', 'paragraph', 'table', 'unordered-list', ], name: 'in', path: ['content', 0, 'nodeType'], value: nodeType, }, ]); }, ); it.each(Object.values(INLINES))( 'fails with a inline node (%s) as direct children of the root node', (nodeType) => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { // @ts-expect-error we force a wrong node type to check that it fails nodeType, content: [], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [ 'blockquote', 'embedded-asset-block', 'embedded-entry-block', 'embedded-resource-block', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5', 'heading-6', 'hr', 'ordered-list', 'paragraph', 'table', 'unordered-list', ], name: 'in', path: ['content', 0, 'nodeType'], value: nodeType, }, ]); }, ); it('fails with text as a direct children of the root node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { // @ts-expect-error we force a wrong node type to check that it fails nodeType: 'text', data: {}, marks: [], value: 'Hello World', }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [ 'blockquote', 'embedded-asset-block', 'embedded-entry-block', 'embedded-resource-block', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5', 'heading-6', 'hr', 'ordered-list', 'paragraph', 'table', 'unordered-list', ], name: 'in', path: ['content', 0, 'nodeType'], value: 'text', }, ]); }); it('fails with inline node and text as a direct children of the root node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { // @ts-expect-error we force a wrong node type to check that it fails nodeType: 'text', data: {}, marks: [], value: 'Hello World', }, { // @ts-expect-error we force a wrong node type to check that it fails nodeType: INLINES.ASSET_HYPERLINK, data: { target: {} }, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [ 'blockquote', 'embedded-asset-block', 'embedded-entry-block', 'embedded-resource-block', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5', 'heading-6', 'hr', 'ordered-list', 'paragraph', 'table', 'unordered-list', ], name: 'in', path: ['content', 0, 'nodeType'], value: 'text', }, ]); }); it.each([BLOCKS.OL_LIST, BLOCKS.UL_LIST] as const)( 'fails for invalid block nodes inside of (%s)', (nodeType) => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: nodeType, content: [{ nodeType: BLOCKS.PARAGRAPH, content: [], data: {} }], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [BLOCKS.LIST_ITEM], name: 'in', path: ['content', 0, 'content', 0, 'nodeType'], value: BLOCKS.PARAGRAPH, }, ]); }, ); it('fails on text node directly inside of a list item node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.UL_LIST, content: [ { nodeType: BLOCKS.LIST_ITEM, content: [ { nodeType: 'text', data: {}, value: 'Hello World', marks: [], }, ], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [ 'blockquote', 'embedded-asset-block', 'embedded-entry-block', 'embedded-resource-block', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5', 'heading-6', 'hr', 'ordered-list', 'paragraph', 'unordered-list', ], name: 'in', path: ['content', 0, 'content', 0, 'content', 0, 'nodeType'], value: 'text', }, ]); }); it('fails on invalid block nodes inside of a table node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.TABLE, content: [{ nodeType: BLOCKS.PARAGRAPH, content: [], data: {} }], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [BLOCKS.TABLE_ROW], name: 'in', path: ['content', 0, 'content', 0, 'nodeType'], value: BLOCKS.PARAGRAPH, }, ]); }); it('fails on invalid block nodes inside of a table row node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.TABLE, content: [ { nodeType: BLOCKS.TABLE_ROW, content: [{ nodeType: BLOCKS.PARAGRAPH, content: [], data: {} }], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [BLOCKS.TABLE_CELL, BLOCKS.TABLE_HEADER_CELL], name: 'in', path: ['content', 0, 'content', 0, 'content', 0, 'nodeType'], value: BLOCKS.PARAGRAPH, }, ]); }); it('fails on invalid block nodes inside of a table header node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.TABLE, content: [ { nodeType: BLOCKS.TABLE_ROW, content: [{ nodeType: BLOCKS.PARAGRAPH, content: [], data: {} }], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [BLOCKS.TABLE_CELL, BLOCKS.TABLE_HEADER_CELL], name: 'in', path: ['content', 0, 'content', 0, 'content', 0, 'nodeType'], value: BLOCKS.PARAGRAPH, }, ]); }); it.each([BLOCKS.TABLE_CELL, BLOCKS.TABLE_HEADER_CELL] as const)( 'fails on invalid node inside of %s', (nodeType) => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.TABLE, content: [ { nodeType: BLOCKS.TABLE_ROW, content: [ { nodeType, content: [{ nodeType: 'text', data: {}, marks: [], value: 'Hello World' }], data: {}, }, ], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [BLOCKS.PARAGRAPH], name: 'in', path: ['content', 0, 'content', 0, 'content', 0, 'content', 0, 'nodeType'], value: 'text', }, ]); }, ); it('fails if a table node has not at least one table row', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.TABLE, content: [], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Size must be at least 1', min: 1, name: 'size', path: ['content', 0, 'content'], value: [], }, ]); }); it('fails if a table row node has not at least one table cell', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.TABLE, content: [ { nodeType: BLOCKS.TABLE_ROW, content: [], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Size must be at least 1', min: 1, name: 'size', path: ['content', 0, 'content', 0, 'content'], value: [], }, ]); }); it.each([BLOCKS.TABLE_CELL, BLOCKS.TABLE_HEADER_CELL] as const)( 'fails if a %s has not at least one child', (nodeType) => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.TABLE, content: [ { nodeType: BLOCKS.TABLE_ROW, content: [ { nodeType, content: [], data: {}, }, ], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Size must be at least 1', min: 1, name: 'size', path: ['content', 0, 'content', 0, 'content', 0, 'content'], value: [], }, ]); }, ); it('fails if inline nodes contains something else as a inline node or text', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.PARAGRAPH, content: [ { nodeType: INLINES.HYPERLINK, // @ts-expect-error we force a wrong node type to check that it fails content: [{ nodeType: BLOCKS.PARAGRAPH, content: [], data: {} }], data: { uri: '' }, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: ['text'], name: 'in', path: ['content', 0, 'content', 0, 'content', 0, 'nodeType'], value: BLOCKS.PARAGRAPH, }, ]); }); it.each([ BLOCKS.HEADING_1, BLOCKS.HEADING_2, BLOCKS.HEADING_3, BLOCKS.HEADING_4, BLOCKS.HEADING_5, BLOCKS.HEADING_6, ] as const)( 'fails if the headline node (%s) contains something else as a inline or text node', (nodeType) => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType, content: [ { nodeType: BLOCKS.QUOTE, content: [{ nodeType: 'text', value: 'Hello World', data: {}, marks: [] }], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [ 'asset-hyperlink', 'embedded-entry-inline', 'embedded-resource-inline', 'entry-hyperlink', 'hyperlink', 'resource-hyperlink', 'text', ], name: 'in', path: ['content', 0, 'content', 0, 'nodeType'], value: BLOCKS.QUOTE, }, ]); }, ); it('fails on invalid block nodes inside of a quote node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.QUOTE, content: [ { nodeType: BLOCKS.HEADING_1, content: [{ nodeType: 'text', value: 'Hello World', data: {}, marks: [] }], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'Value must be one of expected values', expected: [BLOCKS.PARAGRAPH], name: 'in', path: ['content', 0, 'content', 0, 'nodeType'], value: BLOCKS.HEADING_1, }, ]); }); it('fails without value property on text nodes', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.PARAGRAPH, content: [ { nodeType: 'text', value: null, data: {}, marks: [], }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The type of "value" is incorrect, expected type: String', name: 'type', path: ['content', 0, 'content', 0, 'value'], type: 'String', value: null, }, ]); }); it('fails with invalid row/colspan on table cell nodes', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.TABLE, content: [ { nodeType: BLOCKS.TABLE_ROW, content: [ { nodeType: BLOCKS.TABLE_CELL, content: [ { nodeType: BLOCKS.PARAGRAPH, content: [{ nodeType: 'text', value: 'Hello Table', data: {}, marks: [] }], data: {}, }, ], data: { rowspan: 'argh' }, }, ], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The type of "rowspan" is incorrect, expected type: Number', name: 'type', path: ['content', 0, 'content', 0, 'content', 0, 'data', 'rowspan'], type: 'Number', value: 'argh', }, ]); }); it.each([INLINES.ASSET_HYPERLINK, INLINES.ENTRY_HYPERLINK, INLINES.RESOURCE_HYPERLINK] as const)( 'fails with invalid properties for %s', (nodeType) => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.PARAGRAPH, content: [ { nodeType, data: {}, content: [{ nodeType: 'text', value: `Hello ${nodeType}`, data: {}, marks: [] }], }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The property "target" is required here', name: 'required', path: ['content', 0, 'content', 0, 'data', 'target'], }, ]); }, ); it('fails with invalid properties for hypperlink node', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.PARAGRAPH, content: [ { nodeType: INLINES.HYPERLINK, data: {}, content: [{ nodeType: 'text', value: 'Hello hyperlink', data: {}, marks: [] }], }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The property "uri" is required here', name: 'required', path: ['content', 0, 'content', 0, 'data', 'uri'], }, ]); }); it.each([INLINES.EMBEDDED_ENTRY, INLINES.EMBEDDED_RESOURCE] as const)( 'fails with invalid properties for %s', (nodeType) => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.PARAGRAPH, content: [ { nodeType, data: {}, content: [], }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([ { details: 'The property "target" is required here', name: 'required', path: ['content', 0, 'content', 0, 'data', 'target'], }, ]); }, ); it('succeeds with a valid structure', () => { const document: Document = { nodeType: BLOCKS.DOCUMENT, content: [ { nodeType: BLOCKS.PARAGRAPH, content: [{ nodeType: 'text', value: 'Hello World', data: {}, marks: [] }], data: {}, }, { nodeType: BLOCKS.HEADING_1, content: [{ nodeType: 'text', value: 'Hello Headline', data: {}, marks: [] }], data: {}, }, { nodeType: BLOCKS.UL_LIST, content: [ { nodeType: BLOCKS.LIST_ITEM, content: [ { nodeType: BLOCKS.PARAGRAPH, content: [{ nodeType: 'text', value: 'Hello List', data: {}, marks: [] }], data: {}, }, ], data: {}, }, ], data: {}, }, { nodeType: BLOCKS.TABLE, content: [ { nodeType: BLOCKS.TABLE_ROW, content: [ { nodeType: BLOCKS.TABLE_CELL, content: [ { nodeType: BLOCKS.PARAGRAPH, content: [{ nodeType: 'text', value: 'Hello Table', data: {}, marks: [] }], data: {}, }, ], data: { rowspan: 2, colspan: 2 }, }, ], data: {}, }, ], data: {}, }, ], data: {}, }; expect(validateRichTextDocument(document)).toEqual([]); }); }); ================================================ FILE: packages/rich-text-types/jest.config.js ================================================ /* eslint-disable */ const getBaseConfig = require('../../baseJestConfig'); const packageJson = require('./package.json'); const packageName = packageJson.name.split('@contentful/')[1]; module.exports = { ...getBaseConfig(packageName), }; ================================================ FILE: packages/rich-text-types/package.json ================================================ { "name": "@contentful/rich-text-types", "version": "17.2.7", "main": "dist/cjs/index.js", "module": "dist/esm/index.mjs", "types": "dist/types/index.d.ts", "exports": { ".": { "types": "./dist/types/index.d.ts", "import": "./dist/esm/index.mjs", "require": "./dist/cjs/index.js", "default": "./dist/cjs/index.js" }, "./package.json": "./package.json" }, "files": [ "dist" ], "repository": { "type": "git", "url": "https://github.com/contentful/rich-text.git" }, "license": "MIT", "engines": { "node": ">=20.0.0" }, "publishConfig": { "access": "public", "registry": "https://npm.pkg.github.com/" }, "scripts": { "prebuild": "rimraf dist", "build": "npm run build:types && npm run build:cjs && npm run build:esm && npm run fix-esm-imports", "build:types": "tsc --outDir dist/types --emitDeclarationOnly", "build:cjs": "swc src --strip-leading-paths --config-file ../../.swcrc -d dist/cjs -C module.type=commonjs", "build:esm": "swc src --strip-leading-paths --config-file ../../.swcrc -d dist/esm --out-file-extension mjs -C jsc.target=es2022 -C module.type=es6 -C module.resolveFully=true -C module.outFileExtension=mjs", "fix-esm-imports": "node ./scripts/fix-esm-import-extensions.mjs", "start": "tsc && swc src --strip-leading-paths --config-file ../../.swcrc -d dist/esm --out-file-extension mjs -C module.type=es6 -C module.resolveFully=true -C module.outFileExtension=mjs -w", "test": "jest" }, "devDependencies": { "@types/jest": "^30.0.0", "@types/node": "^25.0.2", "ts-jest": "^29.1.2" } } ================================================ FILE: packages/rich-text-types/scripts/fix-esm-import-extensions.mjs ================================================ // Rewrite relative `*.js` specifiers to `*.mjs` across built ESM files import { readdir, readFile, writeFile } from 'node:fs/promises'; import { dirname, join, extname } from 'node:path'; import { fileURLToPath } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); const esmDir = join(here, '..', 'dist', 'esm'); // Recursively walk all .mjs files under `dir` async function* walk(dir) { for (const entry of await readdir(dir, { withFileTypes: true })) { const p = join(dir, entry.name); if (entry.isDirectory()) yield* walk(p); else if (extname(p) === '.mjs') yield p; } } const REWRITES = [ // import ... from './x.js' [/(from\s+['"])(\.{1,2}\/[^'"]+)\.js(['"])/g, '$1$2.mjs$3'], // export ... from './x.js' [/(export\s+(?:\*|{[\s\S]*?})\s+from\s+['"])(\.{1,2}\/[^'"]+)\.js(['"])/g, '$1$2.mjs$3'], // import('./x.js') [/(import\(\s*['"])(\.{1,2}\/[^'"]+)\.js(['"]\s*\))/g, '$1$2.mjs$3'], ]; function rewrite(code) { return REWRITES.reduce((s, [re, replacement]) => s.replace(re, replacement), code); } for await (const file of walk(esmDir)) { const original = await readFile(file, 'utf8'); const updated = rewrite(original); if (updated !== original) { await writeFile(file, updated, 'utf8'); } } ================================================ FILE: packages/rich-text-types/src/blocks.ts ================================================ /** * Map of all Contentful block types. Blocks contain inline or block nodes. */ export enum BLOCKS { DOCUMENT = 'document', PARAGRAPH = 'paragraph', HEADING_1 = 'heading-1', HEADING_2 = 'heading-2', HEADING_3 = 'heading-3', HEADING_4 = 'heading-4', HEADING_5 = 'heading-5', HEADING_6 = 'heading-6', OL_LIST = 'ordered-list', UL_LIST = 'unordered-list', LIST_ITEM = 'list-item', HR = 'hr', QUOTE = 'blockquote', EMBEDDED_ENTRY = 'embedded-entry-block', EMBEDDED_ASSET = 'embedded-asset-block', EMBEDDED_RESOURCE = 'embedded-resource-block', TABLE = 'table', TABLE_ROW = 'table-row', TABLE_CELL = 'table-cell', TABLE_HEADER_CELL = 'table-header-cell', } ================================================ FILE: packages/rich-text-types/src/emptyDocument.ts ================================================ import { BLOCKS } from './blocks.js'; import { Document } from './types.js'; /** * A rich text document considered to be empty. * Any other document structure than this is not considered empty. */ export const EMPTY_DOCUMENT: Document = { nodeType: BLOCKS.DOCUMENT, data: {}, content: [ { nodeType: BLOCKS.PARAGRAPH, data: {}, content: [ { nodeType: 'text', value: '', marks: [], data: {}, }, ], }, ], }; ================================================ FILE: packages/rich-text-types/src/helpers.ts ================================================ import { BLOCKS } from './blocks.js'; import { INLINES } from './inlines.js'; import { Block, Inline, Node, Text, Document as CDocument } from './types.js'; /** * Tiny replacement for Object.values(object).includes(key) to * avoid including CoreJS polyfills */ function hasValue(obj: Record, value: unknown) { for (const key of Object.keys(obj)) { if (value === obj[key]) { return true; } } return false; } /** * Checks if the node is an instance of Inline. */ export function isInline(node: Node): node is Inline { return hasValue(INLINES, node.nodeType); } /** * Checks if the node is an instance of Block. */ export function isBlock(node: Node): node is Block { return hasValue(BLOCKS, node.nodeType); } /** * Checks if the node is an instance of Text. */ export function isText(node: Node): node is Text { return node.nodeType === 'text'; } /** * Checks if a paragraph is empty (has only one child and that child is an empty string text node) */ export function isEmptyParagraph(node: Block): boolean { if (node.nodeType !== BLOCKS.PARAGRAPH) { return false; } if (node.content.length !== 1) { return false; } const textNode = node.content[0]; return textNode.nodeType === 'text' && (textNode as Text).value === ''; } function isValidDocument(document: unknown): document is CDocument { return ( document != null && typeof document === 'object' && 'content' in document && Array.isArray((document as CDocument).content) ); } const MIN_NODES_FOR_STRIPPING = 2; /** * Strips empty trailing paragraph from a document if enabled * @param document - The rich text document to process * @returns A new document with the empty trailing paragraph removed (if conditions are met) * @example * const processedDoc = stripEmptyTrailingParagraphFromDocument(document); */ export function stripEmptyTrailingParagraphFromDocument(document: CDocument): CDocument { if (!isValidDocument(document) || document.content.length < MIN_NODES_FOR_STRIPPING) { return document; } const lastNode = document.content[document.content.length - 1]; // Check if the last node is an empty paragraph if (isEmptyParagraph(lastNode)) { return { ...document, content: document.content.slice(0, -1), }; } return document; } ================================================ FILE: packages/rich-text-types/src/index.ts ================================================ export { BLOCKS } from './blocks.js'; export { INLINES } from './inlines.js'; export { MARKS } from './marks.js'; export * from './schemaConstraints.js'; export * from './types.js'; export * from './nodeTypes.js'; export { EMPTY_DOCUMENT } from './emptyDocument.js'; import * as helpers from './helpers.js'; export { helpers }; export { validateRichTextDocument } from './validator/index.js'; export type { ValidationError } from './validator/index.js'; ================================================ FILE: packages/rich-text-types/src/inlines.ts ================================================ /** * Map of all Contentful inline types. Inline contain inline or text nodes. * * @note This should be kept in alphabetical order since the * [validation package](https://github.com/contentful/content-stack/tree/master/packages/validation) * relies on the values being in a predictable order. */ export enum INLINES { ASSET_HYPERLINK = 'asset-hyperlink', EMBEDDED_ENTRY = 'embedded-entry-inline', EMBEDDED_RESOURCE = 'embedded-resource-inline', ENTRY_HYPERLINK = 'entry-hyperlink', HYPERLINK = 'hyperlink', RESOURCE_HYPERLINK = 'resource-hyperlink', } ================================================ FILE: packages/rich-text-types/src/marks.ts ================================================ /** * Map of all Contentful marks. */ export enum MARKS { BOLD = 'bold', ITALIC = 'italic', UNDERLINE = 'underline', CODE = 'code', SUPERSCRIPT = 'superscript', SUBSCRIPT = 'subscript', STRIKETHROUGH = 'strikethrough', } ================================================ FILE: packages/rich-text-types/src/nodeTypes.ts ================================================ import { BLOCKS } from './blocks.js'; import { INLINES } from './inlines.js'; import { Block, Inline, ListItemBlock, Text } from './types.js'; // eslint-disable-next-line @typescript-eslint/ban-types type EmptyNodeData = {}; // BLOCKS // Heading export interface Heading1 extends Block { nodeType: BLOCKS.HEADING_1; data: EmptyNodeData; content: Array ; } export interface Heading2 extends Block { nodeType: BLOCKS.HEADING_2; data: EmptyNodeData; content: Array ; } export interface Heading3 extends Block { nodeType: BLOCKS.HEADING_3; data: EmptyNodeData; content: Array ; } export interface Heading4 extends Block { nodeType: BLOCKS.HEADING_4; data: EmptyNodeData; content: Array ; } export interface Heading5 extends Block { nodeType: BLOCKS.HEADING_5; data: EmptyNodeData; content: Array ; } export interface Heading6 extends Block { nodeType: BLOCKS.HEADING_6; data: EmptyNodeData; content: Array ; } // Paragraph export interface Paragraph extends Block { nodeType: BLOCKS.PARAGRAPH; data: EmptyNodeData; content: Array ; } // Quote export interface Quote extends Block { nodeType: BLOCKS.QUOTE; data: EmptyNodeData; content: Paragraph[]; } // Horizontal rule export interface Hr extends Block { nodeType: BLOCKS.HR; /** * * @maxItems 0 */ data: EmptyNodeData; content: Array ; } // OL export interface OrderedList extends Block { nodeType: BLOCKS.OL_LIST; data: EmptyNodeData; content: ListItem[]; } // UL export interface UnorderedList extends Block { nodeType: BLOCKS.UL_LIST; data: EmptyNodeData; content: ListItem[]; } export interface ListItem extends Block { nodeType: BLOCKS.LIST_ITEM; data: EmptyNodeData; content: ListItemBlock[]; } // taken from graphql schema-generator/contentful-types/link.ts export interface Link { sys: { type: 'Link'; linkType: T; id: string; }; } export interface ResourceLink { sys: { type: 'ResourceLink'; linkType: 'Contentful:Entry'; urn: string; }; } export interface EntryLinkBlock extends Block { nodeType: BLOCKS.EMBEDDED_ENTRY; data: { target: Link<'Entry'>; }; /** * * @maxItems 0 */ content: Array ; } export interface AssetLinkBlock extends Block { nodeType: BLOCKS.EMBEDDED_ASSET; data: { target: Link<'Asset'>; }; /** * * @maxItems 0 */ content: Array ; } export interface ResourceLinkBlock extends Block { nodeType: BLOCKS.EMBEDDED_RESOURCE; data: { target: ResourceLink; }; /** * * @maxItems 0 */ content: Array ; } // INLINE export interface EntryLinkInline extends Inline { nodeType: INLINES.EMBEDDED_ENTRY; data: { target: Link<'Entry'>; }; /** * * @maxItems 0 */ content: Text[]; } export interface ResourceLinkInline extends Inline { nodeType: INLINES.EMBEDDED_RESOURCE; data: { target: ResourceLink; }; /** * * @maxItems 0 */ content: Text[]; } export interface Hyperlink extends Inline { nodeType: INLINES.HYPERLINK; data: { uri: string; }; content: Text[]; } export interface AssetHyperlink extends Inline { nodeType: INLINES.ASSET_HYPERLINK; data: { target: Link<'Asset'>; }; content: Text[]; } export interface EntryHyperlink extends Inline { nodeType: INLINES.ENTRY_HYPERLINK; data: { target: Link<'Entry'>; }; content: Text[]; } export interface ResourceHyperlink extends Inline { nodeType: INLINES.RESOURCE_HYPERLINK; data: { target: ResourceLink; }; content: Text[]; } export interface TableCell extends Block { nodeType: BLOCKS.TABLE_HEADER_CELL | BLOCKS.TABLE_CELL; data: { colspan?: number; rowspan?: number; }; /** * @minItems 1 */ content: Paragraph[]; } export interface TableHeaderCell extends TableCell { nodeType: BLOCKS.TABLE_HEADER_CELL; } // An abstract table row can have both table cell types export interface TableRow extends Block { nodeType: BLOCKS.TABLE_ROW; data: EmptyNodeData; /** * @minItems 1 */ content: TableCell[]; } export interface Table extends Block { nodeType: BLOCKS.TABLE; data: EmptyNodeData; /** * @minItems 1 */ content: TableRow[]; } ================================================ FILE: packages/rich-text-types/src/schemaConstraints.ts ================================================ import { BLOCKS } from './blocks.js'; import { INLINES } from './inlines.js'; import { MARKS } from './marks.js'; export type TopLevelBlockEnum = | BLOCKS.PARAGRAPH | BLOCKS.HEADING_1 | BLOCKS.HEADING_2 | BLOCKS.HEADING_3 | BLOCKS.HEADING_4 | BLOCKS.HEADING_5 | BLOCKS.HEADING_6 | BLOCKS.OL_LIST | BLOCKS.UL_LIST | BLOCKS.HR | BLOCKS.QUOTE | BLOCKS.EMBEDDED_ENTRY | BLOCKS.EMBEDDED_ASSET | BLOCKS.EMBEDDED_RESOURCE | BLOCKS.TABLE; /** * Array of all top level block types. * Only these block types can be the direct children of the document. */ export const TOP_LEVEL_BLOCKS: TopLevelBlockEnum[] = [ BLOCKS.PARAGRAPH, BLOCKS.HEADING_1, BLOCKS.HEADING_2, BLOCKS.HEADING_3, BLOCKS.HEADING_4, BLOCKS.HEADING_5, BLOCKS.HEADING_6, BLOCKS.OL_LIST, BLOCKS.UL_LIST, BLOCKS.HR, BLOCKS.QUOTE, BLOCKS.EMBEDDED_ENTRY, BLOCKS.EMBEDDED_ASSET, BLOCKS.EMBEDDED_RESOURCE, BLOCKS.TABLE, ]; export type ListItemBlockEnum = | BLOCKS.PARAGRAPH | BLOCKS.HEADING_1 | BLOCKS.HEADING_2 | BLOCKS.HEADING_3 | BLOCKS.HEADING_4 | BLOCKS.HEADING_5 | BLOCKS.HEADING_6 | BLOCKS.OL_LIST | BLOCKS.UL_LIST | BLOCKS.HR | BLOCKS.QUOTE | BLOCKS.EMBEDDED_ENTRY | BLOCKS.EMBEDDED_ASSET | BLOCKS.EMBEDDED_RESOURCE; /** * Array of all allowed block types inside list items */ export const LIST_ITEM_BLOCKS: TopLevelBlockEnum[] = [ BLOCKS.PARAGRAPH, BLOCKS.HEADING_1, BLOCKS.HEADING_2, BLOCKS.HEADING_3, BLOCKS.HEADING_4, BLOCKS.HEADING_5, BLOCKS.HEADING_6, BLOCKS.OL_LIST, BLOCKS.UL_LIST, BLOCKS.HR, BLOCKS.QUOTE, BLOCKS.EMBEDDED_ENTRY, BLOCKS.EMBEDDED_ASSET, BLOCKS.EMBEDDED_RESOURCE, ]; export const TABLE_BLOCKS = [ BLOCKS.TABLE, BLOCKS.TABLE_ROW, BLOCKS.TABLE_CELL, BLOCKS.TABLE_HEADER_CELL, ]; /** * Array of all void block types */ export const VOID_BLOCKS = [ BLOCKS.HR, BLOCKS.EMBEDDED_ENTRY, BLOCKS.EMBEDDED_ASSET, BLOCKS.EMBEDDED_RESOURCE, ]; /** * Dictionary of all container block types, and the set block types they accept as children. * * Note: This does not include `[BLOCKS.DOCUMENT]: TOP_LEVEL_BLOCKS` */ export const CONTAINERS = { [BLOCKS.OL_LIST]: [BLOCKS.LIST_ITEM], [BLOCKS.UL_LIST]: [BLOCKS.LIST_ITEM], [BLOCKS.LIST_ITEM]: LIST_ITEM_BLOCKS, [BLOCKS.QUOTE]: [BLOCKS.PARAGRAPH], [BLOCKS.TABLE]: [BLOCKS.TABLE_ROW], [BLOCKS.TABLE_ROW]: [BLOCKS.TABLE_CELL, BLOCKS.TABLE_HEADER_CELL], [BLOCKS.TABLE_CELL]: [BLOCKS.PARAGRAPH, BLOCKS.UL_LIST, BLOCKS.OL_LIST], [BLOCKS.TABLE_HEADER_CELL]: [BLOCKS.PARAGRAPH], }; /** * Array of all heading levels */ export const HEADINGS = [ BLOCKS.HEADING_1, BLOCKS.HEADING_2, BLOCKS.HEADING_3, BLOCKS.HEADING_4, BLOCKS.HEADING_5, BLOCKS.HEADING_6, ]; /** * Array of all block types that may contain text and inline nodes. */ export const TEXT_CONTAINERS = [BLOCKS.PARAGRAPH, ...HEADINGS]; /** * Node types before `tables` release. */ export const V1_NODE_TYPES = [ BLOCKS.DOCUMENT, BLOCKS.PARAGRAPH, BLOCKS.HEADING_1, BLOCKS.HEADING_2, BLOCKS.HEADING_3, BLOCKS.HEADING_4, BLOCKS.HEADING_5, BLOCKS.HEADING_6, BLOCKS.OL_LIST, BLOCKS.UL_LIST, BLOCKS.LIST_ITEM, BLOCKS.HR, BLOCKS.QUOTE, BLOCKS.EMBEDDED_ENTRY, BLOCKS.EMBEDDED_ASSET, INLINES.HYPERLINK, INLINES.ENTRY_HYPERLINK, INLINES.ASSET_HYPERLINK, INLINES.EMBEDDED_ENTRY, 'text', ]; /** * Marks before `superscript` & `subscript` release. */ export const V1_MARKS = [MARKS.BOLD, MARKS.CODE, MARKS.ITALIC, MARKS.UNDERLINE]; ================================================ FILE: packages/rich-text-types/src/types.ts ================================================ import { BLOCKS } from './blocks.js'; import { INLINES } from './inlines.js'; import { ListItemBlockEnum, TopLevelBlockEnum } from './schemaConstraints.js'; /** * @additionalProperties true */ export type NodeData = Record ; export interface Node { readonly nodeType: string; data: NodeData; } export interface Block extends Node { nodeType: BLOCKS; content: Array ; } export interface Inline extends Node { nodeType: INLINES; content: Array ; } export interface TopLevelBlock extends Block { nodeType: TopLevelBlockEnum; } export interface Document extends Node { nodeType: BLOCKS.DOCUMENT; content: TopLevelBlock[]; } export interface Text extends Node { nodeType: 'text'; value: string; marks: Mark[]; } export interface Mark { type: string; } export interface ListItemBlock extends Block { nodeType: ListItemBlockEnum; } ================================================ FILE: packages/rich-text-types/src/validator/assert.ts ================================================ import { maxSizeError, typeMismatchError, enumError, unknownPropertyError, requiredPropertyError, minSizeError, } from './errors.js'; import type { Path } from './path.js'; import { ValidationError } from './types.js'; export class ObjectAssertion { private _errors: ValidationError[] = []; constructor( private readonly obj: Record , private readonly path: Path, ) {} catch = (...errors: ValidationError[]): void => { this._errors.push(...errors); }; get errors(): ValidationError[] { const serializeError = (error: ValidationError): string => JSON.stringify({ details: error.details, path: error.path, }); return this._errors.filter( (error, index) => this._errors.findIndex((step) => serializeError(error) === serializeError(step)) === index, ); } /** * Asserts the key exists in the object. You probably shouldn't call this * function directly. Instead, use `$.object`, `$.number`, `$.string`, etc. */ exists = (key: string): boolean => { if (key in this.obj) { return true; } this.catch( requiredPropertyError({ property: key, path: this.path.of(key), }), ); return false; }; /** * Asserts the key exists in the object and its value is a plain object. if * no key is provided, it asserts the object itself. */ public object = (key?: string): boolean => { const value = key ? this.obj[key] : this.obj; if (key) { if (!this.exists(key)) { return false; } } if (typeof value === 'object' && !Array.isArray(value) && value !== null) { return true; } const path = key ? this.path.of(key) : this.path; const property = key ?? this.path.last() ?? 'value'; this.catch( typeMismatchError({ typeName: 'Object', property, path, value, }), ); return false; }; /** * Asserts the key exists in the object and its value is a string. */ public string = (key: string): boolean => { const value = this.obj[key]; if (key && !this.exists(key)) { return false; } if (typeof value === 'string') { return true; } this.catch( typeMismatchError({ typeName: 'String', property: key, path: this.path.of(key), value, }), ); return false; }; /** * Asserts the key exists in the object and its value is a number. */ public number = (key: string, optional?: boolean): boolean => { const value = this.obj[key]; if (optional && !(key in this.obj)) { return true; } if (!this.exists(key)) { return false; } if (typeof value === 'number' && !Number.isNaN(value)) { return true; } this.catch( typeMismatchError({ typeName: 'Number', property: key, path: this.path.of(key), value, }), ); return false; }; /** * Asserts the key exists in the object and its value is an array. You don't * need to manually call this function before `$.each` or `$.maxLength`. */ public array = (key: string): boolean => { const value = this.obj[key]; if (key && !this.exists(key)) { return false; } if (Array.isArray(value)) { return true; } this.catch( typeMismatchError({ typeName: 'Array', property: key, path: this.path.of(key), value, }), ); return false; }; /** * Asserts the value of the key is one of the expected values. */ public enum = (key: string, expected: string[]): boolean => { const value = this.obj[key]; if (typeof value === 'string' && expected.includes(value)) { return true; } this.catch( enumError({ expected, value, path: this.path.of(key), }), ); return false; }; /** * Asserts the array value of the object key is empty. If the value isn't an * array, the function captures a type error and returns false. */ public empty = (key: string): boolean => { if (!this.array(key)) { return false; } const value = this.obj[key] as Array ; if (value.length === 0) { return true; } this.catch( maxSizeError({ max: 0, value, path: this.path.of(key), }), ); return false; }; /** * Asserts the length of the value of the object key is at least `min`. If the * value isn't an array, the function captures a type error and returns false. */ public minLength = (key: string, min: number): boolean => { if (!this.array(key)) { return false; } const value = this.obj[key] as Array ; if (value.length >= min) { return true; } this.catch( minSizeError({ min, value, path: this.path.of(key), }), ); return false; }; /** * Asserts the object has no additional properties other than the ones * specified */ public noAdditionalProperties = (properties: string[]): boolean => { const unknowns = Object.keys(this.obj) .sort() .filter((key) => !properties.includes(key)); unknowns.forEach((property) => this.catch( unknownPropertyError({ property, path: this.path.of(property), }), ), ); return unknowns.length === 0; }; /** * Iterates over the value of the key and assert each item. If the value isn't * an array, the function captures a type error and safely exits. * * To maintain compatibility with previous implementation, we stop early if we * find any errors. */ public each = (key: string, assert: (item: any, path: Path) => ValidationError[]): void => { if (!this.array(key)) { return; } const value = this.obj[key] as Array ; let foundErrors = false; value.forEach((item, index) => { if (foundErrors) { return; } const errors = assert(item, this.path.of(key).of(index)); if (errors.length > 0) { foundErrors = true; } this.catch(...errors); }); }; } ================================================ FILE: packages/rich-text-types/src/validator/errors.ts ================================================ import type { Path } from './path.js'; import { ValidationError } from './types.js'; export const typeMismatchError = ({ path, property, typeName, value, }: { path: Path; property: string | number; typeName: string; value: any; }): ValidationError => { return { details: `The type of "${property}" is incorrect, expected type: ${typeName}`, name: 'type', path: path.toArray(), type: typeName, value, }; }; export const minSizeError = ({ min, value, path, }: { min: number; value: any; path: Path; }): ValidationError => { return { name: 'size', min, path: path.toArray(), details: `Size must be at least ${min}`, value, }; }; export const maxSizeError = ({ max, value, path, }: { max: number; value: any; path: Path; }): ValidationError => { return { name: 'size', max, path: path.toArray(), details: `Size must be at most ${max}`, value, }; }; export const enumError = ({ expected, value, path, }: { expected: string[]; value: any; path: Path; }): ValidationError => { return { details: `Value must be one of expected values`, name: 'in', expected: [...expected].sort(), path: path.toArray(), value, }; }; export const unknownPropertyError = ({ property, path, }: { property: string; path: Path; }): ValidationError => { return { details: `The property "${property}" is not expected`, name: 'unexpected', path: path.toArray(), }; }; export const requiredPropertyError = ({ property, path, }: { property: string; path: Path; }): ValidationError => { return { details: `The property "${property}" is required here`, name: 'required', path: path.toArray(), }; }; ================================================ FILE: packages/rich-text-types/src/validator/index.ts ================================================ import { BLOCKS } from '../blocks.js'; import { INLINES } from '../inlines.js'; import { CONTAINERS, LIST_ITEM_BLOCKS, TOP_LEVEL_BLOCKS } from '../schemaConstraints.js'; import { Document, Text } from '../types.js'; import { ObjectAssertion } from './assert.js'; import { NodeAssertion, Node, HyperLinkAssertion, assert, assertLink, VOID_CONTENT, } from './node.js'; import { Path } from './path.js'; import { assertText } from './text.js'; import type { ValidationError } from './types.js'; export type { ValidationError }; const assertInlineOrText = assert([...Object.values(INLINES), 'text'].sort()); const assertList = assert([BLOCKS.LIST_ITEM]); const assertVoidEntryLink = assertLink('Entry', VOID_CONTENT); const assertTableCell = assert( () => ({ nodeTypes: [BLOCKS.PARAGRAPH], min: 1, }), (data, path) => { const $ = new ObjectAssertion(data, path); $.noAdditionalProperties(['colspan', 'rowspan']); $.number('colspan', true); $.number('rowspan', true); return $.errors; }, ); const nodeValidator: Record > = { [BLOCKS.DOCUMENT]: assert(TOP_LEVEL_BLOCKS), [BLOCKS.PARAGRAPH]: assertInlineOrText, [BLOCKS.HEADING_1]: assertInlineOrText, [BLOCKS.HEADING_2]: assertInlineOrText, [BLOCKS.HEADING_3]: assertInlineOrText, [BLOCKS.HEADING_4]: assertInlineOrText, [BLOCKS.HEADING_5]: assertInlineOrText, [BLOCKS.HEADING_6]: assertInlineOrText, [BLOCKS.QUOTE]: assert(CONTAINERS[BLOCKS.QUOTE]), [BLOCKS.EMBEDDED_ENTRY]: assertVoidEntryLink, [BLOCKS.EMBEDDED_ASSET]: assertLink('Asset', VOID_CONTENT), [BLOCKS.EMBEDDED_RESOURCE]: assertLink('Contentful:Entry', VOID_CONTENT), [BLOCKS.HR]: assert(VOID_CONTENT), [BLOCKS.OL_LIST]: assertList, [BLOCKS.UL_LIST]: assertList, [BLOCKS.LIST_ITEM]: assert([...LIST_ITEM_BLOCKS].sort()), [BLOCKS.TABLE]: assert(() => ({ nodeTypes: [BLOCKS.TABLE_ROW], min: 1, })), [BLOCKS.TABLE_ROW]: assert(() => ({ nodeTypes: [BLOCKS.TABLE_CELL, BLOCKS.TABLE_HEADER_CELL], min: 1, })), [BLOCKS.TABLE_CELL]: assertTableCell, [BLOCKS.TABLE_HEADER_CELL]: assertTableCell, [INLINES.HYPERLINK]: new HyperLinkAssertion(), [INLINES.EMBEDDED_ENTRY]: assertVoidEntryLink, [INLINES.EMBEDDED_RESOURCE]: assertLink('Contentful:Entry', VOID_CONTENT), [INLINES.ENTRY_HYPERLINK]: assertLink('Entry', ['text']), [INLINES.ASSET_HYPERLINK]: assertLink('Asset', ['text']), [INLINES.RESOURCE_HYPERLINK]: assertLink('Contentful:Entry', ['text']), }; function validateNode(node: Node | Text, path: Path): ValidationError[] { if (node.nodeType === 'text') { return assertText(node, path); } const errors = nodeValidator[node.nodeType].assert(node, path); if (errors.length > 0) { return errors; } const $ = new ObjectAssertion(node, path); $.each('content', (item, path) => { // We already know those are valid nodes thanks to the assertion done in // the NodeAssertion class return validateNode(item, path); }); return $.errors; } export const validateRichTextDocument = (document: Document): ValidationError[] => { const path = new Path(); const $ = new ObjectAssertion(document, path); if ($.object()) { $.enum('nodeType', [BLOCKS.DOCUMENT]); } if ($.errors.length > 0) { return $.errors; } return validateNode(document, path); }; ================================================ FILE: packages/rich-text-types/src/validator/node.ts ================================================ import { AssetHyperlink, AssetLinkBlock, EntryHyperlink, EntryLinkBlock, Hyperlink, ResourceLinkBlock, ResourceLinkInline, } from '../nodeTypes.js'; import { Block, Document, Inline } from '../types.js'; import { ObjectAssertion } from './assert.js'; import type { Path } from './path.js'; import { ValidationError } from './types.js'; export type Node = Document | Block | Inline; export type GetContentRule = | string[] | (( node: T, path: Path, ) => { nodeTypes: string[]; min?: number; }); export type ValidateData = (data: T['data'], path: Path) => ValidationError[]; export const VOID_CONTENT: GetContentRule = []; export class NodeAssertion { constructor( private contentRule: GetContentRule , private validateData?: ValidateData , ) {} assert(node: T, path: Path): ValidationError[] { const $ = new ObjectAssertion(node, path); if (!$.object()) { return $.errors; } $.noAdditionalProperties(['nodeType', 'data', 'content']); const { nodeTypes, min = 0 } = Array.isArray(this.contentRule) ? { nodeTypes: this.contentRule, } : this.contentRule(node, path); if (nodeTypes.length === 0 && min > 0) { throw new Error( `Invalid content rule. Cannot have enforce a 'min' of ${min} with no nodeTypes`, ); } $.minLength('content', min); // Is void if (nodeTypes.length === 0) { $.empty('content'); } // Ensure content nodes have valid nodeTypes without validating the full // shape which is something that's only done later if the current node is // valid. else { $.each('content', (item, path) => { const item$ = new ObjectAssertion(item, path); if (!item$.object()) { return item$.errors; } item$.enum('nodeType', nodeTypes); return item$.errors; }); } if ($.object('data')) { const dataErrors = this.validateData?.(node.data, path.of('data')) ?? []; $.catch(...dataErrors); } return $.errors; } } export class EntityLinkAssertion< T extends | EntryLinkBlock | EntryHyperlink | AssetLinkBlock | AssetHyperlink | ResourceLinkBlock | ResourceLinkInline, > extends NodeAssertion { private type: 'ResourceLink' | 'Link'; constructor( private linkType: 'Entry' | 'Asset' | 'Contentful:Entry', contentNodeTypes: GetContentRule , ) { super(contentNodeTypes, (data, path) => this.assertLink(data, path)); this.type = this.linkType.startsWith('Contentful:') ? 'ResourceLink' : 'Link'; } private assertLink = (data: T['data'], path: Path): ValidationError[] => { const $ = new ObjectAssertion(data, path); if ($.object('target')) { const sys$ = new ObjectAssertion(data.target.sys, path.of('target').of('sys')); if (sys$.object()) { sys$.enum('type', [this.type]); sys$.enum('linkType', [this.linkType]); if (this.type === 'Link') { sys$.string('id'); sys$.noAdditionalProperties(['type', 'linkType', 'id']); } else if (this.type === 'ResourceLink') { sys$.string('urn'); sys$.noAdditionalProperties(['type', 'linkType', 'urn']); } } $.catch(...sys$.errors); } $.noAdditionalProperties(['target']); return $.errors; }; } export class HyperLinkAssertion extends NodeAssertion { constructor() { super(['text'], (data, path) => this.assertLink(data, path)); } private assertLink = (data: T['data'], path: Path): ValidationError[] => { const $ = new ObjectAssertion(data, path); $.string('uri'); $.noAdditionalProperties(['uri']); return $.errors; }; } export const assert = ( contentRule: GetContentRule , validateData?: ValidateData , ): NodeAssertion => { return new NodeAssertion(contentRule, validateData); }; export const assertLink = < T extends | EntryLinkBlock | EntryHyperlink | AssetLinkBlock | AssetHyperlink | ResourceLinkBlock | ResourceLinkInline, >( linkType: 'Entry' | 'Asset' | 'Contentful:Entry', contentRule: GetContentRule , ): EntityLinkAssertion => { return new EntityLinkAssertion(linkType, contentRule); }; ================================================ FILE: packages/rich-text-types/src/validator/path.ts ================================================ export class Path { constructor(private readonly path: (string | number)[] = []) {} of = (element: string | number): Path => { return new Path([...this.path, element]); }; isRoot = (): boolean => { return this.path.length === 0; }; last = (): string | number | undefined => { return this.path[this.path.length - 1]; }; toArray = (): (string | number)[] => { return this.path; }; } ================================================ FILE: packages/rich-text-types/src/validator/text.ts ================================================ import { Text } from '../types.js'; import { ObjectAssertion } from './assert.js'; import type { Path } from './path.js'; import { ValidationError } from './types.js'; export function assertText(text: Text, path: Path): ValidationError[] { const $ = new ObjectAssertion(text, path); if (!$.object()) { return $.errors; } $.noAdditionalProperties(['nodeType', 'data', 'value', 'marks']); $.object('data'); $.each('marks', (mark, path) => { const mark$ = new ObjectAssertion(mark, path); if (!mark$.object()) { return mark$.errors; } // For historical reasons, we don't explicitly check for supported marks // e.g. bold, italic ..etc. This makes it possible for a customer to add // custom marks mark$.string('type'); return mark$.errors; }); $.string('value'); return $.errors; } ================================================ FILE: packages/rich-text-types/src/validator/types.ts ================================================ export type ValidationError = { name: string; type?: string; value?: Record | string | number | boolean | null; min?: number | string; max?: number | string; details?: string | null; path?: (string | number)[]; contentTypeId?: string | string[]; nodeType?: string; customMessage?: string; expected?: string[]; }; ================================================ FILE: packages/rich-text-types/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "target": "ES2020", "module": "esnext", "moduleResolution": "node", "declarationDir": "dist/types", "outDir": "dist", "typeRoots": ["../../node_modules/@types", "node_modules/@types", "src/typings"], "types": ["jest", "node"], "inlineSources": true, "declaration": true, "isolatedModules": true, "skipLibCheck": true }, "include": ["src"] } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - 'packages/*' linkWorkspacePackages: true preferWorkspacePackages: true ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "local>contentful/renovate-config" ] } ================================================ FILE: rollup.config.js ================================================ import commonjs from '@rollup/plugin-commonjs'; import resolve from '@rollup/plugin-node-resolve'; import swc from '@rollup/plugin-swc'; import json from 'rollup-plugin-json'; export default (outputFile, overrides = {}) => ({ input: 'src/index.ts', output: [ { file: outputFile, format: 'cjs', sourcemap: true, }, { file: outputFile.replace(/\.es5\.js$/, '.esm.js'), format: 'es', sourcemap: true, }, ], watch: { include: 'src/**', }, plugins: [ json(), swc({ swc: { jsc: { parser: { syntax: 'typescript', tsx: true, }, target: 'es2019', loose: false, minify: { compress: false, mangle: false, }, }, minify: false, }, }), commonjs(), // Allow node_modules resolution, so you can use 'external' to control // which external modules to include in the bundle // https://github.com/rollup/rollup-plugin-node-resolve#usage resolve({ extensions: ['.mjs', '.js', '.json', '.node', '.ts', '.tsx'], }), ], ...overrides, }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es5", "module": "es2015", "lib": [ "es2015", "es2016", "es2017", // otherwise schema generation fails with: // Cannot find name 'Window' "dom" ], "esModuleInterop": true, "strict": true, "strictNullChecks": false, "sourceMap": true, "declaration": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "jsx": "react", "types": ["jest", "node"] } }