├── .editorconfig ├── .eslintignore ├── .github ├── renovate.json └── workflows │ ├── ci.yml │ ├── format-if-needed.yml │ └── release-please.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── components │ ├── block.tsx │ ├── defaults.tsx │ ├── list.tsx │ ├── marks.tsx │ ├── styles.ts │ └── unknown.tsx ├── index.ts └── react-native-portable-text.tsx ├── test ├── .eslintrc ├── __snapshots__ │ └── runthrough.test.tsx.snap ├── fixtures │ ├── 001-empty-block.ts │ ├── 002-single-span.ts │ ├── 003-multiple-spans.ts │ ├── 004-basic-mark-single-span.ts │ ├── 005-basic-mark-multiple-adjacent-spans.ts │ ├── 006-basic-mark-nested-marks.ts │ ├── 007-link-mark-def.ts │ ├── 008-plain-header-block.ts │ ├── 009-messy-link-text.ts │ ├── 010-basic-bullet-list.ts │ ├── 011-basic-numbered-list.ts │ ├── 012-image-support.ts │ ├── 013-materialized-image-support.ts │ ├── 014-nested-lists.ts │ ├── 015-all-basic-marks.ts │ ├── 016-deep-weird-lists.ts │ ├── 017-all-default-block-styles.ts │ ├── 018-marks-all-the-way-down.ts │ ├── 019-keyless.ts │ ├── 020-empty-array.ts │ ├── 021-list-without-level.ts │ ├── 022-inline-nodes.ts │ ├── 023-hard-breaks.ts │ ├── 024-inline-images.ts │ ├── 025-image-with-hotspot.ts │ ├── 026-inline-block-with-text.ts │ ├── 027-styled-list-items.ts │ ├── 028-custom-list-item-type.ts │ ├── 050-custom-block-type.ts │ ├── 051-override-defaults.ts │ ├── 052-custom-marks.ts │ ├── 053-override-default-marks.ts │ ├── 060-list-issue.ts │ ├── 061-missing-mark-component.ts │ └── index.ts └── runthrough.test.tsx ├── tsconfig.json ├── vite.config.ts └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /coverage 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>sanity-io/renovate-config"], 4 | "packageRules": [ 5 | { 6 | "matchDepTypes": ["dependencies"], 7 | "matchPackageNames": ["@portabletext/types", "@portabletext/react"], 8 | "rangeStrategy": "bump", 9 | "groupName": null, 10 | "groupSlug": null, 11 | "semanticCommitType": "fix" 12 | }, 13 | { 14 | "matchDepTypes": ["peerDependencies"], 15 | "matchPackageNames": ["react", "react-native"], 16 | "rangeStrategy": "widen", 17 | "groupName": null, 18 | "groupSlug": null, 19 | "semanticCommitType": "fix" 20 | } 21 | ], 22 | "ignorePresets": ["github>sanity-io/renovate-config:group-non-major"] 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | merge_group: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 14 | cancel-in-progress: true 15 | 16 | permissions: 17 | contents: read # for checkout 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | name: Lint & Build 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: lts/* 28 | - run: corepack enable && pnpm --version 29 | - run: pnpm install 30 | - run: pnpm type-check 31 | - run: pnpm lint 32 | - run: pnpm build 33 | 34 | test: 35 | runs-on: ${{ matrix.platform }} 36 | name: Node.js ${{ matrix.node-version }} / ${{ matrix.platform }} 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | platform: [macos-latest, ubuntu-latest] 41 | node-version: [lts/*] 42 | include: 43 | - platform: ubuntu-latest 44 | node-version: current 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-node@v4 48 | with: 49 | node-version: ${{ matrix.node-version }} 50 | - run: corepack enable && pnpm --version 51 | - run: pnpm install 52 | - run: pnpm test 53 | -------------------------------------------------------------------------------- /.github/workflows/format-if-needed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Auto format 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }} 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | contents: read # for checkout 14 | 15 | jobs: 16 | run: 17 | name: Can the code be formatted? 🤔 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: lts/* 24 | - run: corepack enable && pnpm --version 25 | - run: pnpm install --ignore-scripts 26 | - run: pnpm format 27 | - run: git restore .github/workflows CHANGELOG.md 28 | - uses: actions/create-github-app-token@v1 29 | id: generate-token 30 | with: 31 | app-id: ${{ secrets.ECOSCRIPT_APP_ID }} 32 | private-key: ${{ secrets.ECOSCRIPT_APP_PRIVATE_KEY }} 33 | - uses: peter-evans/create-pull-request@8867c4aba1b742c39f8d0ba35429c2dfa4b6cb20 # v7 34 | with: 35 | body: I ran `pnpm format` 🧑‍💻 36 | branch: actions/format 37 | commit-message: 'chore(format): 🤖 ✨' 38 | delete-branch: true 39 | labels: 🤖 bot 40 | sign-commits: true 41 | title: 'chore(format): 🤖 ✨' 42 | token: ${{ steps.generate-token.outputs.token }} 43 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Please 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | release-please: 14 | permissions: 15 | id-token: write # to enable use of OIDC for npm provenance 16 | # permissions for pushing commits and opening PRs are handled by the `generate-token` step 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/create-github-app-token@v1 20 | id: generate-token 21 | with: 22 | app-id: ${{ secrets.ECOSCRIPT_APP_ID }} 23 | private-key: ${{ secrets.ECOSCRIPT_APP_PRIVATE_KEY }} 24 | # This action will create a release PR when regular conventional commits are pushed to main, it'll also detect if a release PR is merged and npm publish should happen 25 | - uses: google-github-actions/release-please-action@v3 26 | id: release 27 | with: 28 | release-type: node 29 | token: ${{ steps.generate-token.outputs.token }} 30 | 31 | # Publish to NPM on new releases 32 | - uses: actions/checkout@v4 33 | if: ${{ steps.release.outputs.releases_created }} 34 | - uses: pnpm/action-setup@v2 35 | if: ${{ steps.release.outputs.releases_created }} 36 | - uses: actions/setup-node@v4 37 | if: ${{ steps.release.outputs.releases_created }} 38 | with: 39 | cache: pnpm 40 | node-version: lts/* 41 | registry-url: 'https://registry.npmjs.org' 42 | - run: corepack enable && pnpm --version && pnpm install 43 | if: ${{ steps.release.outputs.releases_created }} 44 | - name: Set publishing config 45 | run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" 46 | if: ${{ steps.release.outputs.releases_created }} 47 | env: 48 | NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}} 49 | # Release Please has already incremented versions and published tags, so we just 50 | # need to publish the new version to npm here 51 | - run: pnpm publish 52 | if: ${{ steps.release.outputs.releases_created }} 53 | env: 54 | NPM_CONFIG_PROVENANCE: true 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS finder cache file 40 | .DS_Store 41 | 42 | # VS Code settings 43 | .vscode 44 | 45 | # Lockfiles 46 | yarn.lock 47 | 48 | # Cache 49 | .cache 50 | 51 | # Compiled portable text 52 | /dist 53 | 54 | package-lock.json 55 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /coverage 3 | .editorconfig 4 | .eslintrc 5 | .gitignore 6 | .github 7 | .prettierrc 8 | .travis.yml 9 | .nyc_output 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 📓 Changelog 4 | 5 | All notable changes to this project will be documented in this file. See 6 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 7 | 8 | ## [1.1.13](https://github.com/portabletext/react-native-portabletext/compare/v1.1.12...v1.1.13) (2024-09-10) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update dependency react-native to v0.75.2 ([#150](https://github.com/portabletext/react-native-portabletext/issues/150)) ([6e12024](https://github.com/portabletext/react-native-portabletext/commit/6e12024780db193850e3e83822d0b2d7550aa995)) 14 | 15 | ## [1.1.12](https://github.com/portabletext/react-native-portabletext/compare/v1.1.11...v1.1.12) (2024-05-28) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **deps:** update dependency react-native to v0.74.1 ([#144](https://github.com/portabletext/react-native-portabletext/issues/144)) ([b8cc814](https://github.com/portabletext/react-native-portabletext/commit/b8cc8147fc3c8e31229de159eee7728599e7e344)) 21 | 22 | ## [1.1.11](https://github.com/portabletext/react-native-portabletext/compare/v1.1.10...v1.1.11) (2024-05-28) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **deps:** update dependency @portabletext/react to ^3.1.0 ([#146](https://github.com/portabletext/react-native-portabletext/issues/146)) ([b4fb82b](https://github.com/portabletext/react-native-portabletext/commit/b4fb82bee906cf42e0c62ab89a14da82b51b04af)) 28 | 29 | ## [1.1.10](https://github.com/portabletext/react-native-portabletext/compare/v1.1.9...v1.1.10) (2024-04-11) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **deps:** update dependency @portabletext/react to ^3.0.18 ([#134](https://github.com/portabletext/react-native-portabletext/issues/134)) ([427e40e](https://github.com/portabletext/react-native-portabletext/commit/427e40eeb7c55e7f17e325fb26c6a447600bb2ca)) 35 | * **deps:** update dependency @portabletext/types to ^2.0.13 ([#132](https://github.com/portabletext/react-native-portabletext/issues/132)) ([8ced8c0](https://github.com/portabletext/react-native-portabletext/commit/8ced8c0b46ec81eedb83f2538995c9b06ff6ee49)) 36 | 37 | ## [1.1.9](https://github.com/portabletext/react-native-portabletext/compare/v1.1.8...v1.1.9) (2024-04-05) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * **deps:** update dependency @portabletext/react to ^3.0.17 ([#125](https://github.com/portabletext/react-native-portabletext/issues/125)) ([314007c](https://github.com/portabletext/react-native-portabletext/commit/314007c19acd605af03e33e1b35ebaa55fd9825c)) 43 | 44 | ## [1.1.8](https://github.com/portabletext/react-native-portabletext/compare/v1.1.7...v1.1.8) (2024-04-05) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * **deps:** update dependency @portabletext/react to ^3.0.15 ([#118](https://github.com/portabletext/react-native-portabletext/issues/118)) ([4323aae](https://github.com/portabletext/react-native-portabletext/commit/4323aae34b8f6e6d38192e710c236d86226f8e40)) 50 | * **deps:** update dependency @portabletext/types to ^2.0.12 ([#119](https://github.com/portabletext/react-native-portabletext/issues/119)) ([a698505](https://github.com/portabletext/react-native-portabletext/commit/a6985055e31f28a824ff105bea86b09bf04f285b)) 51 | 52 | ## [1.1.7](https://github.com/portabletext/react-native-portabletext/compare/v1.1.6...v1.1.7) (2024-03-18) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * **deps:** update dependency @portabletext/react to ^3.0.13 ([#111](https://github.com/portabletext/react-native-portabletext/issues/111)) ([39066aa](https://github.com/portabletext/react-native-portabletext/commit/39066aa2429e3c9b3fa0d0399b82a5120de5b191)) 58 | * **deps:** update dependency @portabletext/types to ^2.0.10 ([#107](https://github.com/portabletext/react-native-portabletext/issues/107)) ([b796642](https://github.com/portabletext/react-native-portabletext/commit/b796642147a27f26fca14fedb60b5b1fbfccd4d2)) 59 | 60 | ## [1.1.6](https://github.com/portabletext/react-native-portabletext/compare/v1.1.5...v1.1.6) (2024-03-16) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * add TS exports ([506e375](https://github.com/portabletext/react-native-portabletext/commit/506e3755b9f61f2ef1cdf72ea282ddb1ac5b7780)), closes [#92](https://github.com/portabletext/react-native-portabletext/issues/92) 66 | * **deps:** update dependency @portabletext/react to ^3.0.12 ([#99](https://github.com/portabletext/react-native-portabletext/issues/99)) ([700af50](https://github.com/portabletext/react-native-portabletext/commit/700af509ed5d197bbeadf4dfaf78cabaffc43418)) 67 | * **deps:** update dependency @portabletext/types to ^2.0.9 ([#100](https://github.com/portabletext/react-native-portabletext/issues/100)) ([0c924ed](https://github.com/portabletext/react-native-portabletext/commit/0c924ed2f521a0255ecde564ee878c7257c5da2a)) 68 | * **deps:** update dependency react-native to ^0.66 || ^0.67 || ^0.68 || ^0.69 || ^0.70 || ^0.71 || ^0.72 || ^0.73.0 ([#85](https://github.com/portabletext/react-native-portabletext/issues/85)) ([52d9273](https://github.com/portabletext/react-native-portabletext/commit/52d9273744fd941d8d2f66e675483aae54d60bc7)) 69 | 70 | ## [1.1.5](https://github.com/portabletext/react-native-portabletext/compare/v1.1.4...v1.1.5) (2023-10-10) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * **deps:** update dependency @portabletext/react to ^3.0.11 ([#72](https://github.com/portabletext/react-native-portabletext/issues/72)) ([90bc1a4](https://github.com/portabletext/react-native-portabletext/commit/90bc1a4ca70ef2c25c248b0755a092c5a613b4da)) 76 | * **deps:** update dependency @portabletext/types to ^2.0.8 ([#70](https://github.com/portabletext/react-native-portabletext/issues/70)) ([627de5d](https://github.com/portabletext/react-native-portabletext/commit/627de5d8242165a14f885124a6e2fc5898820111)) 77 | 78 | ## [1.1.4](https://github.com/portabletext/react-native-portabletext/compare/v1.1.3...v1.1.4) (2023-09-29) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * **deps:** update dependency @portabletext/react to ^3.0.9 ([1799079](https://github.com/portabletext/react-native-portabletext/commit/17990795201fc7da7976a6ae2c2ac8c20320f0f5)) 84 | * **deps:** update dependency @portabletext/types to ^2.0.7 ([#54](https://github.com/portabletext/react-native-portabletext/issues/54)) ([764bd6b](https://github.com/portabletext/react-native-portabletext/commit/764bd6b467195e2d66a9ba5f973b02df6477a88a)) 85 | 86 | ## [1.1.3](https://github.com/portabletext/react-native-portabletext/compare/v1.1.2...v1.1.3) (2023-08-24) 87 | 88 | ### Bug Fixes 89 | 90 | - **deps:** update dependency react-native to ^0.66 || ^0.67 || ^0.68 || ^0.69 || ^0.70 || ^0.71 || ^0.72.0 ([#35](https://github.com/portabletext/react-native-portabletext/issues/35)) ([a38f67e](https://github.com/portabletext/react-native-portabletext/commit/a38f67e3e8b2ad8bac78ac32c65b6cc64cf17388)), closes [#28](https://github.com/portabletext/react-native-portabletext/issues/28) 91 | 92 | ## [1.1.2](https://github.com/portabletext/react-native-portabletext/compare/v1.1.1...v1.1.2) (2023-08-24) 93 | 94 | ### Bug Fixes 95 | 96 | - **deps:** mark `@portabletext/react` as external ([ba46ba2](https://github.com/portabletext/react-native-portabletext/commit/ba46ba22432aca11cfd6f07ab078d03f8273978e)) 97 | 98 | ## [1.1.1](https://github.com/portabletext/react-native-portabletext/compare/v1.1.0...v1.1.1) (2023-08-24) 99 | 100 | ### Bug Fixes 101 | 102 | - correct `main` and `module` pkg json paths ([1080f87](https://github.com/portabletext/react-native-portabletext/commit/1080f8785661956c918da4ead4b0cbea3f4246ea)) 103 | 104 | ## [1.1.0](https://github.com/portabletext/react-native-portabletext/compare/v1.0.0...v1.1.0) (2023-08-24) 105 | 106 | ### Features 107 | 108 | - update Portable Text dependencies ([#27](https://github.com/portabletext/react-native-portabletext/issues/27)) ([caa183a](https://github.com/portabletext/react-native-portabletext/commit/caa183aab7044586ce32bc3860dfc53cb2294a11)) 109 | 110 | ### Bug Fixes 111 | 112 | - add provenance ([56b28f2](https://github.com/portabletext/react-native-portabletext/commit/56b28f22009f90aefadbaf9ea8ea7909fc7541c2)) 113 | 114 | ## [1.0.0](https://github.com/portabletext/react-native-portabletext/compare/v0.0.3...v1.0.0) (2023-04-18) 115 | 116 | ### ⚠ BREAKING CHANGES 117 | 118 | - **deps:** update dependency react-native to ^0.71.0 (#8) 119 | 120 | ### Bug Fixes 121 | 122 | - **deps:** downgrade `@portabletext/react` to `v1` ([2e6d7f3](https://github.com/portabletext/react-native-portabletext/commit/2e6d7f39c3a4d3577f4f1781a31dff0594086bfb)) 123 | - **deps:** update dependency @portabletext/react to v2 ([#12](https://github.com/portabletext/react-native-portabletext/issues/12)) ([4afbbf9](https://github.com/portabletext/react-native-portabletext/commit/4afbbf99d41888491a7b6cbb3986f2e1793e54e6)) 124 | - **deps:** update dependency @portabletext/types to v2 ([#13](https://github.com/portabletext/react-native-portabletext/issues/13)) ([6a40153](https://github.com/portabletext/react-native-portabletext/commit/6a40153a4e048f36f4dde32eec96841eff1edc08)) 125 | - **deps:** update dependency react-native to ^0.71.0 ([#8](https://github.com/portabletext/react-native-portabletext/issues/8)) ([6fe0c5d](https://github.com/portabletext/react-native-portabletext/commit/6fe0c5dfdbd3cc991fd36267c3a4e358e2b22ebb)) 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sanity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @portabletext/react-native 2 | 3 | **Note: Experimental!** Please report issues and feedback. 4 | 5 | Render [Portable Text](https://portabletext.org/) with React Native. 6 | 7 | Utilizes [@portabletext/react](https://github.com/portabletext/react-portabletext) under the hood, and thus has the exact same API, but will render React Native views instead of HTML. 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install --save @portabletext/react-native 13 | ``` 14 | 15 | ## Basic usage 16 | 17 | ```js 18 | import {PortableText} from '@portabletext/react-native' 19 | 20 | 24 | ``` 25 | 26 | ## Styling the output 27 | 28 | The rendered views has very little or no styling applied, so you will quite often want to pass [custom components](#customizing-components) to override the built-in ones. 29 | 30 | ## Customizing components 31 | 32 | Customizing components are done in the same way as in [@portabletext/react](https://github.com/portabletext/react-portabletext#customizing-components) - see the [README](https://github.com/portabletext/react-portabletext#customizing-components) for more details. 33 | 34 | ## License 35 | 36 | MIT © [Sanity.io](https://www.sanity.io/) 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@portabletext/react-native", 3 | "version": "1.1.13", 4 | "description": "*Experimental*: Render Portable Text with React Native", 5 | "keywords": [ 6 | "portable-text" 7 | ], 8 | "homepage": "https://github.com/portabletext/react-native-portabletext#readme", 9 | "bugs": { 10 | "url": "https://github.com/portabletext/react-native-portabletext/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/portabletext/react-native-portabletext.git" 15 | }, 16 | "license": "MIT", 17 | "author": "Sanity.io ", 18 | "type": "module", 19 | "main": "./dist/react-native-portable-text.es.js", 20 | "module": "./dist/react-native-portable-text.es.js", 21 | "types": "./dist/react-native-portable-text.d.ts", 22 | "files": [ 23 | "dist" 24 | ], 25 | "scripts": { 26 | "build": "vite build -c ./vite.config.ts", 27 | "format": "prettier --write --cache --ignore-unknown .", 28 | "lint": "eslint .", 29 | "prepublishOnly": "pnpm build", 30 | "test": "vitest", 31 | "type-check": "tsc --noEmit" 32 | }, 33 | "prettier": { 34 | "bracketSpacing": false, 35 | "plugins": [ 36 | "prettier-plugin-packagejson" 37 | ], 38 | "printWidth": 100, 39 | "semi": false, 40 | "singleQuote": true 41 | }, 42 | "eslintConfig": { 43 | "parserOptions": { 44 | "ecmaFeatures": { 45 | "modules": true 46 | }, 47 | "ecmaVersion": 9, 48 | "sourceType": "module" 49 | }, 50 | "extends": [ 51 | "sanity", 52 | "sanity/react", 53 | "sanity/typescript", 54 | "prettier" 55 | ], 56 | "rules": { 57 | "react/prop-types": "off" 58 | }, 59 | "ignorePatterns": [ 60 | "lib/**/" 61 | ] 62 | }, 63 | "dependencies": { 64 | "@portabletext/react": "^3.1.0", 65 | "@portabletext/types": "^2.0.13" 66 | }, 67 | "devDependencies": { 68 | "@react-native/polyfills": "^2.0.0", 69 | "@types/react": "^18.3.3", 70 | "@types/react-native": "^0.73.0", 71 | "@types/react-test-renderer": "^18.3.0", 72 | "@typescript-eslint/eslint-plugin": "^7.11.0", 73 | "@typescript-eslint/parser": "^7.11.0", 74 | "@vitejs/plugin-react": "^4.3.0", 75 | "eslint": "^8.57.0", 76 | "eslint-config-prettier": "^9.1.0", 77 | "eslint-config-sanity": "^7.1.2", 78 | "eslint-plugin-react": "^7.34.2", 79 | "eslint-plugin-react-hooks": "^4.6.2", 80 | "prettier": "^3.2.5", 81 | "prettier-plugin-packagejson": "^2.5.0", 82 | "react": "^18.3.1", 83 | "react-native": "^0.75.2", 84 | "react-test-renderer": "^18.3.1", 85 | "typescript": "^5.4.5", 86 | "vite": "^5.2.11", 87 | "vite-plugin-dts": "^3.9.1", 88 | "vitest": "1.6.0", 89 | "vitest-react-native": "0.1.5" 90 | }, 91 | "peerDependencies": { 92 | "react": "^17 || ^18", 93 | "react-native": "^0.66 || ^0.67 || ^0.68 || ^0.69 || ^0.70 || ^0.71 || ^0.72 || ^0.73.0 || ^0.74.0 || ^0.75.0" 94 | }, 95 | "packageManager": "pnpm@9.1.3", 96 | "publishConfig": { 97 | "access": "public" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/components/block.tsx: -------------------------------------------------------------------------------- 1 | import type {PortableTextComponent} from '@portabletext/react' 2 | import type {PortableTextBlock, PortableTextBlockStyle} from '@portabletext/types' 3 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 4 | import React from 'react' 5 | import {Text, View} from 'react-native' 6 | 7 | import {blockStyles, textStyles} from './styles' 8 | 9 | type BlockStyleName = keyof typeof blockStyles 10 | 11 | export const DefaultBlock: PortableTextComponent = ({children, value}) => { 12 | const style = (value.style || 'normal') as BlockStyleName 13 | 14 | // Wrap in a text element to make children display inline 15 | return ( 16 | 17 | {children} 18 | 19 | ) 20 | } 21 | 22 | export const defaultBlockStyles: Record< 23 | PortableTextBlockStyle, 24 | PortableTextComponent | undefined 25 | > = { 26 | normal: DefaultBlock, 27 | blockquote: DefaultBlock, 28 | h1: DefaultBlock, 29 | h2: DefaultBlock, 30 | h3: DefaultBlock, 31 | h4: DefaultBlock, 32 | h5: DefaultBlock, 33 | h6: DefaultBlock, 34 | } 35 | -------------------------------------------------------------------------------- /src/components/defaults.tsx: -------------------------------------------------------------------------------- 1 | import type {PortableTextReactComponents} from '@portabletext/react' 2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 3 | import React from 'react' 4 | import {Text} from 'react-native' 5 | 6 | import {defaultBlockStyles} from './block' 7 | import {DefaultList, defaultListItems} from './list' 8 | import {defaultMarks} from './marks' 9 | import { 10 | DefaultUnknownBlockStyle, 11 | DefaultUnknownList, 12 | DefaultUnknownListItem, 13 | DefaultUnknownMark, 14 | DefaultUnknownType, 15 | } from './unknown' 16 | 17 | export const DefaultHardBreak = (): React.JSX.Element => {'\n'} 18 | 19 | export const defaultComponents: PortableTextReactComponents = { 20 | types: {}, 21 | 22 | block: defaultBlockStyles, 23 | marks: defaultMarks, 24 | list: DefaultList, 25 | listItem: defaultListItems, 26 | hardBreak: DefaultHardBreak, 27 | 28 | unknownType: DefaultUnknownType, 29 | unknownMark: DefaultUnknownMark, 30 | unknownList: DefaultUnknownList, 31 | unknownListItem: DefaultUnknownListItem, 32 | unknownBlockStyle: DefaultUnknownBlockStyle, 33 | } 34 | -------------------------------------------------------------------------------- /src/components/list.tsx: -------------------------------------------------------------------------------- 1 | import type {PortableTextListComponent, PortableTextListItemComponent} from '@portabletext/react' 2 | import type {PortableTextListItemType} from '@portabletext/types' 3 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 4 | import React from 'react' 5 | import {Text, View} from 'react-native' 6 | 7 | import {listStyles} from './styles' 8 | 9 | export const DefaultList: PortableTextListComponent = ({value, children}) => { 10 | const base = value.level > 1 ? listStyles.listDeep : listStyles.list 11 | const padding = {paddingLeft: 16 * value.level} 12 | return {children} 13 | } 14 | 15 | export const defaultListItems: Record< 16 | PortableTextListItemType, 17 | PortableTextListItemComponent | undefined 18 | > = { 19 | bullet: ({children}) => ( 20 | 21 | {'\u00B7'} 22 | {children} 23 | 24 | ), 25 | number: ({children, index}) => ( 26 | 27 | {index + 1}. 28 | {children} 29 | 30 | ), 31 | } 32 | -------------------------------------------------------------------------------- /src/components/marks.tsx: -------------------------------------------------------------------------------- 1 | import type {PortableTextMarkComponent} from '@portabletext/react' 2 | import type {TypedObject} from '@portabletext/types' 3 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 4 | import React, {useCallback} from 'react' 5 | import {Linking, Text} from 'react-native' 6 | 7 | import {markStyles} from './styles' 8 | 9 | interface DefaultLink extends TypedObject { 10 | _type: 'link' 11 | href: string 12 | } 13 | 14 | const Link: PortableTextMarkComponent = ({children, value}) => { 15 | const href = value?.href 16 | const onPress = useCallback(() => (href ? Linking.openURL(href) : undefined), [href]) 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | } 24 | 25 | export const defaultMarks: Record = { 26 | em: ({children}) => {children}, 27 | strong: ({children}) => {children}, 28 | code: ({children}) => {children}, 29 | underline: ({children}) => {children}, 30 | 'strike-through': ({children}) => {children}, 31 | link: Link, 32 | } 33 | -------------------------------------------------------------------------------- /src/components/styles.ts: -------------------------------------------------------------------------------- 1 | import {StyleSheet} from 'react-native' 2 | 3 | export const blockStyles = StyleSheet.create({ 4 | normal: {marginBottom: 16}, 5 | 6 | blockquote: { 7 | paddingHorizontal: 14, 8 | borderLeftWidth: 3.5, 9 | borderLeftColor: '#dfe2e5', 10 | marginBottom: 16, 11 | }, 12 | 13 | h1: {marginVertical: 22}, 14 | h2: {marginVertical: 20}, 15 | h3: {marginVertical: 18}, 16 | h4: {marginVertical: 18}, 17 | h5: {marginVertical: 18}, 18 | h6: {marginVertical: 18}, 19 | }) 20 | 21 | export const listStyles = StyleSheet.create({ 22 | list: { 23 | marginVertical: 16, 24 | }, 25 | 26 | listDeep: { 27 | marginVertical: 0, 28 | }, 29 | 30 | listItem: { 31 | flex: 1, 32 | flexWrap: 'wrap', 33 | flexDirection: 'row', 34 | }, 35 | 36 | bulletListIcon: { 37 | marginLeft: 10, 38 | marginRight: 10, 39 | fontWeight: 'bold', 40 | }, 41 | 42 | listItemWrapper: { 43 | flexDirection: 'row', 44 | justifyContent: 'flex-start', 45 | }, 46 | }) 47 | 48 | export const textStyles = StyleSheet.create({ 49 | h1: { 50 | fontWeight: 'bold', 51 | fontSize: 32, 52 | }, 53 | 54 | h2: { 55 | fontWeight: 'bold', 56 | fontSize: 24, 57 | }, 58 | 59 | h3: { 60 | fontWeight: 'bold', 61 | fontSize: 18, 62 | }, 63 | 64 | h4: { 65 | fontWeight: 'bold', 66 | fontSize: 16, 67 | }, 68 | 69 | h5: { 70 | fontWeight: 'bold', 71 | fontSize: 14, 72 | }, 73 | 74 | h6: { 75 | fontWeight: 'bold', 76 | fontSize: 10, 77 | }, 78 | 79 | normal: {}, 80 | blockquote: {}, 81 | }) 82 | 83 | export const markStyles = StyleSheet.create({ 84 | strong: { 85 | fontWeight: 'bold', 86 | }, 87 | 88 | em: { 89 | fontStyle: 'italic', 90 | }, 91 | 92 | link: { 93 | textDecorationLine: 'underline', 94 | }, 95 | 96 | underline: { 97 | textDecorationLine: 'underline', 98 | }, 99 | 100 | strikeThrough: { 101 | textDecorationLine: 'line-through', 102 | }, 103 | 104 | code: { 105 | paddingVertical: 3, 106 | paddingHorizontal: 5, 107 | backgroundColor: 'rgba(27, 31, 35, 0.05)', 108 | color: '#24292e', 109 | }, 110 | }) 111 | 112 | export const utilityStyles = StyleSheet.create({ 113 | hidden: { 114 | display: 'none', 115 | }, 116 | }) 117 | -------------------------------------------------------------------------------- /src/components/unknown.tsx: -------------------------------------------------------------------------------- 1 | import type {PortableTextReactComponents} from '@portabletext/react' 2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 3 | import React from 'react' 4 | import {Text, View} from 'react-native' 5 | 6 | import {DefaultBlock} from './block' 7 | import {defaultListItems} from './list' 8 | import {utilityStyles} from './styles' 9 | 10 | const DefaultListItem = defaultListItems.bullet || View 11 | 12 | export const DefaultUnknownType: PortableTextReactComponents['unknownType'] = ({value}) => { 13 | const warning = `Unknown block type "${value._type}", specify a component for it in the \`components.types\` prop` 14 | 15 | // eslint-disable-next-line no-console 16 | console.warn(warning) 17 | 18 | return {warning} 19 | } 20 | 21 | export const DefaultUnknownMark: PortableTextReactComponents['unknownMark'] = ({ 22 | markType, 23 | children, 24 | }) => { 25 | // eslint-disable-next-line no-console 26 | console.warn( 27 | `Unknown mark type "${markType}", please specify a component for it in the \`components.marks\` prop`, 28 | ) 29 | 30 | return {children} 31 | } 32 | 33 | export const DefaultUnknownBlockStyle: PortableTextReactComponents['unknownBlockStyle'] = ({ 34 | value, 35 | ...props 36 | }) => { 37 | const style = value.style || 'normal' 38 | // eslint-disable-next-line no-console 39 | console.warn( 40 | `Unknown block style "${style}", please specify a component for it in the \`components.block\` prop`, 41 | ) 42 | 43 | return 44 | } 45 | 46 | // This shouldn't ever happen, since we're overriding the main `List` component, 47 | // but leaving it here for posterity (and because the types _require_ one right now) 48 | export const DefaultUnknownList: PortableTextReactComponents['unknownList'] = ({ 49 | children, 50 | value, 51 | }) => { 52 | // eslint-disable-next-line no-console 53 | console.warn( 54 | `Unknown list style "${ 55 | value.listItem || 'bullet' 56 | }", please specify a component for it in the \`components.list\` prop`, 57 | ) 58 | 59 | return {children} 60 | } 61 | 62 | export const DefaultUnknownListItem: PortableTextReactComponents['unknownListItem'] = ({ 63 | value, 64 | ...props 65 | }) => { 66 | const style = value.listItem || 'bullet' 67 | // eslint-disable-next-line no-console 68 | console.warn( 69 | `Unknown list item style "${style}", please specify a component for it in the \`components.list\` prop`, 70 | ) 71 | 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line simple-import-sort/exports 2 | export * from '@portabletext/react' 3 | export {defaultComponents} from './components/defaults' 4 | export {PortableText} from './react-native-portable-text' 5 | -------------------------------------------------------------------------------- /src/react-native-portable-text.tsx: -------------------------------------------------------------------------------- 1 | import type {PortableTextProps} from '@portabletext/react' 2 | import {mergeComponents, PortableText as BasePortableText} from '@portabletext/react' 3 | import type {PortableTextBlock, TypedObject} from '@portabletext/types' 4 | import React from 'react' 5 | 6 | import {defaultComponents} from './components/defaults' 7 | 8 | export type * from '@portabletext/react' 9 | 10 | export function PortableText( 11 | props: Omit, 'listNestingMode'>, 12 | ): React.JSX.Element { 13 | return ( 14 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": {"jest": true} 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/001-empty-block.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _key: 'R5FvMrjo', 5 | _type: 'block', 6 | children: [], 7 | markDefs: [], 8 | style: 'normal', 9 | } 10 | 11 | export default { 12 | input, 13 | output: '

', 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/002-single-span.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _key: 'R5FvMrjo', 5 | _type: 'block', 6 | children: [ 7 | { 8 | _key: 'cZUQGmh4', 9 | _type: 'span', 10 | marks: [], 11 | text: 'Plain text.', 12 | }, 13 | ], 14 | markDefs: [], 15 | style: 'normal', 16 | } 17 | 18 | export default { 19 | input, 20 | output: '

Plain text.

', 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/003-multiple-spans.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _key: 'R5FvMrjo', 5 | _type: 'block', 6 | children: [ 7 | { 8 | _key: 'cZUQGmh4', 9 | _type: 'span', 10 | marks: [], 11 | text: 'Span number one. ', 12 | }, 13 | { 14 | _key: 'toaiCqIK', 15 | _type: 'span', 16 | marks: [], 17 | text: 'And span number two.', 18 | }, 19 | ], 20 | markDefs: [], 21 | style: 'normal', 22 | } 23 | 24 | export default { 25 | input, 26 | output: '

Span number one. And span number two.

', 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/004-basic-mark-single-span.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _key: 'R5FvMrjo', 5 | _type: 'block', 6 | children: [ 7 | { 8 | _key: 'cZUQGmh4', 9 | _type: 'span', 10 | marks: ['code'], 11 | text: 'sanity', 12 | }, 13 | { 14 | _key: 'toaiCqIK', 15 | _type: 'span', 16 | marks: [], 17 | text: ' is the name of the CLI tool.', 18 | }, 19 | ], 20 | markDefs: [], 21 | style: 'normal', 22 | } 23 | 24 | export default { 25 | input, 26 | output: '

sanity is the name of the CLI tool.

', 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/005-basic-mark-multiple-adjacent-spans.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _key: 'R5FvMrjo', 5 | _type: 'block', 6 | children: [ 7 | { 8 | _key: 'cZUQGmh4', 9 | _type: 'span', 10 | marks: ['strong'], 11 | text: 'A word of', 12 | }, 13 | { 14 | _key: 'toaiCqIK', 15 | _type: 'span', 16 | marks: ['strong'], 17 | text: ' warning;', 18 | }, 19 | { 20 | _key: 'gaZingA', 21 | _type: 'span', 22 | marks: [], 23 | text: ' Sanity is addictive.', 24 | }, 25 | ], 26 | markDefs: [], 27 | style: 'normal', 28 | } 29 | 30 | export default { 31 | input, 32 | output: '

A word of warning; Sanity is addictive.

', 33 | } 34 | -------------------------------------------------------------------------------- /test/fixtures/006-basic-mark-nested-marks.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _key: 'R5FvMrjo', 5 | _type: 'block', 6 | children: [ 7 | { 8 | _key: 'cZUQGmh4', 9 | _type: 'span', 10 | marks: ['strong'], 11 | text: 'A word of ', 12 | }, 13 | { 14 | _key: 'toaiCqIK', 15 | _type: 'span', 16 | marks: ['strong', 'em'], 17 | text: 'warning;', 18 | }, 19 | { 20 | _key: 'gaZingA', 21 | _type: 'span', 22 | marks: [], 23 | text: ' Sanity is addictive.', 24 | }, 25 | ], 26 | markDefs: [], 27 | style: 'normal', 28 | } 29 | 30 | export default { 31 | input, 32 | output: '

A word of warning; Sanity is addictive.

', 33 | } 34 | -------------------------------------------------------------------------------- /test/fixtures/007-link-mark-def.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _key: 'R5FvMrjo', 5 | _type: 'block', 6 | children: [ 7 | { 8 | _key: 'cZUQGmh4', 9 | _type: 'span', 10 | marks: [], 11 | text: 'A word of warning; ', 12 | }, 13 | { 14 | _key: 'toaiCqIK', 15 | _type: 'span', 16 | marks: ['someLinkId'], 17 | text: 'Sanity', 18 | }, 19 | { 20 | _key: 'gaZingA', 21 | _type: 'span', 22 | marks: [], 23 | text: ' is addictive.', 24 | }, 25 | ], 26 | markDefs: [ 27 | { 28 | _type: 'link', 29 | _key: 'someLinkId', 30 | href: 'https://sanity.io/', 31 | }, 32 | ], 33 | style: 'normal', 34 | } 35 | 36 | export default { 37 | input, 38 | output: '

A word of warning; Sanity is addictive.

', 39 | } 40 | -------------------------------------------------------------------------------- /test/fixtures/008-plain-header-block.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _key: 'R5FvMrjo', 5 | _type: 'block', 6 | children: [ 7 | { 8 | _key: 'cZUQGmh4', 9 | _type: 'span', 10 | marks: [], 11 | text: 'Dat heading', 12 | }, 13 | ], 14 | markDefs: [], 15 | style: 'h2', 16 | } 17 | 18 | export default { 19 | input, 20 | output: '

Dat heading

', 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/009-messy-link-text.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _type: 'block', 5 | children: [ 6 | { 7 | _key: 'a1ph4', 8 | _type: 'span', 9 | marks: ['zomgLink'], 10 | text: 'Sanity', 11 | }, 12 | { 13 | _key: 'b374', 14 | _type: 'span', 15 | marks: [], 16 | text: ' can be used to power almost any ', 17 | }, 18 | { 19 | _key: 'ch4r1i3', 20 | _type: 'span', 21 | marks: ['zomgLink', 'strong', 'em'], 22 | text: 'app', 23 | }, 24 | { 25 | _key: 'd3174', 26 | _type: 'span', 27 | marks: ['em', 'zomgLink'], 28 | text: ' or website', 29 | }, 30 | { 31 | _key: 'ech0', 32 | _type: 'span', 33 | marks: [], 34 | text: '.', 35 | }, 36 | ], 37 | markDefs: [ 38 | { 39 | _key: 'zomgLink', 40 | _type: 'link', 41 | href: 'https://sanity.io/', 42 | }, 43 | ], 44 | style: 'blockquote', 45 | } 46 | 47 | export default { 48 | input, 49 | output: 50 | '
Sanity can be used to power almost any app or website.
', 51 | } 52 | -------------------------------------------------------------------------------- /test/fixtures/010-basic-bullet-list.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | style: 'normal', 6 | _type: 'block', 7 | _key: 'f94596b05b41', 8 | markDefs: [], 9 | children: [ 10 | { 11 | _type: 'span', 12 | text: "Let's test some of these lists!", 13 | marks: [], 14 | }, 15 | ], 16 | }, 17 | { 18 | listItem: 'bullet', 19 | style: 'normal', 20 | level: 1, 21 | _type: 'block', 22 | _key: '937effb1cd06', 23 | markDefs: [], 24 | children: [ 25 | { 26 | _type: 'span', 27 | text: 'Bullet 1', 28 | marks: [], 29 | }, 30 | ], 31 | }, 32 | { 33 | listItem: 'bullet', 34 | style: 'normal', 35 | level: 1, 36 | _type: 'block', 37 | _key: 'bd2d22278b88', 38 | markDefs: [], 39 | children: [ 40 | { 41 | _type: 'span', 42 | text: 'Bullet 2', 43 | marks: [], 44 | }, 45 | ], 46 | }, 47 | { 48 | listItem: 'bullet', 49 | style: 'normal', 50 | level: 1, 51 | _type: 'block', 52 | _key: 'a97d32e9f747', 53 | markDefs: [], 54 | children: [ 55 | { 56 | _type: 'span', 57 | text: 'Bullet 3', 58 | marks: [], 59 | }, 60 | ], 61 | }, 62 | ] 63 | 64 | export default { 65 | input, 66 | output: [ 67 | '

Let's test some of these lists!

', 68 | '
    ', 69 | '
  • Bullet 1
  • ', 70 | '
  • Bullet 2
  • ', 71 | '
  • Bullet 3
  • ', 72 | '
', 73 | ].join(''), 74 | } 75 | -------------------------------------------------------------------------------- /test/fixtures/011-basic-numbered-list.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | style: 'normal', 6 | _type: 'block', 7 | _key: 'f94596b05b41', 8 | markDefs: [], 9 | children: [ 10 | { 11 | _type: 'span', 12 | text: "Let's test some of these lists!", 13 | marks: [], 14 | }, 15 | ], 16 | }, 17 | { 18 | listItem: 'number', 19 | style: 'normal', 20 | level: 1, 21 | _type: 'block', 22 | _key: '937effb1cd06', 23 | markDefs: [], 24 | children: [ 25 | { 26 | _type: 'span', 27 | text: 'Number 1', 28 | marks: [], 29 | }, 30 | ], 31 | }, 32 | { 33 | listItem: 'number', 34 | style: 'normal', 35 | level: 1, 36 | _type: 'block', 37 | _key: 'bd2d22278b88', 38 | markDefs: [], 39 | children: [ 40 | { 41 | _type: 'span', 42 | text: 'Number 2', 43 | marks: [], 44 | }, 45 | ], 46 | }, 47 | { 48 | listItem: 'number', 49 | style: 'normal', 50 | level: 1, 51 | _type: 'block', 52 | _key: 'a97d32e9f747', 53 | markDefs: [], 54 | children: [ 55 | { 56 | _type: 'span', 57 | text: 'Number 3', 58 | marks: [], 59 | }, 60 | ], 61 | }, 62 | ] 63 | 64 | export default { 65 | input, 66 | output: [ 67 | '

Let's test some of these lists!

', 68 | '
    ', 69 | '
  1. Number 1
  2. ', 70 | '
  3. Number 2
  4. ', 71 | '
  5. Number 3
  6. ', 72 | '
', 73 | ].join(''), 74 | } 75 | -------------------------------------------------------------------------------- /test/fixtures/012-image-support.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock, TypedObject} from '@portabletext/types' 2 | 3 | interface Image extends TypedObject { 4 | _type: 'image' 5 | asset: { 6 | _type: string 7 | _ref: string 8 | } 9 | } 10 | 11 | const input: (PortableTextBlock | Image)[] = [ 12 | { 13 | style: 'normal', 14 | _type: 'block', 15 | _key: 'bd73ec5f61a1', 16 | markDefs: [], 17 | children: [ 18 | { 19 | _type: 'span', 20 | text: 'Also, images are pretty common.', 21 | marks: [], 22 | }, 23 | ], 24 | }, 25 | { 26 | _type: 'image', 27 | _key: 'd234a4fa317a', 28 | asset: { 29 | _type: 'reference', 30 | _ref: 'image-YiOKD0O6AdjKPaK24WtbOEv0-3456x2304-jpg', 31 | }, 32 | }, 33 | ] 34 | 35 | export default { 36 | input, 37 | output: [ 38 | '
', 39 | '

Also, images are pretty common.

', 40 | '
', 41 | '
', 42 | ].join(''), 43 | } 44 | -------------------------------------------------------------------------------- /test/fixtures/013-materialized-image-support.ts: -------------------------------------------------------------------------------- 1 | import type {ArbitraryTypedObject, PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: (PortableTextBlock | ArbitraryTypedObject)[] = [ 4 | { 5 | style: 'normal', 6 | _type: 'block', 7 | _key: 'bd73ec5f61a1', 8 | markDefs: [], 9 | children: [ 10 | { 11 | _type: 'span', 12 | text: 'Also, images are pretty common.', 13 | marks: [], 14 | }, 15 | ], 16 | }, 17 | { 18 | _key: 'bd45080bf448', 19 | _type: 'image', 20 | asset: { 21 | _createdAt: '2017-08-02T23:04:57Z', 22 | _id: 'image-caC3MscJLd3mNAbMdQ6-5748x3832-jpg', 23 | _rev: 'ch7HXy1Ux9jmVKZ6TKPoZ8', 24 | _type: 'sanity.imageAsset', 25 | _updatedAt: '2017-09-19T18:05:06Z', 26 | assetId: 'caC3MscJLd3mNAbMdQ6', 27 | extension: 'jpg', 28 | metadata: { 29 | dimensions: { 30 | aspectRatio: 1.5, 31 | height: 3832, 32 | width: 5748, 33 | }, 34 | }, 35 | mimeType: 'image/jpeg', 36 | path: 'images/3do82whm/production/caC3MscJLd3mNAbMdQ6-5748x3832.jpg', 37 | url: 'https://cdn.sanity.io/images/3do82whm/production/caC3MscJLd3mNAbMdQ6-5748x3832.jpg', 38 | }, 39 | }, 40 | ] 41 | 42 | export default { 43 | input, 44 | output: [ 45 | '
', 46 | '

Also, images are pretty common.

', 47 | '
', 48 | '
', 49 | ].join(''), 50 | } 51 | -------------------------------------------------------------------------------- /test/fixtures/014-nested-lists.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | _type: 'block', 6 | _key: 'a', 7 | markDefs: [], 8 | style: 'normal', 9 | children: [{_type: 'span', marks: [], text: 'Span'}], 10 | }, 11 | { 12 | _type: 'block', 13 | _key: 'b', 14 | markDefs: [], 15 | level: 1, 16 | children: [{_type: 'span', marks: [], text: 'Item 1, level 1'}], 17 | listItem: 'bullet', 18 | }, 19 | { 20 | _type: 'block', 21 | _key: 'c', 22 | markDefs: [], 23 | level: 1, 24 | children: [{_type: 'span', marks: [], text: 'Item 2, level 1'}], 25 | listItem: 'bullet', 26 | }, 27 | { 28 | _type: 'block', 29 | _key: 'd', 30 | markDefs: [], 31 | level: 2, 32 | children: [{_type: 'span', marks: [], text: 'Item 3, level 2'}], 33 | listItem: 'number', 34 | }, 35 | { 36 | _type: 'block', 37 | _key: 'e', 38 | markDefs: [], 39 | level: 3, 40 | children: [{_type: 'span', marks: [], text: 'Item 4, level 3'}], 41 | listItem: 'number', 42 | }, 43 | { 44 | _type: 'block', 45 | _key: 'f', 46 | markDefs: [], 47 | level: 2, 48 | children: [{_type: 'span', marks: [], text: 'Item 5, level 2'}], 49 | listItem: 'number', 50 | }, 51 | { 52 | _type: 'block', 53 | _key: 'g', 54 | markDefs: [], 55 | level: 2, 56 | children: [{_type: 'span', marks: [], text: 'Item 6, level 2'}], 57 | listItem: 'number', 58 | }, 59 | { 60 | _type: 'block', 61 | _key: 'h', 62 | markDefs: [], 63 | level: 1, 64 | children: [{_type: 'span', marks: [], text: 'Item 7, level 1'}], 65 | listItem: 'bullet', 66 | }, 67 | { 68 | _type: 'block', 69 | _key: 'i', 70 | markDefs: [], 71 | level: 1, 72 | children: [{_type: 'span', marks: [], text: 'Item 8, level 1'}], 73 | listItem: 'bullet', 74 | }, 75 | { 76 | _type: 'block', 77 | _key: 'j', 78 | markDefs: [], 79 | level: 1, 80 | children: [{_type: 'span', marks: [], text: 'Item 1 of list 2'}], 81 | listItem: 'number', 82 | }, 83 | { 84 | _type: 'block', 85 | _key: 'k', 86 | markDefs: [], 87 | level: 1, 88 | children: [{_type: 'span', marks: [], text: 'Item 2 of list 2'}], 89 | listItem: 'number', 90 | }, 91 | { 92 | _type: 'block', 93 | _key: 'l', 94 | markDefs: [], 95 | level: 2, 96 | children: [{_type: 'span', marks: [], text: 'Item 3 of list 2, level 2'}], 97 | listItem: 'number', 98 | }, 99 | { 100 | _type: 'block', 101 | _key: 'm', 102 | markDefs: [], 103 | style: 'normal', 104 | children: [{_type: 'span', marks: [], text: 'Just a block'}], 105 | }, 106 | ] 107 | 108 | export default { 109 | input, 110 | output: [ 111 | '

Span

', 112 | '
    ', 113 | '
  • Item 1, level 1
  • ', 114 | '
  • ', 115 | ' Item 2, level 1', 116 | '
      ', 117 | '
    1. ', 118 | ' Item 3, level 2', 119 | '
        ', 120 | '
      1. Item 4, level 3
      2. ', 121 | '
      ', 122 | '
    2. ', 123 | '
    3. Item 5, level 2
    4. ', 124 | '
    5. Item 6, level 2
    6. ', 125 | '
    ', 126 | '
  • ', 127 | '
  • Item 7, level 1
  • ', 128 | '
  • Item 8, level 1
  • ', 129 | '
', 130 | '
    ', 131 | '
  1. Item 1 of list 2
  2. ', 132 | '
  3. ', 133 | ' Item 2 of list 2', 134 | '
      ', 135 | '
    1. Item 3 of list 2, level 2
    2. ', 136 | '
    ', 137 | '
  4. ', 138 | '
', 139 | '

Just a block

', 140 | ] 141 | .map((line) => line.trim()) 142 | .join(''), 143 | } 144 | -------------------------------------------------------------------------------- /test/fixtures/015-all-basic-marks.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _key: 'R5FvMrjo', 5 | _type: 'block', 6 | children: [ 7 | { 8 | _key: 'a', 9 | _type: 'span', 10 | marks: ['code'], 11 | text: 'code', 12 | }, 13 | { 14 | _key: 'b', 15 | _type: 'span', 16 | marks: ['strong'], 17 | text: 'strong', 18 | }, 19 | { 20 | _key: 'c', 21 | _type: 'span', 22 | marks: ['em'], 23 | text: 'em', 24 | }, 25 | { 26 | _key: 'd', 27 | _type: 'span', 28 | marks: ['underline'], 29 | text: 'underline', 30 | }, 31 | { 32 | _key: 'e', 33 | _type: 'span', 34 | marks: ['strike-through'], 35 | text: 'strike-through', 36 | }, 37 | { 38 | _key: 'f', 39 | _type: 'span', 40 | marks: ['dat-link'], 41 | text: 'link', 42 | }, 43 | ], 44 | markDefs: [ 45 | { 46 | _key: 'dat-link', 47 | _type: 'link', 48 | href: 'https://www.sanity.io/', 49 | }, 50 | ], 51 | style: 'normal', 52 | } 53 | 54 | export default { 55 | input, 56 | output: [ 57 | '

', 58 | 'code', 59 | 'strong', 60 | 'em', 61 | 'underline', 62 | 'strike-through', 63 | 'link', 64 | '

', 65 | ].join(''), 66 | } 67 | -------------------------------------------------------------------------------- /test/fixtures/016-deep-weird-lists.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | listItem: 'bullet', 6 | style: 'normal', 7 | level: 1, 8 | _type: 'block', 9 | _key: 'fde2e840a29c', 10 | markDefs: [], 11 | children: [ 12 | { 13 | _type: 'span', 14 | text: 'Item a', 15 | marks: [], 16 | }, 17 | ], 18 | }, 19 | { 20 | listItem: 'bullet', 21 | style: 'normal', 22 | level: 1, 23 | _type: 'block', 24 | _key: 'c16f11c71638', 25 | markDefs: [], 26 | children: [ 27 | { 28 | _type: 'span', 29 | text: 'Item b', 30 | marks: [], 31 | }, 32 | ], 33 | }, 34 | { 35 | listItem: 'number', 36 | style: 'normal', 37 | level: 1, 38 | _type: 'block', 39 | _key: 'e92f55b185ae', 40 | markDefs: [], 41 | children: [ 42 | { 43 | _type: 'span', 44 | text: 'Item 1', 45 | marks: [], 46 | }, 47 | ], 48 | }, 49 | { 50 | listItem: 'number', 51 | style: 'normal', 52 | level: 1, 53 | _type: 'block', 54 | _key: 'a77e71209aff', 55 | markDefs: [], 56 | children: [ 57 | { 58 | _type: 'span', 59 | text: 'Item 2', 60 | marks: [], 61 | }, 62 | ], 63 | }, 64 | { 65 | listItem: 'number', 66 | style: 'normal', 67 | level: 2, 68 | _type: 'block', 69 | _key: 'da1f863df265', 70 | markDefs: [], 71 | children: [ 72 | { 73 | _type: 'span', 74 | text: 'Item 2, a', 75 | marks: [], 76 | }, 77 | ], 78 | }, 79 | { 80 | listItem: 'number', 81 | style: 'normal', 82 | level: 2, 83 | _type: 'block', 84 | _key: '60d8c92bed0d', 85 | markDefs: [], 86 | children: [ 87 | { 88 | _type: 'span', 89 | text: 'Item 2, b', 90 | marks: [], 91 | }, 92 | ], 93 | }, 94 | { 95 | listItem: 'number', 96 | style: 'normal', 97 | level: 1, 98 | _type: 'block', 99 | _key: '6dbc061d5d36', 100 | markDefs: [], 101 | children: [ 102 | { 103 | _type: 'span', 104 | text: 'Item 3', 105 | marks: [], 106 | }, 107 | ], 108 | }, 109 | { 110 | style: 'normal', 111 | _type: 'block', 112 | _key: 'bb89bd1ef2c9', 113 | markDefs: [], 114 | children: [ 115 | { 116 | _type: 'span', 117 | text: '', 118 | marks: [], 119 | }, 120 | ], 121 | }, 122 | { 123 | listItem: 'bullet', 124 | style: 'normal', 125 | level: 1, 126 | _type: 'block', 127 | _key: '289c1f176eab', 128 | markDefs: [], 129 | children: [ 130 | { 131 | _type: 'span', 132 | text: 'In', 133 | marks: [], 134 | }, 135 | ], 136 | }, 137 | { 138 | listItem: 'bullet', 139 | style: 'normal', 140 | level: 2, 141 | _type: 'block', 142 | _key: '011f8cc6d19b', 143 | markDefs: [], 144 | children: [ 145 | { 146 | _type: 'span', 147 | text: 'Out', 148 | marks: [], 149 | }, 150 | ], 151 | }, 152 | { 153 | listItem: 'bullet', 154 | style: 'normal', 155 | level: 1, 156 | _type: 'block', 157 | _key: 'ccfb4e37b798', 158 | markDefs: [], 159 | children: [ 160 | { 161 | _type: 'span', 162 | text: 'In', 163 | marks: [], 164 | }, 165 | ], 166 | }, 167 | { 168 | listItem: 'bullet', 169 | style: 'normal', 170 | level: 2, 171 | _type: 'block', 172 | _key: 'bd0102405e5c', 173 | markDefs: [], 174 | children: [ 175 | { 176 | _type: 'span', 177 | text: 'Out', 178 | marks: [], 179 | }, 180 | ], 181 | }, 182 | { 183 | listItem: 'bullet', 184 | style: 'normal', 185 | level: 3, 186 | _type: 'block', 187 | _key: '030fda546030', 188 | markDefs: [], 189 | children: [ 190 | { 191 | _type: 'span', 192 | text: 'Even More', 193 | marks: [], 194 | }, 195 | ], 196 | }, 197 | { 198 | listItem: 'bullet', 199 | style: 'normal', 200 | level: 4, 201 | _type: 'block', 202 | _key: '80369435aed0', 203 | markDefs: [], 204 | children: [ 205 | { 206 | _type: 'span', 207 | text: 'Even deeper', 208 | marks: [], 209 | }, 210 | ], 211 | }, 212 | { 213 | listItem: 'bullet', 214 | style: 'normal', 215 | level: 2, 216 | _type: 'block', 217 | _key: '3b36919a8914', 218 | markDefs: [], 219 | children: [ 220 | { 221 | _type: 'span', 222 | text: 'Two steps back', 223 | marks: [], 224 | }, 225 | ], 226 | }, 227 | { 228 | listItem: 'bullet', 229 | style: 'normal', 230 | level: 1, 231 | _type: 'block', 232 | _key: '9193cbc6ba54', 233 | markDefs: [], 234 | children: [ 235 | { 236 | _type: 'span', 237 | text: 'All the way back', 238 | marks: [], 239 | }, 240 | ], 241 | }, 242 | { 243 | listItem: 'bullet', 244 | style: 'normal', 245 | level: 3, 246 | _type: 'block', 247 | _key: '256fe8487d7a', 248 | markDefs: [], 249 | children: [ 250 | { 251 | _type: 'span', 252 | text: 'Skip a step', 253 | marks: [], 254 | }, 255 | ], 256 | }, 257 | { 258 | listItem: 'number', 259 | style: 'normal', 260 | level: 1, 261 | _type: 'block', 262 | _key: 'aaa', 263 | markDefs: [], 264 | children: [ 265 | { 266 | _type: 'span', 267 | text: 'New list', 268 | marks: [], 269 | }, 270 | ], 271 | }, 272 | { 273 | listItem: 'number', 274 | style: 'normal', 275 | level: 2, 276 | _type: 'block', 277 | _key: 'bbb', 278 | markDefs: [], 279 | children: [ 280 | { 281 | _type: 'span', 282 | text: 'Next level', 283 | marks: [], 284 | }, 285 | ], 286 | }, 287 | { 288 | listItem: 'bullet', 289 | style: 'normal', 290 | level: 1, 291 | _type: 'block', 292 | _key: 'ccc', 293 | markDefs: [], 294 | children: [ 295 | { 296 | _type: 'span', 297 | text: 'New bullet list', 298 | marks: [], 299 | }, 300 | ], 301 | }, 302 | ] 303 | 304 | export default { 305 | input, 306 | output: [ 307 | '
    ', 308 | '
  • Item a
  • ', 309 | '
  • Item b
  • ', 310 | '
', 311 | '
    ', 312 | '
  1. Item 1
  2. ', 313 | '
  3. ', 314 | 'Item 2', 315 | '
      ', 316 | '
    1. Item 2, a
    2. ', 317 | '
    3. Item 2, b
    4. ', 318 | '
    ', 319 | '
  4. ', 320 | '
  5. Item 3
  6. ', 321 | '
', 322 | '

', 323 | '
    ', 324 | '
  • ', 325 | 'In', 326 | '
      ', 327 | '
    • Out
    • ', 328 | '
    ', 329 | '
  • ', 330 | '
  • ', 331 | 'In', 332 | '
      ', 333 | '
    • ', 334 | 'Out', 335 | '
        ', 336 | '
      • ', 337 | 'Even More', 338 | '
          ', 339 | '
        • Even deeper
        • ', 340 | '
        ', 341 | '
      • ', 342 | '
      ', 343 | '
    • ', 344 | '
    • Two steps back
    • ', 345 | '
    ', 346 | '
  • ', 347 | '
  • ', 348 | 'All the way back', 349 | '
      ', 350 | '
    • Skip a step
    • ', 351 | '
    ', 352 | '
  • ', 353 | '
', 354 | '
    ', 355 | '
  1. New list
    1. Next level
  2. ', 356 | '
', 357 | '
  • New bullet list
', 358 | ].join(''), 359 | } 360 | -------------------------------------------------------------------------------- /test/fixtures/017-all-default-block-styles.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | style: 'h1', 6 | _type: 'block', 7 | _key: 'b07278ae4e5a', 8 | markDefs: [], 9 | children: [ 10 | { 11 | _type: 'span', 12 | text: 'Sanity', 13 | marks: [], 14 | }, 15 | ], 16 | }, 17 | { 18 | style: 'h2', 19 | _type: 'block', 20 | _key: '0546428bbac2', 21 | markDefs: [], 22 | children: [ 23 | { 24 | _type: 'span', 25 | text: 'The outline', 26 | marks: [], 27 | }, 28 | ], 29 | }, 30 | { 31 | style: 'h3', 32 | _type: 'block', 33 | _key: '34024674e160', 34 | markDefs: [], 35 | children: [ 36 | { 37 | _type: 'span', 38 | text: 'More narrow details', 39 | marks: [], 40 | }, 41 | ], 42 | }, 43 | { 44 | style: 'h4', 45 | _type: 'block', 46 | _key: '06ca981a1d18', 47 | markDefs: [], 48 | children: [ 49 | { 50 | _type: 'span', 51 | text: 'Even less thing', 52 | marks: [], 53 | }, 54 | ], 55 | }, 56 | { 57 | style: 'h5', 58 | _type: 'block', 59 | _key: '06ca98afnjkg', 60 | markDefs: [], 61 | children: [ 62 | { 63 | _type: 'span', 64 | text: 'Small header', 65 | marks: [], 66 | }, 67 | ], 68 | }, 69 | { 70 | style: 'h6', 71 | _type: 'block', 72 | _key: 'cc0afafn', 73 | markDefs: [], 74 | children: [ 75 | { 76 | _type: 'span', 77 | text: 'Lowest thing', 78 | marks: [], 79 | }, 80 | ], 81 | }, 82 | { 83 | style: 'blockquote', 84 | _type: 'block', 85 | _key: '0ee0381658d0', 86 | markDefs: [], 87 | children: [ 88 | { 89 | _type: 'span', 90 | text: 'A block quote of awesomeness', 91 | marks: [], 92 | }, 93 | ], 94 | }, 95 | { 96 | style: 'normal', 97 | _type: 'block', 98 | _key: '44fb584a634c', 99 | markDefs: [], 100 | children: [ 101 | { 102 | _type: 'span', 103 | text: 'Plain old normal block', 104 | marks: [], 105 | }, 106 | ], 107 | }, 108 | { 109 | _type: 'block', 110 | _key: 'abcdefg', 111 | markDefs: [], 112 | children: [ 113 | { 114 | _type: 'span', 115 | text: 'Default to "normal" style', 116 | marks: [], 117 | }, 118 | ], 119 | }, 120 | ] 121 | 122 | export default { 123 | input, 124 | output: [ 125 | '

Sanity

', 126 | '

The outline

', 127 | '

More narrow details

', 128 | '

Even less thing

', 129 | '
Small header
', 130 | '
Lowest thing
', 131 | '
A block quote of awesomeness
', 132 | '

Plain old normal block

', 133 | '

Default to "normal" style

', 134 | ].join(''), 135 | } 136 | -------------------------------------------------------------------------------- /test/fixtures/018-marks-all-the-way-down.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _type: 'block', 5 | children: [ 6 | { 7 | _key: 'a1ph4', 8 | _type: 'span', 9 | marks: ['mark1', 'em', 'mark2'], 10 | text: 'Sanity', 11 | }, 12 | { 13 | _key: 'b374', 14 | _type: 'span', 15 | marks: ['mark2', 'mark1', 'em'], 16 | text: ' FTW', 17 | }, 18 | ], 19 | markDefs: [ 20 | { 21 | _key: 'mark1', 22 | _type: 'highlight', 23 | thickness: 1, 24 | }, 25 | { 26 | _key: 'mark2', 27 | _type: 'highlight', 28 | thickness: 3, 29 | }, 30 | ], 31 | } 32 | 33 | export default { 34 | input, 35 | output: [ 36 | '

', 37 | '', 38 | '', 39 | 'Sanity FTW', 40 | '', 41 | '', 42 | '

', 43 | ].join(''), 44 | } 45 | -------------------------------------------------------------------------------- /test/fixtures/019-keyless.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | _type: 'block', 6 | children: [ 7 | { 8 | _type: 'span', 9 | marks: [], 10 | text: 'sanity', 11 | }, 12 | { 13 | _type: 'span', 14 | marks: [], 15 | text: ' is a full time job', 16 | }, 17 | ], 18 | markDefs: [], 19 | style: 'normal', 20 | }, 21 | { 22 | _type: 'block', 23 | children: [ 24 | { 25 | _type: 'span', 26 | marks: [], 27 | text: 'in a world that ', 28 | }, 29 | { 30 | _type: 'span', 31 | marks: [], 32 | text: 'is always changing', 33 | }, 34 | ], 35 | markDefs: [], 36 | style: 'normal', 37 | }, 38 | ] 39 | 40 | export default { 41 | input, 42 | output: '

sanity is a full time job

in a world that is always changing

', 43 | } 44 | -------------------------------------------------------------------------------- /test/fixtures/020-empty-array.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | input: [], 3 | output: '', 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/021-list-without-level.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | _key: 'e3ac53b5b339', 6 | _type: 'block', 7 | children: [ 8 | { 9 | _type: 'span', 10 | marks: [], 11 | text: 'In-person access: Research appointments', 12 | }, 13 | ], 14 | markDefs: [], 15 | style: 'h2', 16 | }, 17 | { 18 | _key: 'a25f0be55c47', 19 | _type: 'block', 20 | children: [ 21 | { 22 | _type: 'span', 23 | marks: [], 24 | text: 'The collection may be examined by arranging a research appointment ', 25 | }, 26 | { 27 | _type: 'span', 28 | marks: ['strong'], 29 | text: 'in advance', 30 | }, 31 | { 32 | _type: 'span', 33 | marks: [], 34 | text: ' by contacting the ACT archivist by email or phone. ACT generally does not accept walk-in research patrons, although requests may be made in person at the Archivist’s office (E15-222). ACT recommends arranging appointments at least three weeks in advance in order to ensure availability. ACT reserves the right to cancel research appointments at any time. Appointment scheduling is subject to institute holidays and closings. ', 35 | }, 36 | ], 37 | markDefs: [], 38 | style: 'normal', 39 | }, 40 | { 41 | _key: '9490a3085498', 42 | _type: 'block', 43 | children: [ 44 | { 45 | _type: 'span', 46 | marks: [], 47 | text: 'The collection space is located at:\n20 Ames Street\nBuilding E15-235\nCambridge, Massachusetts 02139', 48 | }, 49 | ], 50 | markDefs: [], 51 | style: 'normal', 52 | }, 53 | { 54 | _key: '4c37f3bc1d71', 55 | _type: 'block', 56 | children: [ 57 | { 58 | _type: 'span', 59 | marks: [], 60 | text: 'In-person access: Space policies', 61 | }, 62 | ], 63 | markDefs: [], 64 | style: 'h2', 65 | }, 66 | { 67 | _key: 'a77cf4905e83', 68 | _type: 'block', 69 | children: [ 70 | { 71 | _type: 'span', 72 | marks: [], 73 | text: 'The Archivist or an authorized ACT staff member must attend researchers at all times.', 74 | }, 75 | ], 76 | listItem: 'bullet', 77 | markDefs: [], 78 | style: 'normal', 79 | }, 80 | { 81 | _key: '9a039c533554', 82 | _type: 'block', 83 | children: [ 84 | { 85 | _type: 'span', 86 | marks: [], 87 | text: 'No pens, markers, or adhesives (e.g. “Post-it” notes) are permitted in the collection space; pencils will be provided upon request.', 88 | }, 89 | ], 90 | listItem: 'bullet', 91 | markDefs: [], 92 | style: 'normal', 93 | }, 94 | { 95 | _key: 'beeee9405136', 96 | _type: 'block', 97 | children: [ 98 | { 99 | _type: 'span', 100 | marks: [], 101 | text: 'Cotton gloves must be worn when handling collection materials; gloves will be provided by the Archivist.', 102 | }, 103 | ], 104 | listItem: 'bullet', 105 | markDefs: [], 106 | style: 'normal', 107 | }, 108 | { 109 | _key: '8b78daa65d60', 110 | _type: 'block', 111 | children: [ 112 | { 113 | _type: 'span', 114 | marks: [], 115 | text: 'No food or beverages are permitted in the collection space.', 116 | }, 117 | ], 118 | listItem: 'bullet', 119 | markDefs: [], 120 | style: 'normal', 121 | }, 122 | { 123 | _key: 'd0188e00a887', 124 | _type: 'block', 125 | children: [ 126 | { 127 | _type: 'span', 128 | marks: [], 129 | text: 'Laptop use is permitted in the collection space, as well as digital cameras and cellphones. Unless otherwise authorized, any equipment in the collection space (including but not limited to computers, telephones, scanners, and viewing equipment) is for use by ACT staff members only.', 130 | }, 131 | ], 132 | listItem: 'bullet', 133 | markDefs: [], 134 | style: 'normal', 135 | }, 136 | { 137 | _key: '06486dd9e1c6', 138 | _type: 'block', 139 | children: [ 140 | { 141 | _type: 'span', 142 | marks: [], 143 | text: 'Photocopying machines in the ACT hallway will be accessible by patrons under the supervision of the Archivist.', 144 | }, 145 | ], 146 | listItem: 'bullet', 147 | markDefs: [], 148 | style: 'normal', 149 | }, 150 | { 151 | _key: 'e6f6f5255fb6', 152 | _type: 'block', 153 | children: [ 154 | { 155 | _type: 'span', 156 | marks: [], 157 | text: 'Patrons may only browse materials that have been made available for access.', 158 | }, 159 | ], 160 | listItem: 'bullet', 161 | markDefs: [], 162 | style: 'normal', 163 | }, 164 | { 165 | _key: '99b3e265fa02', 166 | _type: 'block', 167 | children: [ 168 | { 169 | _type: 'span', 170 | marks: [], 171 | text: 'Remote access: Reference requests', 172 | }, 173 | ], 174 | markDefs: [], 175 | style: 'h2', 176 | }, 177 | { 178 | _key: 'ea13459d9e46', 179 | _type: 'block', 180 | children: [ 181 | { 182 | _type: 'span', 183 | marks: [], 184 | text: 'For patrons who are unable to arrange for an on-campus visit to the Archives and Special Collections, reference questions may be directed to the Archivist remotely by email or phone. Generally, emails and phone calls will receive a response within 72 hours of receipt. Requests are typically filled in the order they are received.', 185 | }, 186 | ], 187 | markDefs: [], 188 | style: 'normal', 189 | }, 190 | { 191 | _key: '100958e35c94', 192 | _type: 'block', 193 | children: [ 194 | { 195 | _type: 'span', 196 | marks: ['strong'], 197 | text: 'Use of patron information', 198 | }, 199 | ], 200 | markDefs: [], 201 | style: 'h2', 202 | }, 203 | { 204 | _key: '2e0dde67b7df', 205 | _type: 'block', 206 | children: [ 207 | { 208 | _type: 'span', 209 | marks: [], 210 | text: 'Patrons requesting collection materials in person or remotely may be asked to provide certain information to the Archivist, such as contact information and topic(s) of research. This information is only used to track requests for statistical evaluations of collection use and will not be disclosed to outside organizations for any purpose. ACT will endeavor to protect the privacy of all patrons accessing collections.', 211 | }, 212 | ], 213 | markDefs: [], 214 | style: 'normal', 215 | }, 216 | { 217 | _key: '8f39a1ec6366', 218 | _type: 'block', 219 | children: [ 220 | { 221 | _type: 'span', 222 | marks: ['strong'], 223 | text: 'Fees', 224 | }, 225 | ], 226 | markDefs: [], 227 | style: 'h2', 228 | }, 229 | { 230 | _key: '090062c9e8ce', 231 | _type: 'block', 232 | children: [ 233 | { 234 | _type: 'span', 235 | marks: [], 236 | text: 'ACT reserves the right to charge an hourly rate for requests that require more than three hours of research on behalf of a patron (remote requests). Collection materials may be scanned and made available upon request, but digitization of certain materials may incur costs. Additionally, requests to publish, exhibit, or otherwise reproduce and display collection materials may incur use fees.', 237 | }, 238 | ], 239 | markDefs: [], 240 | style: 'normal', 241 | }, 242 | { 243 | _key: 'e2b58e246069', 244 | _type: 'block', 245 | children: [ 246 | { 247 | _type: 'span', 248 | marks: ['strong'], 249 | text: 'Use of MIT-owned materials by patrons', 250 | }, 251 | ], 252 | markDefs: [], 253 | style: 'h2', 254 | }, 255 | { 256 | _key: '7cedb6800dc6', 257 | _type: 'block', 258 | children: [ 259 | { 260 | _type: 'span', 261 | marks: [], 262 | text: 'Permission to examine collection materials in person or remotely (by receiving transfers of digitized materials) does not imply or grant permission to publish or exhibit those materials. Permission to publish, exhibit, or otherwise use collection materials is granted on a case by case basis in accordance with MIT policy, restrictions that may have been placed on materials by donors or depositors, and copyright law. To request permission to publish, exhibit, or otherwise use collection materials, contact the Archivist. ', 263 | }, 264 | { 265 | _type: 'span', 266 | marks: ['strong'], 267 | text: 'When permission is granted by MIT, patrons must comply with all guidelines provided by ACT for citations, credits, and copyright statements. Exclusive rights to examine or publish material will not be granted.', 268 | }, 269 | ], 270 | markDefs: [], 271 | style: 'normal', 272 | }, 273 | ] 274 | 275 | export default { 276 | input, 277 | output: `

In-person access: Research appointments

The collection may be examined by arranging a research appointment in advance by contacting the ACT archivist by email or phone. ACT generally does not accept walk-in research patrons, although requests may be made in person at the Archivist’s office (E15-222). ACT recommends arranging appointments at least three weeks in advance in order to ensure availability. ACT reserves the right to cancel research appointments at any time. Appointment scheduling is subject to institute holidays and closings.

The collection space is located at:
20 Ames Street
Building E15-235
Cambridge, Massachusetts 02139

In-person access: Space policies

  • The Archivist or an authorized ACT staff member must attend researchers at all times.
  • No pens, markers, or adhesives (e.g. “Post-it” notes) are permitted in the collection space; pencils will be provided upon request.
  • Cotton gloves must be worn when handling collection materials; gloves will be provided by the Archivist.
  • No food or beverages are permitted in the collection space.
  • Laptop use is permitted in the collection space, as well as digital cameras and cellphones. Unless otherwise authorized, any equipment in the collection space (including but not limited to computers, telephones, scanners, and viewing equipment) is for use by ACT staff members only.
  • Photocopying machines in the ACT hallway will be accessible by patrons under the supervision of the Archivist.
  • Patrons may only browse materials that have been made available for access.

Remote access: Reference requests

For patrons who are unable to arrange for an on-campus visit to the Archives and Special Collections, reference questions may be directed to the Archivist remotely by email or phone. Generally, emails and phone calls will receive a response within 72 hours of receipt. Requests are typically filled in the order they are received.

Use of patron information

Patrons requesting collection materials in person or remotely may be asked to provide certain information to the Archivist, such as contact information and topic(s) of research. This information is only used to track requests for statistical evaluations of collection use and will not be disclosed to outside organizations for any purpose. ACT will endeavor to protect the privacy of all patrons accessing collections.

Fees

ACT reserves the right to charge an hourly rate for requests that require more than three hours of research on behalf of a patron (remote requests). Collection materials may be scanned and made available upon request, but digitization of certain materials may incur costs. Additionally, requests to publish, exhibit, or otherwise reproduce and display collection materials may incur use fees.

Use of MIT-owned materials by patrons

Permission to examine collection materials in person or remotely (by receiving transfers of digitized materials) does not imply or grant permission to publish or exhibit those materials. Permission to publish, exhibit, or otherwise use collection materials is granted on a case by case basis in accordance with MIT policy, restrictions that may have been placed on materials by donors or depositors, and copyright law. To request permission to publish, exhibit, or otherwise use collection materials, contact the Archivist. When permission is granted by MIT, patrons must comply with all guidelines provided by ACT for citations, credits, and copyright statements. Exclusive rights to examine or publish material will not be granted.

`, 278 | } 279 | -------------------------------------------------------------------------------- /test/fixtures/022-inline-nodes.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | _type: 'block', 6 | _key: 'bd73ec5f61a1', 7 | style: 'normal', 8 | markDefs: [], 9 | children: [ 10 | { 11 | _type: 'span', 12 | text: "I enjoyed it. It's not perfect, but I give it a strong ", 13 | marks: [], 14 | }, 15 | { 16 | _type: 'rating', 17 | _key: 'd234a4fa317a', 18 | type: 'dice', 19 | rating: 5, 20 | }, 21 | { 22 | _type: 'span', 23 | text: ', and look forward to the next season!', 24 | marks: [], 25 | }, 26 | ], 27 | }, 28 | { 29 | _type: 'block', 30 | _key: 'foo', 31 | markDefs: [], 32 | children: [ 33 | { 34 | _type: 'span', 35 | text: 'Sibling paragraph', 36 | marks: [], 37 | }, 38 | ], 39 | }, 40 | ] 41 | 42 | export default { 43 | input, 44 | output: [ 45 | '

I enjoyed it. It's not perfect, but I give it a strong ', 46 | '', 47 | ', and look forward to the next season!

', 48 | '

Sibling paragraph

', 49 | ].join(''), 50 | } 51 | -------------------------------------------------------------------------------- /test/fixtures/023-hard-breaks.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | _type: 'block', 6 | _key: 'bd73ec5f61a1', 7 | style: 'normal', 8 | markDefs: [], 9 | children: [ 10 | { 11 | _type: 'span', 12 | text: 'A paragraph\ncan have hard\n\nbreaks.', 13 | marks: [], 14 | }, 15 | ], 16 | }, 17 | ] 18 | 19 | export default { 20 | input, 21 | output: '

A paragraph
can have hard

breaks.

', 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/024-inline-images.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | _key: '08707ed2945b', 6 | _type: 'block', 7 | style: 'normal', 8 | children: [ 9 | { 10 | _key: '08707ed2945b0', 11 | text: 'Foo! Bar!', 12 | _type: 'span', 13 | marks: ['code'], 14 | }, 15 | { 16 | _key: 'a862cadb584f', 17 | _type: 'image', 18 | url: 'https://cdn.provider/star.png', 19 | }, 20 | {_key: '08707ed2945b1', text: 'Neat', _type: 'span', marks: []}, 21 | ], 22 | markDefs: [], 23 | }, 24 | 25 | { 26 | _key: 'abc', 27 | _type: 'block', 28 | style: 'normal', 29 | children: [ 30 | { 31 | _key: '08707ed2945b0', 32 | text: 'Foo! Bar! ', 33 | _type: 'span', 34 | marks: ['code'], 35 | }, 36 | { 37 | _key: 'a862cadb584f', 38 | _type: 'image', 39 | url: 'https://cdn.provider/star.png', 40 | }, 41 | {_key: '08707ed2945b1', text: ' Baz!', _type: 'span', marks: ['code']}, 42 | ], 43 | markDefs: [], 44 | }, 45 | 46 | { 47 | _key: 'def', 48 | _type: 'block', 49 | style: 'normal', 50 | children: [ 51 | { 52 | _key: '08707ed2945b0', 53 | text: 'Foo! Bar! ', 54 | _type: 'span', 55 | marks: [], 56 | }, 57 | { 58 | _key: 'a862cadb584f', 59 | _type: 'image', 60 | url: 'https://cdn.provider/star.png', 61 | }, 62 | {_key: '08707ed2945b1', text: ' Baz!', _type: 'span', marks: ['code']}, 63 | ], 64 | markDefs: [], 65 | }, 66 | ] 67 | 68 | export default { 69 | input, 70 | output: 71 | '

Foo! Bar!Neat

Foo! Bar! Baz!

Foo! Bar! Baz!

', 72 | } 73 | -------------------------------------------------------------------------------- /test/fixtures/025-image-with-hotspot.ts: -------------------------------------------------------------------------------- 1 | import type {ArbitraryTypedObject, PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: (PortableTextBlock | ArbitraryTypedObject)[] = [ 4 | { 5 | style: 'normal', 6 | _type: 'block', 7 | _key: 'bd73ec5f61a1', 8 | markDefs: [], 9 | children: [ 10 | { 11 | _type: 'span', 12 | text: 'Also, images are pretty common.', 13 | marks: [], 14 | }, 15 | ], 16 | }, 17 | { 18 | _type: 'image', 19 | _key: '53811e851487', 20 | asset: { 21 | _type: 'reference', 22 | _ref: 'image-c2f0fc30003e6d7c79dcb5338a9b3d297cab4a8a-2000x1333-jpg', 23 | }, 24 | crop: { 25 | top: 0.0960960960960961, 26 | bottom: 0.09609609609609615, 27 | left: 0.2340000000000001, 28 | right: 0.2240000000000001, 29 | }, 30 | hotspot: { 31 | /* eslint-disable id-length */ 32 | x: 0.505, 33 | y: 0.49999999999999994, 34 | /* eslint-enable id-length */ 35 | height: 0.8078078078078077, 36 | width: 0.5419999999999998, 37 | }, 38 | }, 39 | ] 40 | 41 | export default { 42 | input, 43 | output: [ 44 | '
', 45 | '

Also, images are pretty common.

', 46 | '
', 47 | '
', 48 | ].join(''), 49 | } 50 | -------------------------------------------------------------------------------- /test/fixtures/026-inline-block-with-text.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | _type: 'block', 6 | _key: 'foo', 7 | style: 'normal', 8 | children: [ 9 | {_type: 'span', text: 'Men, '}, 10 | {_type: 'button', text: 'bli med du også'}, 11 | {_type: 'span', text: ', da!'}, 12 | ], 13 | }, 14 | ] 15 | 16 | export default { 17 | input, 18 | output: '

Men, , da!

', 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/027-styled-list-items.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | style: 'normal', 6 | _type: 'block', 7 | _key: 'f94596b05b41', 8 | markDefs: [], 9 | children: [ 10 | { 11 | _type: 'span', 12 | text: "Let's test some of these lists!", 13 | marks: [], 14 | }, 15 | ], 16 | }, 17 | { 18 | listItem: 'bullet', 19 | style: 'normal', 20 | level: 1, 21 | _type: 'block', 22 | _key: '937effb1cd06', 23 | markDefs: [], 24 | children: [ 25 | { 26 | _type: 'span', 27 | text: 'Bullet 1', 28 | marks: [], 29 | }, 30 | ], 31 | }, 32 | { 33 | listItem: 'bullet', 34 | style: 'h1', 35 | level: 1, 36 | _type: 'block', 37 | _key: 'bd2d22278b88', 38 | markDefs: [], 39 | children: [ 40 | { 41 | _type: 'span', 42 | text: 'Bullet 2', 43 | marks: [], 44 | }, 45 | ], 46 | }, 47 | { 48 | listItem: 'bullet', 49 | style: 'normal', 50 | level: 1, 51 | _type: 'block', 52 | _key: 'a97d32e9f747', 53 | markDefs: [], 54 | children: [ 55 | { 56 | _type: 'span', 57 | text: 'Bullet 3', 58 | marks: [], 59 | }, 60 | ], 61 | }, 62 | ] 63 | 64 | export default { 65 | input, 66 | output: [ 67 | '

Let's test some of these lists!

', 68 | '
    ', 69 | '
  • Bullet 1
  • ', 70 | '
  • Bullet 2

  • ', 71 | '
  • Bullet 3
  • ', 72 | '
', 73 | ].join(''), 74 | } 75 | -------------------------------------------------------------------------------- /test/fixtures/028-custom-list-item-type.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | listItem: 'square', 6 | style: 'normal', 7 | level: 1, 8 | _type: 'block', 9 | _key: '937effb1cd06', 10 | markDefs: [], 11 | children: [ 12 | { 13 | _type: 'span', 14 | text: 'Square 1', 15 | marks: [], 16 | }, 17 | ], 18 | }, 19 | { 20 | listItem: 'square', 21 | style: 'normal', 22 | level: 1, 23 | _type: 'block', 24 | _key: 'bd2d22278b88', 25 | markDefs: [], 26 | children: [ 27 | { 28 | _type: 'span', 29 | text: 'Square 2', 30 | marks: [], 31 | }, 32 | ], 33 | }, 34 | { 35 | listItem: 'disc', 36 | style: 'normal', 37 | level: 2, 38 | _type: 'block', 39 | _key: 'a97d32e9f747', 40 | markDefs: [], 41 | children: [ 42 | { 43 | _type: 'span', 44 | text: 'Dat disc', 45 | marks: [], 46 | }, 47 | ], 48 | }, 49 | { 50 | listItem: 'square', 51 | style: 'normal', 52 | level: 1, 53 | _type: 'block', 54 | _key: 'a97d32e9f747', 55 | markDefs: [], 56 | children: [ 57 | { 58 | _type: 'span', 59 | text: 'Square 3', 60 | marks: [], 61 | }, 62 | ], 63 | }, 64 | ] 65 | 66 | export default { 67 | input, 68 | output: [ 69 | '
    ', 70 | '
  • Square 1
  • ', 71 | '
  • ', 72 | ' Square 2', 73 | '
      ', 74 | '
    • Dat disc
    • ', 75 | '
    ', 76 | '
  • ', 77 | '
  • Square 3
  • ', 78 | '
', 79 | ] 80 | .map((line) => line.trim()) 81 | .join(''), 82 | } 83 | -------------------------------------------------------------------------------- /test/fixtures/050-custom-block-type.ts: -------------------------------------------------------------------------------- 1 | import type {ArbitraryTypedObject} from '@portabletext/types' 2 | 3 | const input: ArbitraryTypedObject[] = [ 4 | { 5 | _type: 'code', 6 | _key: '9a15ea2ed8a2', 7 | language: 'javascript', 8 | code: "const foo = require('foo')\n\nfoo('hi there', (err, thing) => {\n console.log(err)\n})\n", 9 | }, 10 | ] 11 | 12 | export default { 13 | input, 14 | output: [ 15 | '
',
16 |     '',
17 |     'const foo = require('foo')\n\n',
18 |     'foo('hi there', (err, thing) => {\n',
19 |     '  console.log(err)\n',
20 |     '})\n',
21 |     '
', 22 | ].join(''), 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/051-override-defaults.ts: -------------------------------------------------------------------------------- 1 | import type {ArbitraryTypedObject} from '@portabletext/types' 2 | 3 | const input: ArbitraryTypedObject[] = [ 4 | { 5 | _type: 'image', 6 | _key: 'd234a4fa317a', 7 | asset: { 8 | _type: 'reference', 9 | _ref: 'image-YiOKD0O6AdjKPaK24WtbOEv0-3456x2304-jpg', 10 | }, 11 | }, 12 | ] 13 | 14 | export default { 15 | input, 16 | output: [ 17 | 'Such image', 20 | ].join(''), 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/052-custom-marks.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _type: 'block', 5 | children: [ 6 | { 7 | _key: 'a1ph4', 8 | _type: 'span', 9 | marks: ['mark1'], 10 | text: 'Sanity', 11 | }, 12 | ], 13 | markDefs: [ 14 | { 15 | _key: 'mark1', 16 | _type: 'highlight', 17 | thickness: 5, 18 | }, 19 | ], 20 | } 21 | 22 | export default { 23 | input, 24 | output: '

Sanity

', 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/053-override-default-marks.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _type: 'block', 5 | children: [ 6 | { 7 | _key: 'a1ph4', 8 | _type: 'span', 9 | marks: ['mark1'], 10 | text: 'Sanity', 11 | }, 12 | ], 13 | markDefs: [ 14 | { 15 | _key: 'mark1', 16 | _type: 'link', 17 | href: 'https://sanity.io', 18 | }, 19 | ], 20 | } 21 | 22 | export default { 23 | input, 24 | output: '

Sanity

', 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/060-list-issue.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock[] = [ 4 | { 5 | _key: '68e32a42bc86', 6 | _type: 'block', 7 | children: [ 8 | { 9 | _type: 'span', 10 | marks: [], 11 | text: 'Lorem ipsum', 12 | }, 13 | ], 14 | markDefs: [], 15 | style: 'h2', 16 | }, 17 | { 18 | _key: 'e5a6349a2145', 19 | _type: 'block', 20 | children: [ 21 | { 22 | _type: 'span', 23 | marks: [], 24 | text: 'Lorem ipsum', 25 | }, 26 | ], 27 | markDefs: [], 28 | style: 'normal', 29 | }, 30 | { 31 | _key: '22659f66b40b', 32 | _type: 'block', 33 | children: [ 34 | { 35 | _type: 'span', 36 | marks: [], 37 | text: 'Lorem ipsum', 38 | }, 39 | ], 40 | level: 1, 41 | listItem: 'number', 42 | markDefs: [], 43 | style: 'normal', 44 | }, 45 | { 46 | _key: '87b8d684fb9e', 47 | _type: 'block', 48 | children: [ 49 | { 50 | _type: 'span', 51 | marks: [], 52 | text: 'Lorem ipsum', 53 | }, 54 | ], 55 | level: 1, 56 | listItem: 'number', 57 | markDefs: [], 58 | style: 'normal', 59 | }, 60 | { 61 | _key: 'a14d35e806c5', 62 | _type: 'block', 63 | children: [ 64 | { 65 | _type: 'span', 66 | marks: [], 67 | text: 'Lorem ipsum', 68 | }, 69 | ], 70 | level: 1, 71 | listItem: 'number', 72 | markDefs: [], 73 | style: 'normal', 74 | }, 75 | { 76 | _key: '4bc360f7123a', 77 | _type: 'block', 78 | children: [ 79 | { 80 | _type: 'span', 81 | marks: [], 82 | text: 'Lorem ipsum', 83 | }, 84 | ], 85 | level: 1, 86 | listItem: 'number', 87 | markDefs: [], 88 | style: 'normal', 89 | }, 90 | { 91 | _key: '22f50c9e40a6', 92 | _type: 'block', 93 | children: [ 94 | { 95 | _type: 'span', 96 | marks: [], 97 | text: 'Lorem ipsum', 98 | }, 99 | ], 100 | level: 1, 101 | listItem: 'number', 102 | markDefs: [], 103 | style: 'normal', 104 | }, 105 | { 106 | _key: '664cca534e5d', 107 | _type: 'block', 108 | children: [ 109 | { 110 | _type: 'span', 111 | marks: [], 112 | text: 'Lorem ipsum', 113 | }, 114 | ], 115 | level: 1, 116 | listItem: 'number', 117 | markDefs: [], 118 | style: 'normal', 119 | }, 120 | { 121 | _key: '1e9b2d0b4ef6', 122 | _type: 'block', 123 | children: [ 124 | { 125 | _type: 'span', 126 | marks: [], 127 | text: 'Lorem ipsum', 128 | }, 129 | ], 130 | level: 1, 131 | listItem: 'number', 132 | markDefs: [], 133 | style: 'normal', 134 | }, 135 | { 136 | _key: '24ede750fde5', 137 | _type: 'block', 138 | children: [ 139 | { 140 | _type: 'span', 141 | marks: [], 142 | text: 'Lorem ipsum', 143 | }, 144 | ], 145 | level: 1, 146 | listItem: 'number', 147 | markDefs: [], 148 | style: 'normal', 149 | }, 150 | { 151 | _key: '89eeaeac72c5', 152 | _type: 'block', 153 | children: [ 154 | { 155 | _type: 'span', 156 | marks: [], 157 | text: 'Lorem ipsum', 158 | }, 159 | ], 160 | level: 1, 161 | listItem: 'number', 162 | markDefs: [], 163 | style: 'normal', 164 | }, 165 | { 166 | _key: '993fb23a4fbb', 167 | _type: 'block', 168 | children: [ 169 | { 170 | _type: 'span', 171 | marks: [], 172 | text: 'Lorem ipsum', 173 | }, 174 | ], 175 | level: 1, 176 | listItem: 'number', 177 | markDefs: [], 178 | style: 'normal', 179 | }, 180 | { 181 | _key: '09b00b82c010', 182 | _type: 'block', 183 | children: [ 184 | { 185 | _type: 'span', 186 | marks: [], 187 | text: 'Lorem ipsum', 188 | }, 189 | ], 190 | markDefs: [], 191 | style: 'h2', 192 | }, 193 | { 194 | _key: 'e1ec0b8bccbe', 195 | _type: 'block', 196 | children: [ 197 | { 198 | _type: 'span', 199 | marks: [], 200 | text: 'Lorem ipsum', 201 | }, 202 | ], 203 | markDefs: [], 204 | style: 'normal', 205 | }, 206 | { 207 | _key: 'ff11fb1a52ad', 208 | _type: 'block', 209 | children: [ 210 | { 211 | _type: 'span', 212 | marks: [], 213 | text: 'Lorem ipsum', 214 | }, 215 | ], 216 | markDefs: [], 217 | style: 'normal', 218 | }, 219 | { 220 | _key: '034604cee2d9', 221 | _type: 'block', 222 | children: [ 223 | { 224 | _type: 'span', 225 | marks: [], 226 | text: 'Lorem ipsum', 227 | }, 228 | ], 229 | markDefs: [], 230 | style: 'normal', 231 | }, 232 | { 233 | _key: '836431a777a8', 234 | _type: 'block', 235 | children: [ 236 | { 237 | _type: 'span', 238 | marks: [], 239 | text: 'Lorem ipsum', 240 | }, 241 | ], 242 | markDefs: [], 243 | style: 'h2', 244 | }, 245 | { 246 | _key: 'a2c1052ca675', 247 | _type: 'block', 248 | children: [ 249 | { 250 | _type: 'span', 251 | marks: [], 252 | text: 'Lorem ipsum', 253 | }, 254 | { 255 | _type: 'span', 256 | marks: ['abaab54abef7'], 257 | text: 'Lorem ipsum', 258 | }, 259 | { 260 | _type: 'span', 261 | marks: [], 262 | text: 'Lorem ipsum', 263 | }, 264 | { 265 | _type: 'span', 266 | marks: ['36e7c773d148'], 267 | text: 'Lorem ipsum', 268 | }, 269 | { 270 | _type: 'span', 271 | marks: [], 272 | text: 'Lorem ipsum', 273 | }, 274 | { 275 | _type: 'span', 276 | marks: ['4352c44c3077'], 277 | text: 'Lorem ipsum', 278 | }, 279 | { 280 | _type: 'span', 281 | marks: [], 282 | text: 'Lorem ipsum', 283 | }, 284 | ], 285 | markDefs: [ 286 | { 287 | _key: 'abaab54abef7', 288 | _type: 'link', 289 | href: 'https://example.com', 290 | }, 291 | { 292 | _key: '36e7c773d148', 293 | _type: 'link', 294 | href: 'https://example.com', 295 | }, 296 | { 297 | _key: '4352c44c3077', 298 | _type: 'link', 299 | href: 'https://example.com', 300 | }, 301 | ], 302 | style: 'normal', 303 | }, 304 | { 305 | _key: '008e004a87e3', 306 | _type: 'block', 307 | children: [ 308 | { 309 | _type: 'span', 310 | marks: [], 311 | text: 'Lorem ipsum', 312 | }, 313 | ], 314 | markDefs: [], 315 | style: 'h3', 316 | }, 317 | { 318 | _key: '383dddd69bef', 319 | _type: 'block', 320 | children: [ 321 | { 322 | _type: 'span', 323 | marks: [], 324 | text: 'Lorem ipsum', 325 | }, 326 | ], 327 | level: 1, 328 | listItem: 'bullet', 329 | markDefs: [], 330 | style: 'normal', 331 | }, 332 | { 333 | _key: 'ea36cba89a66', 334 | _type: 'block', 335 | children: [ 336 | { 337 | _type: 'span', 338 | marks: [], 339 | text: 'Lorem ipsum', 340 | }, 341 | ], 342 | level: 1, 343 | listItem: 'bullet', 344 | markDefs: [], 345 | style: 'normal', 346 | }, 347 | { 348 | _key: '57f05ea5c2bb', 349 | _type: 'block', 350 | children: [ 351 | { 352 | _type: 'span', 353 | marks: [], 354 | text: 'Lorem ipsum', 355 | }, 356 | ], 357 | level: 1, 358 | listItem: 'bullet', 359 | markDefs: [], 360 | style: 'normal', 361 | }, 362 | { 363 | _key: 'd5df37eee363', 364 | _type: 'block', 365 | children: [ 366 | { 367 | _type: 'span', 368 | marks: [], 369 | text: 'Lorem ipsum', 370 | }, 371 | ], 372 | markDefs: [], 373 | style: 'h3', 374 | }, 375 | { 376 | _key: '61231e9bb2f4', 377 | _type: 'block', 378 | children: [ 379 | { 380 | _type: 'span', 381 | marks: [], 382 | text: 'Lorem ipsum', 383 | }, 384 | ], 385 | level: 1, 386 | listItem: 'bullet', 387 | markDefs: [], 388 | style: 'normal', 389 | }, 390 | { 391 | _key: 'e1091120de4d', 392 | _type: 'block', 393 | children: [ 394 | { 395 | _type: 'span', 396 | marks: [], 397 | text: 'Lorem ipsum', 398 | }, 399 | ], 400 | level: 1, 401 | listItem: 'bullet', 402 | markDefs: [], 403 | style: 'normal', 404 | }, 405 | { 406 | _key: 'be53f0b95e8b', 407 | _type: 'block', 408 | children: [ 409 | { 410 | _type: 'span', 411 | marks: [], 412 | text: 'Lorem ipsum', 413 | }, 414 | ], 415 | level: 1, 416 | listItem: 'bullet', 417 | markDefs: [], 418 | style: 'normal', 419 | }, 420 | { 421 | _key: 'e6538941fddf', 422 | _type: 'block', 423 | children: [ 424 | { 425 | _type: 'span', 426 | marks: [], 427 | text: 'Lorem ipsum', 428 | }, 429 | ], 430 | level: 1, 431 | listItem: 'bullet', 432 | markDefs: [], 433 | style: 'normal', 434 | }, 435 | { 436 | _key: 'a852b3d1518a', 437 | _type: 'block', 438 | children: [ 439 | { 440 | _type: 'span', 441 | marks: [], 442 | text: 'Lorem ipsum', 443 | }, 444 | ], 445 | level: 1, 446 | listItem: 'bullet', 447 | markDefs: [], 448 | style: 'normal', 449 | }, 450 | { 451 | _key: 'd77890703306', 452 | _type: 'block', 453 | children: [ 454 | { 455 | _type: 'span', 456 | marks: [], 457 | text: 'Lorem ipsum', 458 | }, 459 | ], 460 | level: 1, 461 | listItem: 'bullet', 462 | markDefs: [], 463 | style: 'normal', 464 | }, 465 | { 466 | _key: 'd061261ee1d2', 467 | _type: 'block', 468 | children: [ 469 | { 470 | _type: 'span', 471 | marks: [], 472 | text: 'Lorem ipsum', 473 | }, 474 | ], 475 | markDefs: [], 476 | style: 'h3', 477 | }, 478 | { 479 | _key: '248cc45717e0', 480 | _type: 'block', 481 | children: [ 482 | { 483 | _type: 'span', 484 | marks: [], 485 | text: 'Lorem ipsum', 486 | }, 487 | ], 488 | level: 1, 489 | listItem: 'bullet', 490 | markDefs: [], 491 | style: 'normal', 492 | }, 493 | { 494 | _key: '09f2ab44df66', 495 | _type: 'block', 496 | children: [ 497 | { 498 | _type: 'span', 499 | marks: [], 500 | text: 'Lorem ipsum', 501 | }, 502 | ], 503 | level: 1, 504 | listItem: 'bullet', 505 | markDefs: [], 506 | style: 'normal', 507 | }, 508 | { 509 | _key: 'ba7b45509071', 510 | _type: 'block', 511 | children: [ 512 | { 513 | _type: 'span', 514 | marks: [], 515 | text: 'Lorem ipsum', 516 | }, 517 | { 518 | _type: 'span', 519 | marks: ['em'], 520 | text: 'Lorem ipsum', 521 | }, 522 | { 523 | _type: 'span', 524 | marks: [], 525 | text: 'Lorem ipsum', 526 | }, 527 | ], 528 | level: 1, 529 | listItem: 'bullet', 530 | markDefs: [], 531 | style: 'normal', 532 | }, 533 | { 534 | _key: '12c77502a595', 535 | _type: 'block', 536 | children: [ 537 | { 538 | _type: 'span', 539 | marks: [], 540 | text: 'Lorem ipsum', 541 | }, 542 | ], 543 | markDefs: [], 544 | style: 'h3', 545 | }, 546 | { 547 | _key: '078039a7af96', 548 | _type: 'block', 549 | children: [ 550 | { 551 | _type: 'span', 552 | marks: [], 553 | text: 'Lorem ipsum', 554 | }, 555 | ], 556 | level: 1, 557 | listItem: 'bullet', 558 | markDefs: [], 559 | style: 'normal', 560 | }, 561 | { 562 | _key: 'e2ea9480bfe5', 563 | _type: 'block', 564 | children: [ 565 | { 566 | _type: 'span', 567 | marks: [], 568 | text: 'Lorem ipsum', 569 | }, 570 | ], 571 | level: 1, 572 | listItem: 'bullet', 573 | markDefs: [], 574 | style: 'normal', 575 | }, 576 | { 577 | _key: 'fdc3bfe19845', 578 | _type: 'block', 579 | children: [ 580 | { 581 | _type: 'span', 582 | marks: [], 583 | text: 'Lorem ipsum', 584 | }, 585 | ], 586 | level: 1, 587 | listItem: 'bullet', 588 | markDefs: [], 589 | style: 'normal', 590 | }, 591 | { 592 | _key: '3201b3d02e0d', 593 | _type: 'block', 594 | children: [ 595 | { 596 | _type: 'span', 597 | marks: [], 598 | text: 'Lorem ipsum', 599 | }, 600 | ], 601 | level: 1, 602 | listItem: 'bullet', 603 | markDefs: [], 604 | style: 'normal', 605 | }, 606 | { 607 | _key: '5ef716ee0309', 608 | _type: 'block', 609 | children: [ 610 | { 611 | _type: 'span', 612 | marks: [], 613 | text: 'Lorem ipsum', 614 | }, 615 | ], 616 | markDefs: [], 617 | style: 'h3', 618 | }, 619 | { 620 | _key: '9a1430f39842', 621 | _type: 'block', 622 | children: [ 623 | { 624 | _type: 'span', 625 | marks: [], 626 | text: 'Lorem ipsum', 627 | }, 628 | ], 629 | level: 1, 630 | listItem: 'bullet', 631 | markDefs: [], 632 | style: 'normal', 633 | }, 634 | { 635 | _key: '5fa8c1cd9d66', 636 | _type: 'block', 637 | children: [ 638 | { 639 | _type: 'span', 640 | marks: [], 641 | text: 'Lorem ipsum', 642 | }, 643 | ], 644 | level: 1, 645 | listItem: 'bullet', 646 | markDefs: [], 647 | style: 'normal', 648 | }, 649 | { 650 | _key: '29240861e0c7', 651 | _type: 'block', 652 | children: [ 653 | { 654 | _type: 'span', 655 | marks: [], 656 | text: 'Lorem ipsum', 657 | }, 658 | ], 659 | markDefs: [], 660 | style: 'h3', 661 | }, 662 | { 663 | _key: '471105eb4eb6', 664 | _type: 'block', 665 | children: [ 666 | { 667 | _type: 'span', 668 | marks: [], 669 | text: 'Lorem ipsum', 670 | }, 671 | ], 672 | level: 1, 673 | listItem: 'bullet', 674 | markDefs: [], 675 | style: 'normal', 676 | }, 677 | { 678 | _key: '2a1754271e84', 679 | _type: 'block', 680 | children: [ 681 | { 682 | _type: 'span', 683 | marks: [], 684 | text: 'Lorem ipsum', 685 | }, 686 | ], 687 | level: 1, 688 | listItem: 'bullet', 689 | markDefs: [], 690 | style: 'normal', 691 | }, 692 | { 693 | _key: 'c820d890f8c7', 694 | _type: 'block', 695 | children: [ 696 | { 697 | _type: 'span', 698 | marks: [], 699 | text: 'Lorem ipsum', 700 | }, 701 | ], 702 | markDefs: [], 703 | style: 'h3', 704 | }, 705 | { 706 | _key: 'b58650f53e9e', 707 | _type: 'block', 708 | children: [ 709 | { 710 | _type: 'span', 711 | marks: [], 712 | text: 'Lorem ipsum', 713 | }, 714 | ], 715 | level: 1, 716 | listItem: 'bullet', 717 | markDefs: [], 718 | style: 'normal', 719 | }, 720 | { 721 | _key: '0ca5f3fb129e', 722 | _type: 'block', 723 | children: [ 724 | { 725 | _type: 'span', 726 | marks: [], 727 | text: 'Lorem ipsum', 728 | }, 729 | ], 730 | level: 1, 731 | listItem: 'bullet', 732 | markDefs: [], 733 | style: 'normal', 734 | }, 735 | { 736 | _key: 'f68449f61111', 737 | _type: 'block', 738 | children: [ 739 | { 740 | _type: 'span', 741 | marks: [], 742 | text: 'Lorem ipsum', 743 | }, 744 | ], 745 | level: 2, 746 | listItem: 'bullet', 747 | markDefs: [], 748 | style: 'normal', 749 | }, 750 | { 751 | _key: '5433045c560a', 752 | _type: 'block', 753 | children: [ 754 | { 755 | _type: 'span', 756 | marks: [], 757 | text: 'Lorem ipsum', 758 | }, 759 | ], 760 | level: 2, 761 | listItem: 'bullet', 762 | markDefs: [], 763 | style: 'normal', 764 | }, 765 | { 766 | _key: '3d85b3b16d79', 767 | _type: 'block', 768 | children: [ 769 | { 770 | _type: 'span', 771 | marks: [], 772 | text: 'Lorem ipsum', 773 | }, 774 | ], 775 | level: 2, 776 | listItem: 'bullet', 777 | markDefs: [], 778 | style: 'normal', 779 | }, 780 | { 781 | _key: '03421acc9f6d', 782 | _type: 'block', 783 | children: [ 784 | { 785 | _type: 'span', 786 | marks: [], 787 | text: 'Lorem ipsum', 788 | }, 789 | ], 790 | level: 2, 791 | listItem: 'bullet', 792 | markDefs: [], 793 | style: 'normal', 794 | }, 795 | { 796 | _key: '3a94842ddd74', 797 | _type: 'block', 798 | children: [ 799 | { 800 | _type: 'span', 801 | marks: [], 802 | text: 'Lorem ipsum', 803 | }, 804 | ], 805 | level: 1, 806 | listItem: 'bullet', 807 | markDefs: [], 808 | style: 'normal', 809 | }, 810 | { 811 | _key: '4e3558037479', 812 | _type: 'block', 813 | children: [ 814 | { 815 | _type: 'span', 816 | marks: [], 817 | text: 'Lorem ipsum', 818 | }, 819 | ], 820 | level: 2, 821 | listItem: 'bullet', 822 | markDefs: [], 823 | style: 'normal', 824 | }, 825 | { 826 | _key: '2cf4b5ddec6f', 827 | _type: 'block', 828 | children: [ 829 | { 830 | _type: 'span', 831 | marks: [], 832 | text: 'Lorem ipsum', 833 | }, 834 | ], 835 | level: 2, 836 | listItem: 'bullet', 837 | markDefs: [], 838 | style: 'normal', 839 | }, 840 | { 841 | _key: '93051319ac3e', 842 | _type: 'block', 843 | children: [ 844 | { 845 | _type: 'span', 846 | marks: [], 847 | text: 'Lorem ipsum', 848 | }, 849 | ], 850 | level: 2, 851 | listItem: 'bullet', 852 | markDefs: [], 853 | style: 'normal', 854 | }, 855 | { 856 | _key: '252749bb01d5', 857 | _type: 'block', 858 | children: [ 859 | { 860 | _type: 'span', 861 | marks: [], 862 | text: 'Lorem ipsum', 863 | }, 864 | ], 865 | level: 2, 866 | listItem: 'bullet', 867 | markDefs: [], 868 | style: 'normal', 869 | }, 870 | { 871 | _key: 'd32eb8106d08', 872 | _type: 'block', 873 | children: [ 874 | { 875 | _type: 'span', 876 | marks: [], 877 | text: 'Lorem ipsum', 878 | }, 879 | ], 880 | level: 2, 881 | listItem: 'bullet', 882 | markDefs: [], 883 | style: 'normal', 884 | }, 885 | { 886 | _key: 'dbdbc5839fb6', 887 | _type: 'block', 888 | children: [ 889 | { 890 | _type: 'span', 891 | marks: [], 892 | text: 'Lorem ipsum', 893 | }, 894 | ], 895 | level: 2, 896 | listItem: 'bullet', 897 | markDefs: [], 898 | style: 'normal', 899 | }, 900 | { 901 | _key: 'f673698e2e27', 902 | _type: 'block', 903 | children: [ 904 | { 905 | _type: 'span', 906 | marks: [], 907 | text: 'Lorem ipsum', 908 | }, 909 | ], 910 | level: 2, 911 | listItem: 'bullet', 912 | markDefs: [], 913 | style: 'normal', 914 | }, 915 | { 916 | _key: '2638df8609e7', 917 | _type: 'block', 918 | children: [ 919 | { 920 | _type: 'span', 921 | marks: [], 922 | text: 'Lorem ipsum', 923 | }, 924 | ], 925 | markDefs: [], 926 | style: 'h2', 927 | }, 928 | { 929 | _key: '8bd25d26c0ab', 930 | _type: 'block', 931 | children: [ 932 | { 933 | _type: 'span', 934 | marks: [], 935 | text: 'Lorem ipsum', 936 | }, 937 | ], 938 | markDefs: [], 939 | style: 'normal', 940 | }, 941 | { 942 | _key: '58fc3993c18c', 943 | _type: 'block', 944 | children: [ 945 | { 946 | _type: 'span', 947 | marks: [], 948 | text: 'Lorem ipsum', 949 | }, 950 | { 951 | _type: 'span', 952 | marks: ['em'], 953 | text: 'Lorem ipsum', 954 | }, 955 | ], 956 | markDefs: [], 957 | style: 'normal', 958 | }, 959 | { 960 | _key: '7845e645190f', 961 | _type: 'block', 962 | children: [ 963 | { 964 | _type: 'span', 965 | marks: [], 966 | text: 'Lorem ipsum', 967 | }, 968 | ], 969 | markDefs: [], 970 | style: 'h2', 971 | }, 972 | { 973 | _key: '26e1555ec20c', 974 | _type: 'block', 975 | children: [ 976 | { 977 | _type: 'span', 978 | marks: [], 979 | text: 'Lorem ipsum', 980 | }, 981 | ], 982 | markDefs: [], 983 | style: 'normal', 984 | }, 985 | { 986 | _key: 'e90b29141e56', 987 | _type: 'block', 988 | children: [ 989 | { 990 | _type: 'span', 991 | marks: [], 992 | text: 'Lorem ipsum', 993 | }, 994 | ], 995 | markDefs: [], 996 | style: 'normal', 997 | }, 998 | { 999 | _key: '7f9ac906a4bd', 1000 | _type: 'block', 1001 | children: [ 1002 | { 1003 | _type: 'span', 1004 | marks: [], 1005 | text: 'Lorem ipsum', 1006 | }, 1007 | ], 1008 | markDefs: [], 1009 | style: 'h2', 1010 | }, 1011 | { 1012 | _key: '9259af58c8be', 1013 | _type: 'block', 1014 | children: [ 1015 | { 1016 | _type: 'span', 1017 | marks: [], 1018 | text: 'Lorem ipsum', 1019 | }, 1020 | ], 1021 | markDefs: [], 1022 | style: 'normal', 1023 | }, 1024 | { 1025 | _key: 'd3343fe575d4', 1026 | _type: 'block', 1027 | children: [ 1028 | { 1029 | _type: 'span', 1030 | marks: [], 1031 | text: 'Lorem ipsum', 1032 | }, 1033 | ], 1034 | level: 1, 1035 | listItem: 'number', 1036 | markDefs: [], 1037 | style: 'normal', 1038 | }, 1039 | { 1040 | _key: '14c57fd646e8', 1041 | _type: 'block', 1042 | children: [ 1043 | { 1044 | _type: 'span', 1045 | marks: [], 1046 | text: 'Lorem ipsum', 1047 | }, 1048 | ], 1049 | level: 1, 1050 | listItem: 'number', 1051 | markDefs: [], 1052 | style: 'normal', 1053 | }, 1054 | { 1055 | _key: 'c8e8905dfe9e', 1056 | _type: 'block', 1057 | children: [ 1058 | { 1059 | _type: 'span', 1060 | marks: [], 1061 | text: 'Lorem ipsum', 1062 | }, 1063 | ], 1064 | level: 1, 1065 | listItem: 'number', 1066 | markDefs: [], 1067 | style: 'normal', 1068 | }, 1069 | { 1070 | _key: '69c4fe9fa4ed', 1071 | _type: 'block', 1072 | children: [ 1073 | { 1074 | _type: 'span', 1075 | marks: [], 1076 | text: 'Lorem ipsum', 1077 | }, 1078 | ], 1079 | level: 1, 1080 | listItem: 'number', 1081 | markDefs: [], 1082 | style: 'normal', 1083 | }, 1084 | { 1085 | _key: 'ae19d6d44753', 1086 | _type: 'block', 1087 | children: [ 1088 | { 1089 | _type: 'span', 1090 | marks: [], 1091 | text: 'Lorem ipsum', 1092 | }, 1093 | ], 1094 | level: 1, 1095 | listItem: 'number', 1096 | markDefs: [], 1097 | style: 'normal', 1098 | }, 1099 | { 1100 | _key: '1136f698594f', 1101 | _type: 'block', 1102 | children: [ 1103 | { 1104 | _type: 'span', 1105 | marks: [], 1106 | text: 'Lorem ipsum', 1107 | }, 1108 | ], 1109 | markDefs: [], 1110 | style: 'normal', 1111 | }, 1112 | { 1113 | _key: 'd94cdd676b75', 1114 | _type: 'block', 1115 | children: [ 1116 | { 1117 | _type: 'span', 1118 | marks: [], 1119 | text: 'Lorem ipsum', 1120 | }, 1121 | ], 1122 | markDefs: [], 1123 | style: 'h2', 1124 | }, 1125 | { 1126 | _key: '660d22bd8f2a', 1127 | _type: 'block', 1128 | children: [ 1129 | { 1130 | _type: 'span', 1131 | marks: [], 1132 | text: 'Lorem ipsum', 1133 | }, 1134 | ], 1135 | markDefs: [], 1136 | style: 'normal', 1137 | }, 1138 | { 1139 | _key: '55c6814da883', 1140 | _type: 'block', 1141 | children: [ 1142 | { 1143 | _type: 'span', 1144 | marks: [], 1145 | text: 'Lorem ipsum', 1146 | }, 1147 | { 1148 | _type: 'span', 1149 | marks: ['96b9a7384bb9'], 1150 | text: 'Lorem ipsum', 1151 | }, 1152 | { 1153 | _type: 'span', 1154 | marks: [], 1155 | text: 'Lorem ipsum', 1156 | }, 1157 | ], 1158 | level: 1, 1159 | listItem: 'bullet', 1160 | markDefs: [ 1161 | { 1162 | _key: '96b9a7384bb9', 1163 | _type: 'link', 1164 | href: 'https://example.com', 1165 | }, 1166 | ], 1167 | style: 'normal', 1168 | }, 1169 | { 1170 | _key: '2baca0a20bca', 1171 | _type: 'block', 1172 | children: [ 1173 | { 1174 | _type: 'span', 1175 | marks: [], 1176 | text: 'Lorem ipsum', 1177 | }, 1178 | { 1179 | _type: 'span', 1180 | marks: ['99d77e03056c'], 1181 | text: 'Lorem ipsum', 1182 | }, 1183 | { 1184 | _type: 'span', 1185 | marks: [], 1186 | text: 'Lorem ipsum', 1187 | }, 1188 | ], 1189 | level: 1, 1190 | listItem: 'bullet', 1191 | markDefs: [ 1192 | { 1193 | _key: '99d77e03056c', 1194 | _type: 'link', 1195 | href: 'https://example.com', 1196 | }, 1197 | ], 1198 | style: 'normal', 1199 | }, 1200 | { 1201 | _key: '512d2c9cc40d', 1202 | _type: 'block', 1203 | children: [ 1204 | { 1205 | _type: 'span', 1206 | marks: [], 1207 | text: 'Lorem ipsum', 1208 | }, 1209 | { 1210 | _type: 'span', 1211 | marks: ['a81f3f515e3e'], 1212 | text: 'Lorem ipsum', 1213 | }, 1214 | { 1215 | _type: 'span', 1216 | marks: [], 1217 | text: 'Lorem ipsum', 1218 | }, 1219 | ], 1220 | level: 1, 1221 | listItem: 'bullet', 1222 | markDefs: [ 1223 | { 1224 | _key: 'a81f3f515e3e', 1225 | _type: 'link', 1226 | href: 'https://example.com', 1227 | }, 1228 | ], 1229 | style: 'normal', 1230 | }, 1231 | { 1232 | _key: '5e68d8b50d64', 1233 | _type: 'block', 1234 | children: [ 1235 | { 1236 | _type: 'span', 1237 | marks: [], 1238 | text: 'Lorem ipsum', 1239 | }, 1240 | { 1241 | _type: 'span', 1242 | marks: ['aedfb56c1761'], 1243 | text: 'Lorem ipsum', 1244 | }, 1245 | { 1246 | _type: 'span', 1247 | marks: [], 1248 | text: 'Lorem ipsum', 1249 | }, 1250 | ], 1251 | level: 1, 1252 | listItem: 'bullet', 1253 | markDefs: [ 1254 | { 1255 | _key: 'aedfb56c1761', 1256 | _type: 'link', 1257 | href: 'https://example.com', 1258 | }, 1259 | ], 1260 | style: 'normal', 1261 | }, 1262 | { 1263 | _key: '8d339b91184a', 1264 | _type: 'block', 1265 | children: [ 1266 | { 1267 | _type: 'span', 1268 | marks: [], 1269 | text: 'Lorem ipsum', 1270 | }, 1271 | { 1272 | _type: 'span', 1273 | marks: ['beec3b2a0459'], 1274 | text: 'Lorem ipsum', 1275 | }, 1276 | { 1277 | _type: 'span', 1278 | marks: [], 1279 | text: 'Lorem ipsum', 1280 | }, 1281 | ], 1282 | level: 1, 1283 | listItem: 'bullet', 1284 | markDefs: [ 1285 | { 1286 | _key: 'beec3b2a0459', 1287 | _type: 'link', 1288 | href: 'https://example.com', 1289 | }, 1290 | ], 1291 | style: 'normal', 1292 | }, 1293 | { 1294 | _key: '09d48934ea88', 1295 | _type: 'block', 1296 | children: [ 1297 | { 1298 | _type: 'span', 1299 | marks: [], 1300 | text: 'Lorem ipsum', 1301 | }, 1302 | { 1303 | _type: 'span', 1304 | marks: ['30559cd94434'], 1305 | text: 'Lorem ipsum', 1306 | }, 1307 | { 1308 | _type: 'span', 1309 | marks: [], 1310 | text: 'Lorem ipsum', 1311 | }, 1312 | ], 1313 | level: 1, 1314 | listItem: 'bullet', 1315 | markDefs: [ 1316 | { 1317 | _key: '30559cd94434', 1318 | _type: 'link', 1319 | href: 'https://example.com', 1320 | }, 1321 | ], 1322 | style: 'normal', 1323 | }, 1324 | { 1325 | _key: '851a44421210', 1326 | _type: 'block', 1327 | children: [ 1328 | { 1329 | _type: 'span', 1330 | marks: [], 1331 | text: 'Lorem ipsum', 1332 | }, 1333 | { 1334 | _type: 'span', 1335 | marks: ['cf109fae377a'], 1336 | text: 'Lorem ipsum', 1337 | }, 1338 | { 1339 | _type: 'span', 1340 | marks: [], 1341 | text: 'Lorem ipsum', 1342 | }, 1343 | ], 1344 | level: 1, 1345 | listItem: 'bullet', 1346 | markDefs: [ 1347 | { 1348 | _key: 'cf109fae377a', 1349 | _type: 'link', 1350 | href: 'https://example.com', 1351 | }, 1352 | ], 1353 | style: 'normal', 1354 | }, 1355 | { 1356 | _key: 'b584b7aee2be', 1357 | _type: 'block', 1358 | children: [ 1359 | { 1360 | _type: 'span', 1361 | marks: [], 1362 | text: 'Lorem ipsum', 1363 | }, 1364 | ], 1365 | markDefs: [], 1366 | style: 'h2', 1367 | }, 1368 | { 1369 | _key: '23e9756111da', 1370 | _type: 'block', 1371 | children: [ 1372 | { 1373 | _type: 'span', 1374 | marks: [], 1375 | text: 'Lorem ipsum', 1376 | }, 1377 | ], 1378 | markDefs: [], 1379 | style: 'normal', 1380 | }, 1381 | ] 1382 | 1383 | export default { 1384 | input, 1385 | } 1386 | -------------------------------------------------------------------------------- /test/fixtures/061-missing-mark-component.ts: -------------------------------------------------------------------------------- 1 | import type {PortableTextBlock} from '@portabletext/types' 2 | 3 | const input: PortableTextBlock = { 4 | _type: 'block', 5 | children: [ 6 | { 7 | _key: 'cZUQGmh4', 8 | _type: 'span', 9 | marks: ['abc'], 10 | text: 'A word of ', 11 | }, 12 | { 13 | _key: 'toaiCqIK', 14 | _type: 'span', 15 | marks: ['abc', 'em'], 16 | text: 'warning;', 17 | }, 18 | { 19 | _key: 'gaZingA', 20 | _type: 'span', 21 | marks: [], 22 | text: ' Sanity is addictive.', 23 | }, 24 | ], 25 | markDefs: [], 26 | } 27 | 28 | export default { 29 | input, 30 | output: 31 | '

A word of warning; Sanity is addictive.

', 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | import emptyBlock from './001-empty-block' 2 | import singleSpan from './002-single-span' 3 | import multipleSpans from './003-multiple-spans' 4 | import basicMarkSingleSpan from './004-basic-mark-single-span' 5 | import basicMarkMultipleAdjacentSpans from './005-basic-mark-multiple-adjacent-spans' 6 | import basicMarkNestedMarks from './006-basic-mark-nested-marks' 7 | import linkMarkDef from './007-link-mark-def' 8 | import plainHeaderBlock from './008-plain-header-block' 9 | import messyLinkText from './009-messy-link-text' 10 | import basicBulletList from './010-basic-bullet-list' 11 | import basicNumberedList from './011-basic-numbered-list' 12 | import imageSupport from './012-image-support' 13 | import materializedImageSupport from './013-materialized-image-support' 14 | import nestedLists from './014-nested-lists' 15 | import allBasicMarks from './015-all-basic-marks' 16 | import deepWeirdLists from './016-deep-weird-lists' 17 | import allDefaultBlockStyles from './017-all-default-block-styles' 18 | import marksAllTheWayDown from './018-marks-all-the-way-down' 19 | import keyless from './019-keyless' 20 | import emptyArray from './020-empty-array' 21 | import listWithoutLevel from './021-list-without-level' 22 | import inlineNodes from './022-inline-nodes' 23 | import hardBreaks from './023-hard-breaks' 24 | import inlineImages from './024-inline-images' 25 | import imageWithHotspot from './025-image-with-hotspot' 26 | import inlineBlockWithText from './026-inline-block-with-text' 27 | import styledListItems from './027-styled-list-items' 28 | import customListItemType from './028-custom-list-item-type' 29 | import customBlockType from './050-custom-block-type' 30 | import overrideDefaults from './051-override-defaults' 31 | import customMarks from './052-custom-marks' 32 | import overrideDefaultMarks from './053-override-default-marks' 33 | import listIssue from './060-list-issue' 34 | import missingMarkComponent from './061-missing-mark-component' 35 | 36 | export { 37 | allBasicMarks, 38 | allDefaultBlockStyles, 39 | basicBulletList, 40 | basicMarkMultipleAdjacentSpans, 41 | basicMarkNestedMarks, 42 | basicMarkSingleSpan, 43 | basicNumberedList, 44 | customBlockType, 45 | customListItemType, 46 | customMarks, 47 | deepWeirdLists, 48 | emptyArray, 49 | emptyBlock, 50 | hardBreaks, 51 | imageSupport, 52 | imageWithHotspot, 53 | inlineBlockWithText, 54 | inlineImages, 55 | inlineNodes, 56 | keyless, 57 | linkMarkDef, 58 | listIssue, 59 | listWithoutLevel, 60 | marksAllTheWayDown, 61 | materializedImageSupport, 62 | messyLinkText, 63 | missingMarkComponent, 64 | multipleSpans, 65 | nestedLists, 66 | overrideDefaultMarks, 67 | overrideDefaults, 68 | plainHeaderBlock, 69 | singleSpan, 70 | styledListItems, 71 | } 72 | -------------------------------------------------------------------------------- /test/runthrough.test.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 2 | import React from 'react' 3 | import {Text, View} from 'react-native' 4 | import renderer from 'react-test-renderer' 5 | import {expect, test} from 'vitest' 6 | 7 | import {PortableText, PortableTextReactComponents} from '../src' 8 | import * as fixtures from './fixtures' 9 | 10 | test('never mutates input', () => { 11 | for (const [key, fixture] of Object.entries(fixtures)) { 12 | if (key === 'default') { 13 | continue 14 | } 15 | 16 | const components: Partial = { 17 | marks: {highlight: () => }, 18 | unknownMark: ({children}) => {children}, 19 | unknownType: ({children}) => {children}, 20 | } 21 | const originalInput = JSON.parse(JSON.stringify(fixture.input)) 22 | const passedInput = fixture.input 23 | const tree = renderer 24 | .create() 25 | .toJSON() 26 | 27 | expect(tree).toMatchSnapshot(key) 28 | 29 | // Should not mutate the input 30 | expect(originalInput).toMatchObject(passedInput) 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "isolatedModules": true, 8 | "lib": ["ES2017"], 9 | "target": "esnext", 10 | "moduleResolution": "node", 11 | "jsx": "react-native" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path' 2 | import {defineConfig} from 'vite' 3 | import dts from 'vite-plugin-dts' 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: resolve(__dirname, 'src/index.ts'), 9 | formats: ['es'], 10 | name: 'react-native-portable-text', 11 | fileName: (format) => `react-native-portable-text.${format}.js`, 12 | }, 13 | rollupOptions: { 14 | external: ['@portabletext/react', '@portabletext/types', 'react', 'react-native'], 15 | output: { 16 | // Since we publish our ./src folder, there's no point in bloating sourcemaps with another copy of it. 17 | sourcemapExcludeSources: true, 18 | }, 19 | }, 20 | sourcemap: true, 21 | }, 22 | plugins: [dts()], 23 | }) 24 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | // this is needed for react jsx support 2 | import react from '@vitejs/plugin-react' 3 | import {defineConfig} from 'vitest/config' 4 | import reactNative from 'vitest-react-native' 5 | 6 | export default defineConfig({ 7 | plugins: [reactNative(), react()], 8 | }) 9 | --------------------------------------------------------------------------------