├── .husky
└── pre-commit
├── README.md
├── apps
├── playground
│ ├── src
│ │ ├── styles
│ │ │ └── globals.css
│ │ ├── vite-env.d.ts
│ │ ├── index.css
│ │ ├── wait.ts
│ │ ├── editor-id-generator.ts
│ │ ├── key-generator.ts
│ │ ├── primitives
│ │ │ ├── spinner.tsx
│ │ │ ├── icon.tsx
│ │ │ ├── group.tsx
│ │ │ ├── toolbar.tsx
│ │ │ ├── field.text.tsx
│ │ │ ├── field.number.tsx
│ │ │ ├── toggle-button.tsx
│ │ │ ├── utils.ts
│ │ │ ├── separator.tsx
│ │ │ ├── container.tsx
│ │ │ └── field.select.tsx
│ │ ├── main.tsx
│ │ ├── plugins
│ │ │ ├── looks-like-url.ts
│ │ │ ├── looks-like-url.test.ts
│ │ │ ├── plugin.link.tsx
│ │ │ └── read-files.ts
│ │ ├── App.tsx
│ │ └── toolbar
│ │ │ ├── button-tooltip.tsx
│ │ │ ├── keyboard-shortcut-preview.tsx
│ │ │ └── button.focus.tsx
│ ├── biome.json
│ ├── tailwind.config.js
│ ├── README.md
│ ├── tsconfig.json
│ ├── .gitignore
│ ├── tsconfig.node.json
│ ├── index.html
│ ├── eslint.config.js
│ └── tsconfig.app.json
└── docs
│ ├── src
│ ├── env.d.ts
│ ├── assets
│ │ ├── houston.webp
│ │ ├── emoji-picker.png
│ │ ├── portable-text-logo.png
│ │ └── logo.svg
│ ├── components
│ │ ├── editor
│ │ │ ├── editor.astro
│ │ │ └── defaultSchema.ts
│ │ ├── EventTypesList.astro
│ │ └── ui
│ │ │ ├── textarea.tsx
│ │ │ └── separator.tsx
│ └── content
│ │ ├── config.ts
│ │ └── docs
│ │ └── reference
│ │ ├── toolbar.mdx
│ │ ├── keyboard-shortcuts.mdx
│ │ └── editor.mdx
│ ├── tsconfig.json
│ ├── .gitignore
│ └── components.json
├── packages
├── racejar
│ ├── .gitignore
│ ├── src
│ │ ├── jest
│ │ │ └── index.ts
│ │ ├── vitest
│ │ │ └── index.ts
│ │ ├── playwright
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── create-parameter-type.ts
│ │ └── hooks.ts
│ ├── tsconfig.json
│ ├── package.config.ts
│ ├── tsconfig.settings.json
│ ├── tsconfig.dist.json
│ └── vitest.config.ts
├── editor
│ ├── src
│ │ ├── test
│ │ │ ├── _exports
│ │ │ │ └── index.ts
│ │ │ ├── vitest
│ │ │ │ ├── _exports
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── step-context.ts
│ │ │ └── index.ts
│ │ ├── utils
│ │ │ ├── _exports
│ │ │ │ └── index.ts
│ │ │ ├── util.get-text-block-text.ts
│ │ │ ├── util.is-keyed-segment.ts
│ │ │ ├── util.is-equal-selection-points.ts
│ │ │ ├── util.is-selection-expanded.ts
│ │ │ ├── asserters.ts
│ │ │ ├── util.is-selection-collapsed.ts
│ │ │ ├── util.is-equal-selections.ts
│ │ │ ├── util.is-empty-text-block.ts
│ │ │ ├── util.reverse-selection.ts
│ │ │ ├── util.selection-point.ts
│ │ │ ├── util.get-selection-end-point.ts
│ │ │ ├── util.get-selection-start-point.ts
│ │ │ ├── util.get-block-start-point.ts
│ │ │ ├── util.block-offset-to-block-selection-point.ts
│ │ │ ├── key-generator.ts
│ │ │ └── util.at-the-beginning-of-block.ts
│ │ ├── behaviors
│ │ │ ├── _exports
│ │ │ │ └── index.ts
│ │ │ ├── behavior.config.ts
│ │ │ ├── behavior.types.guard.ts
│ │ │ └── index.ts
│ │ ├── plugins
│ │ │ ├── _exports
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── plugin.editor-ref.tsx
│ │ │ ├── plugin.internal.change-ref.tsx
│ │ │ ├── plugin.internal.slate-editor-ref.tsx
│ │ │ ├── plugin.behavior.tsx
│ │ │ └── plugin.internal.portable-text-editor-ref.tsx
│ │ ├── selectors
│ │ │ ├── _exports
│ │ │ │ └── index.ts
│ │ │ ├── selector.get-selection.ts
│ │ │ ├── selector.get-value.ts
│ │ │ ├── selector.is-selection-expanded.ts
│ │ │ ├── selector.is-active-style.ts
│ │ │ ├── selector.is-active-list-item.ts
│ │ │ ├── selector.get-active-annotation-marks.ts
│ │ │ ├── selector.get-first-block.ts
│ │ │ ├── selector.get-selection-end-point.ts
│ │ │ ├── selector.get-selection-start-point.ts
│ │ │ ├── selector.get-last-block.ts
│ │ │ ├── selector.is-selection-collapsed.ts
│ │ │ ├── selector.get-focus-span.ts
│ │ │ ├── selector.get-anchor-span.ts
│ │ │ ├── selector.get-focus-inline-object.ts
│ │ │ ├── selector.get-focus-block-object.ts
│ │ │ ├── selector.get-focus-text-block.ts
│ │ │ ├── selector.get-selected-spans.ts
│ │ │ ├── selector.get-anchor-text-block.ts
│ │ │ ├── selector.is-point-after-selection.ts
│ │ │ ├── selector.is-point-before-selection.ts
│ │ │ ├── selector.get-focus-list-block.ts
│ │ │ ├── selector.get-selection-text.ts
│ │ │ ├── selector.get-anchor-block.ts
│ │ │ ├── selector.get-focus-block.ts
│ │ │ ├── selector.is-active-decorator.ts
│ │ │ └── selector.get-selection-end-block.ts
│ │ ├── internal-utils
│ │ │ ├── mime-type.ts
│ │ │ ├── schema.ts
│ │ │ ├── block-keys.ts
│ │ │ ├── string-utils.ts
│ │ │ ├── debug.ts
│ │ │ ├── split-string.ts
│ │ │ ├── move-range-by-operation.ts
│ │ │ ├── selection-text.ts
│ │ │ ├── text-marks.ts
│ │ │ ├── selection-block-keys.ts
│ │ │ ├── string-overlap.test.ts
│ │ │ ├── create-placeholder-block.ts
│ │ │ ├── text-block-key.ts
│ │ │ ├── __tests__
│ │ │ │ └── ranges.test.ts
│ │ │ ├── compound-client-rect.ts
│ │ │ └── collapse-selection.ts
│ │ ├── editor
│ │ │ ├── editor-schema.ts
│ │ │ ├── relay-actor-context.ts
│ │ │ ├── editor-actor-context.ts
│ │ │ ├── editor-context.tsx
│ │ │ ├── slate-plugin.redoing.ts
│ │ │ ├── slate-plugin.undoing.ts
│ │ │ ├── without-normalizing-conditional.ts
│ │ │ ├── withoutPatching.ts
│ │ │ ├── with-normalizing-node.ts
│ │ │ ├── slate-plugin.without-history.ts
│ │ │ ├── withChanges.ts
│ │ │ ├── render.drop-indicator.tsx
│ │ │ ├── with-performing-behavior-operation.ts
│ │ │ ├── render.text.tsx
│ │ │ ├── render.default-object.tsx
│ │ │ ├── use-editor.ts
│ │ │ └── usePortableTextEditor.ts
│ │ ├── priority
│ │ │ ├── priority.core.ts
│ │ │ └── priority.types.ts
│ │ ├── types
│ │ │ ├── block-offset.ts
│ │ │ ├── options.ts
│ │ │ ├── sanity-types.ts
│ │ │ ├── block-with-optional-key.ts
│ │ │ └── slate.ts
│ │ ├── operations
│ │ │ ├── operation.insert.text.ts
│ │ │ ├── operation.move.forward.ts
│ │ │ ├── operation.move.backward.ts
│ │ │ └── operation.select.ts
│ │ ├── converters
│ │ │ └── converters.core.ts
│ │ └── type-utils.ts
│ ├── gherkin-tests
│ │ ├── global.d.ts
│ │ ├── delete.test.ts
│ │ ├── selection.test.ts
│ │ ├── insert.text.test.ts
│ │ ├── block-objects.test.ts
│ │ ├── insert.block.test.ts
│ │ ├── insert.blocks.test.ts
│ │ ├── insert.child.test.ts
│ │ ├── inline-objects.test.ts
│ │ ├── lists.test.ts
│ │ ├── paste.test.ts
│ │ ├── splitting-blocks.test.ts
│ │ ├── insert.break.test.ts
│ │ ├── selection-adjustment.test.ts
│ │ ├── annotations-collaboration.test.ts
│ │ └── decorators.test.ts
│ ├── tsconfig.dist.json
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── biome.json
│ ├── tsconfig.typedoc.json
│ ├── gherkin-spec
│ │ ├── insert.text.feature
│ │ ├── annotations-collaboration.feature
│ │ └── removing-blocks.feature
│ ├── eslint.config.js
│ ├── package.config.ts
│ └── tests
│ │ └── event.ready.test.tsx
├── block-tools
│ ├── src
│ │ ├── rules
│ │ │ ├── _exports
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── util
│ │ │ ├── findBlockType.ts
│ │ │ └── randomKey.ts
│ │ └── HtmlDeserializer
│ │ │ ├── preprocessors
│ │ │ ├── xpathResult.ts
│ │ │ ├── index.ts
│ │ │ └── preprocessor.notion.ts
│ │ │ └── rules
│ │ │ └── index.ts
│ ├── biome.json
│ ├── tsconfig.dist.json
│ ├── test
│ │ ├── tests
│ │ │ ├── HtmlDeserializer
│ │ │ │ ├── codeBlock
│ │ │ │ │ ├── input.html
│ │ │ │ │ └── output.json
│ │ │ │ ├── simple
│ │ │ │ │ ├── input.html
│ │ │ │ │ └── index.ts
│ │ │ │ ├── whitespace3
│ │ │ │ │ ├── input.html
│ │ │ │ │ ├── output.json
│ │ │ │ │ └── index.ts
│ │ │ │ ├── blockTags
│ │ │ │ │ ├── input.html
│ │ │ │ │ └── index.ts
│ │ │ │ ├── decorators
│ │ │ │ │ ├── input.html
│ │ │ │ │ └── index.ts
│ │ │ │ ├── annotations
│ │ │ │ │ ├── input.html
│ │ │ │ │ └── index.ts
│ │ │ │ ├── customRules
│ │ │ │ │ └── input.html
│ │ │ │ ├── complex
│ │ │ │ │ └── index.ts
│ │ │ │ ├── customSchema
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── input.html
│ │ │ │ ├── fromTheWild1
│ │ │ │ │ └── index.ts
│ │ │ │ ├── fromTheWild4
│ │ │ │ │ └── index.ts
│ │ │ │ ├── gdocsLists
│ │ │ │ │ └── index.ts
│ │ │ │ ├── githubIssue
│ │ │ │ │ └── index.ts
│ │ │ │ ├── whitespace2
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── input.html
│ │ │ │ ├── stegaUnicodeCleaner
│ │ │ │ │ └── index.ts
│ │ │ │ ├── whitespaceInPreTags
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── input.html
│ │ │ │ ├── types.ts
│ │ │ │ ├── gdocs
│ │ │ │ │ └── index.ts
│ │ │ │ ├── gdocsFirefox
│ │ │ │ │ └── index.ts
│ │ │ │ ├── gdocsWhitespaceRemove
│ │ │ │ │ └── index.ts
│ │ │ │ ├── gdocsStrikethroughLink
│ │ │ │ │ └── index.ts
│ │ │ │ └── gdocsWhitespaceRemoveFirefox
│ │ │ │ │ └── index.ts
│ │ │ └── util
│ │ │ │ └── randomKey.test.ts
│ │ ├── test-key-generator.ts
│ │ └── html-to-blocks
│ │ │ ├── nested-containers.html
│ │ │ ├── lists.html
│ │ │ ├── from-the-wild-5.html
│ │ │ └── lists.test.ts
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── vitest.config.ts
│ ├── package.config.ts
│ └── tsdoc.json
├── plugin-one-line
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ ├── tsconfig.dist.json
│ ├── package.config.ts
│ ├── tsconfig.settings.json
│ └── eslint.config.js
├── plugin-sdk-value
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ ├── tsconfig.dist.json
│ ├── package.config.ts
│ ├── tsconfig.settings.json
│ ├── vitest.config.ts
│ └── eslint.config.js
├── plugin-markdown-shortcuts
│ ├── src
│ │ ├── index.ts
│ │ └── global.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.dist.json
│ ├── package.config.ts
│ ├── tsconfig.settings.json
│ └── eslint.config.js
├── test
│ ├── src
│ │ ├── index.ts
│ │ └── test-key-generator.ts
│ ├── tsconfig.json
│ ├── tsconfig.settings.json
│ ├── package.config.ts
│ └── tsconfig.dist.json
├── plugin-character-pair-decorator
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ ├── tsconfig.dist.json
│ ├── package.config.ts
│ ├── tsconfig.settings.json
│ └── eslint.config.js
├── patches
│ ├── src
│ │ ├── index.ts
│ │ └── arrayInsert.ts
│ ├── tsconfig.json
│ ├── tsconfig.settings.json
│ ├── tsconfig.dist.json
│ ├── package.config.ts
│ └── vitest.config.ts
├── plugin-input-rule
│ ├── src
│ │ ├── global.d.ts
│ │ ├── index.ts
│ │ ├── rule.stock-ticker.feature
│ │ └── emoji-picker-rules.feature
│ ├── tsconfig.json
│ ├── tsconfig.settings.json
│ ├── package.config.ts
│ ├── tsconfig.dist.json
│ └── eslint.config.js
├── plugin-typography
│ ├── src
│ │ ├── global.d.ts
│ │ ├── index.ts
│ │ └── input-rule.multiplication.feature
│ ├── assets
│ │ ├── typography-demo.gif
│ │ └── smart-undo-with-backspace.gif
│ ├── tsconfig.json
│ ├── tsconfig.settings.json
│ ├── package.config.ts
│ ├── tsconfig.dist.json
│ └── eslint.config.js
├── plugin-emoji-picker
│ ├── src
│ │ ├── global.d.ts
│ │ ├── index.ts
│ │ └── match-emojis.ts
│ ├── tsconfig.json
│ ├── tsconfig.settings.json
│ ├── package.config.ts
│ ├── tsconfig.dist.json
│ └── eslint.config.js
├── markdown
│ ├── src
│ │ ├── from-portable-text
│ │ │ └── renderers
│ │ │ │ ├── hard-break.ts
│ │ │ │ ├── list-item.ts
│ │ │ │ └── block-spacing.ts
│ │ ├── escape.ts
│ │ └── key-generator.ts
│ ├── package.config.ts
│ ├── tsconfig.json
│ ├── tsconfig.settings.json
│ ├── tsconfig.dist.json
│ └── vitest.config.ts
├── keyboard-shortcuts
│ ├── src
│ │ ├── index.ts
│ │ └── is-apple.ts
│ ├── package.config.ts
│ ├── tsconfig.json
│ ├── tsconfig.settings.json
│ ├── tsconfig.dist.json
│ └── vitest.config.ts
├── schema
│ ├── tsconfig.json
│ ├── package.config.ts
│ ├── tsconfig.settings.json
│ ├── tsconfig.dist.json
│ ├── README.md
│ └── src
│ │ └── index.ts
├── sanity-bridge
│ ├── tsconfig.json
│ ├── package.config.ts
│ ├── tsconfig.settings.json
│ ├── tsconfig.dist.json
│ └── src
│ │ ├── index.ts
│ │ ├── start-case.ts
│ │ └── key-generator.ts
└── toolbar
│ ├── tsconfig.json
│ ├── tsconfig.settings.json
│ ├── package.config.ts
│ ├── tsconfig.dist.json
│ ├── src
│ ├── index.ts
│ └── disable-listener.ts
│ └── eslint.config.js
├── examples
├── basic
│ ├── src
│ │ ├── vite-env.d.ts
│ │ └── main.tsx
│ ├── tsconfig.json
│ ├── .gitignore
│ ├── index.html
│ ├── tsconfig.node.json
│ ├── tsconfig.app.json
│ ├── vite.config.ts
│ └── eslint.config.js
└── legacy
│ ├── src
│ ├── vite-env.d.ts
│ └── main.tsx
│ ├── tsconfig.json
│ ├── .gitignore
│ ├── index.html
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ ├── tsconfig.app.json
│ └── eslint.config.js
├── .gitignore
├── .changeset
├── twenty-candies-do.md
├── nine-schools-raise.md
└── config.json
├── .npmrc
├── .prettierignore
├── .prettierrc.mjs
├── .editorconfig
├── .github
└── workflows
│ ├── renovate.yml
│ ├── release.yml
│ ├── check-lint.yml
│ ├── check-types.yml
│ ├── check-format.yml
│ └── check-react-compiler.yml
├── AGENTS.md
└── pnpm-workspace.yaml
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm lint-staged
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | packages/editor/README.md
--------------------------------------------------------------------------------
/apps/playground/src/styles/globals.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/racejar/.gitignore:
--------------------------------------------------------------------------------
1 | test-results
2 |
--------------------------------------------------------------------------------
/packages/editor/src/test/_exports/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../index'
2 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/_exports/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../index'
2 |
--------------------------------------------------------------------------------
/apps/playground/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/basic/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/legacy/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/block-tools/src/rules/_exports/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../index'
2 |
--------------------------------------------------------------------------------
/packages/block-tools/src/rules/index.ts:
--------------------------------------------------------------------------------
1 | export * from './flatten-tables'
2 |
--------------------------------------------------------------------------------
/packages/editor/src/behaviors/_exports/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../index'
2 |
--------------------------------------------------------------------------------
/packages/editor/src/plugins/_exports/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../index'
2 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/_exports/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../index'
2 |
--------------------------------------------------------------------------------
/packages/editor/src/test/vitest/_exports/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../index'
2 |
--------------------------------------------------------------------------------
/packages/plugin-one-line/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './plugin.one-line'
2 |
--------------------------------------------------------------------------------
/packages/racejar/src/jest/index.ts:
--------------------------------------------------------------------------------
1 | export * from './jest-gherkin-driver'
2 |
--------------------------------------------------------------------------------
/packages/editor/src/test/index.ts:
--------------------------------------------------------------------------------
1 | export * from './gherkin-parameter-types'
2 |
--------------------------------------------------------------------------------
/packages/racejar/src/vitest/index.ts:
--------------------------------------------------------------------------------
1 | export * from './vitest-gherkin-driver'
2 |
--------------------------------------------------------------------------------
/packages/racejar/src/playwright/index.ts:
--------------------------------------------------------------------------------
1 | export * from './playwright-gherkin-driver'
2 |
--------------------------------------------------------------------------------
/packages/plugin-sdk-value/src/index.ts:
--------------------------------------------------------------------------------
1 | export {SDKValuePlugin} from './plugin.sdk-value'
2 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/mime-type.ts:
--------------------------------------------------------------------------------
1 | export type MIMEType = `${string}/${string}`
2 |
--------------------------------------------------------------------------------
/packages/plugin-markdown-shortcuts/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './plugin.markdown-shortcuts'
2 |
--------------------------------------------------------------------------------
/packages/test/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './terse-pt'
2 | export * from './test-key-generator'
3 |
--------------------------------------------------------------------------------
/packages/plugin-character-pair-decorator/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './plugin.character-pair-decorator'
2 |
--------------------------------------------------------------------------------
/apps/docs/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .turbo
2 | __screenshots__
3 | dist
4 | node_modules
5 | *.tgz
6 | .vercel
7 | .env*.local
8 | .eslintcache
9 |
--------------------------------------------------------------------------------
/apps/docs/src/assets/houston.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/portabletext/editor/HEAD/apps/docs/src/assets/houston.webp
--------------------------------------------------------------------------------
/packages/patches/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './applyPatch'
2 | export * from './patches'
3 | export * from './types'
4 |
--------------------------------------------------------------------------------
/.changeset/twenty-candies-do.md:
--------------------------------------------------------------------------------
1 | ---
2 | '@portabletext/editor': patch
3 | ---
4 |
5 | fix: make unique key checks faster
6 |
--------------------------------------------------------------------------------
/apps/playground/src/index.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 |
3 | pre {
4 | @apply text-sm;
5 | @apply overflow-auto;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/block-tools/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "//",
3 | "linter": {
4 | "includes": ["!**/*.html"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/apps/docs/src/assets/emoji-picker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/portabletext/editor/HEAD/apps/docs/src/assets/emoji-picker.png
--------------------------------------------------------------------------------
/apps/docs/src/assets/portable-text-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/portabletext/editor/HEAD/apps/docs/src/assets/portable-text-logo.png
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.feature?raw' {
2 | const content: string
3 | export default content
4 | }
5 |
--------------------------------------------------------------------------------
/packages/editor/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "noCheck": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/plugin-input-rule/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.feature?raw' {
2 | const content: string
3 | export default content
4 | }
5 |
--------------------------------------------------------------------------------
/packages/plugin-typography/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.feature?raw' {
2 | const content: string
3 | export default content
4 | }
5 |
--------------------------------------------------------------------------------
/.changeset/nine-schools-raise.md:
--------------------------------------------------------------------------------
1 | ---
2 | '@portabletext/editor': patch
3 | ---
4 |
5 | fix: avoid redundant nested calls to skip normalization
6 |
--------------------------------------------------------------------------------
/packages/block-tools/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "noCheck": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/editor/src/test/vitest/index.ts:
--------------------------------------------------------------------------------
1 | export * from './step-context'
2 | export * from './step-definitions'
3 | export * from './test-editor'
4 |
--------------------------------------------------------------------------------
/packages/plugin-emoji-picker/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.feature?raw' {
2 | const content: string
3 | export default content
4 | }
5 |
--------------------------------------------------------------------------------
/packages/racejar/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "include": ["src", "example", "example-playwright"]
4 | }
5 |
--------------------------------------------------------------------------------
/apps/playground/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "//",
3 | "css": {
4 | "parser": {
5 | "tailwindDirectives": true
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/plugin-markdown-shortcuts/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.feature?raw' {
2 | const content: string
3 | export default content
4 | }
5 |
--------------------------------------------------------------------------------
/packages/patches/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "include": ["**/*.ts"],
4 | "exclude": ["dist", "./node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/plugin-emoji-picker/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './create-match-emojis'
2 | export * from './match-emojis'
3 | export * from './use-emoji-picker'
4 |
--------------------------------------------------------------------------------
/packages/plugin-input-rule/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './input-rule'
2 | export * from './plugin.input-rule'
3 | export * from './text-transform-rule'
4 |
--------------------------------------------------------------------------------
/apps/playground/src/wait.ts:
--------------------------------------------------------------------------------
1 | export function wait(delay: number) {
2 | return new Promise((resolve) => {
3 | setTimeout(resolve, delay)
4 | })
5 | }
6 |
--------------------------------------------------------------------------------
/packages/markdown/src/from-portable-text/renderers/hard-break.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @public
3 | */
4 | export const DefaultHardBreakRenderer = (): string => ' \n'
5 |
--------------------------------------------------------------------------------
/packages/plugin-typography/assets/typography-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/portabletext/editor/HEAD/packages/plugin-typography/assets/typography-demo.gif
--------------------------------------------------------------------------------
/apps/docs/src/components/editor/editor.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import {PortableTextEditor} from './portable-text-editor'
3 | ---
4 |
5 |
6 |
--------------------------------------------------------------------------------
/packages/keyboard-shortcuts/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './keyboard-shortcuts'
2 | export * from './common-shortcuts'
3 | export * from './keyboard-event-definition'
4 |
--------------------------------------------------------------------------------
/packages/plugin-sdk-value/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "include": ["src"],
4 | "exclude": ["dist", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noEmit": true
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/examples/basic/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {"path": "./tsconfig.app.json"},
5 | {"path": "./tsconfig.node.json"}
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/examples/legacy/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {"path": "./tsconfig.app.json"},
5 | {"path": "./tsconfig.node.json"}
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/editor-schema.ts:
--------------------------------------------------------------------------------
1 | import type {Schema} from '@portabletext/schema'
2 |
3 | /**
4 | * @public
5 | */
6 | export type EditorSchema = Schema
7 |
--------------------------------------------------------------------------------
/packages/keyboard-shortcuts/src/is-apple.ts:
--------------------------------------------------------------------------------
1 | export const IS_APPLE =
2 | typeof window !== 'undefined' &&
3 | /Mac|iPod|iPhone|iPad/.test(window.navigator.userAgent)
4 |
--------------------------------------------------------------------------------
/packages/plugin-typography/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './create-decorator-guard'
2 | export * from './input-rules.typography'
3 | export * from './plugin.typography'
4 |
--------------------------------------------------------------------------------
/packages/editor/src/priority/priority.core.ts:
--------------------------------------------------------------------------------
1 | import {createEditorPriority} from './priority.types'
2 |
3 | export const corePriority = createEditorPriority({name: 'core'})
4 |
--------------------------------------------------------------------------------
/packages/racejar/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './compile-feature'
2 | export * from './create-parameter-type'
3 | export * from './step-definitions'
4 | export * from './hooks'
5 |
--------------------------------------------------------------------------------
/packages/schema/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "include": ["**/*.ts", "src/index.ts"],
4 | "exclude": ["dist", "./node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/plugin-typography/assets/smart-undo-with-backspace.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/portabletext/editor/HEAD/packages/plugin-typography/assets/smart-undo-with-backspace.gif
--------------------------------------------------------------------------------
/packages/sanity-bridge/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "include": ["**/*.ts", "src/index.ts"],
4 | "exclude": ["dist", "./node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/test/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "outDir": "./dist"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/patches/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "outDir": "./dist"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/codeBlock/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hei
4 | const foo = 'bar'
5 |
6 |
7 |
--------------------------------------------------------------------------------
/packages/markdown/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | dts: 'rolldown',
6 | })
7 |
--------------------------------------------------------------------------------
/packages/markdown/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "include": ["**/*.ts", "**/*.tsx", "src/index.ts"],
4 | "exclude": ["dist", "./node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/plugin-one-line/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "include": ["**/*.ts", "**/*.tsx", "src/index.ts"],
4 | "exclude": ["dist", "./node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/racejar/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | dts: 'rolldown',
6 | })
7 |
--------------------------------------------------------------------------------
/packages/schema/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | dts: 'rolldown',
6 | })
7 |
--------------------------------------------------------------------------------
/packages/test/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | dts: 'rolldown',
6 | })
7 |
--------------------------------------------------------------------------------
/packages/toolbar/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "include": ["**/*.ts", "**/*.tsx", "src/index.ts"],
4 | "exclude": ["dist", "./node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/apps/playground/src/editor-id-generator.ts:
--------------------------------------------------------------------------------
1 | export function* editorIdGenerator(): Generator {
2 | let index = 0
3 | while (true) {
4 | yield `${index++}`
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/sanity-bridge/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | dts: 'rolldown',
6 | })
7 |
--------------------------------------------------------------------------------
/packages/keyboard-shortcuts/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | dts: 'rolldown',
6 | })
7 |
--------------------------------------------------------------------------------
/packages/keyboard-shortcuts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "include": ["**/*.ts", "**/*.tsx", "src/index.ts"],
4 | "exclude": ["dist", "./node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/plugin-input-rule/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "include": ["src/*.ts", "src/*.tsx"],
4 | "exclude": ["dist", "node_modules", "src/**/*.test.*"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/plugin-typography/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "include": ["src/*.ts", "src/*.tsx"],
4 | "exclude": ["dist", "node_modules", "src/**/*.test.*"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/plugin-character-pair-decorator/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "include": ["**/*.ts", "**/*.tsx", "src/index.ts"],
4 | "exclude": ["dist", "./node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/plugin-emoji-picker/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "include": ["src/*.ts", "src/*.tsx"],
4 | "exclude": ["dist", "node_modules", "src/**/*.test.*"]
5 | }
6 |
--------------------------------------------------------------------------------
/apps/playground/src/key-generator.ts:
--------------------------------------------------------------------------------
1 | export function createKeyGenerator(prefix: string) {
2 | let index = 0
3 | return function keyGenerator(): string {
4 | return `${prefix}${index++}`
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/spinner.tsx:
--------------------------------------------------------------------------------
1 | import {LoaderCircle} from 'lucide-react'
2 |
3 | export function Spinner() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/packages/editor/src/plugins/index.ts:
--------------------------------------------------------------------------------
1 | export {BehaviorPlugin} from './plugin.behavior'
2 | export {EditorRefPlugin} from './plugin.editor-ref'
3 | export {EventListenerPlugin} from './plugin.event-listener'
4 |
--------------------------------------------------------------------------------
/packages/editor/src/types/block-offset.ts:
--------------------------------------------------------------------------------
1 | import type {BlockPath} from './paths'
2 |
3 | /**
4 | * @beta
5 | */
6 | export type BlockOffset = {
7 | path: BlockPath
8 | offset: number
9 | }
10 |
--------------------------------------------------------------------------------
/packages/markdown/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/racejar/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/toolbar/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/playground/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/packages/keyboard-shortcuts/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/plugin-emoji-picker/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/plugin-input-rule/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/plugin-markdown-shortcuts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "include": ["**/*.ts", "**/*.tsx", "src/index.ts"],
4 | "exclude": ["dist", "./node_modules", "src/**/*.test.*"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/plugin-typography/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/playground/README.md:
--------------------------------------------------------------------------------
1 | # Portable Text Playground
2 |
3 | ```sh
4 | # Install dependencies
5 | pnpm install
6 | # Build ./dist/
7 | pnpm build
8 | # Run locally on http://localhost:5173
9 | pnpm dev
10 | ```
11 |
--------------------------------------------------------------------------------
/apps/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.app.json"
6 | },
7 | {
8 | "path": "./tsconfig.node.json"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/relay-actor-context.ts:
--------------------------------------------------------------------------------
1 | import {createContext} from 'react'
2 | import type {RelayActor} from './relay-machine'
3 |
4 | export const RelayActorContext = createContext({} as RelayActor)
5 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/editor-actor-context.ts:
--------------------------------------------------------------------------------
1 | import {createContext} from 'react'
2 | import type {EditorActor} from './editor-machine'
3 |
4 | export const EditorActorContext = createContext({} as EditorActor)
5 |
--------------------------------------------------------------------------------
/packages/plugin-one-line/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "compilerOptions": {
4 | "rootDir": "src"
5 | },
6 | "include": ["src/**/*.ts"],
7 | "exclude": ["dist", "node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/plugin-sdk-value/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "compilerOptions": {
4 | "rootDir": "src"
5 | },
6 | "include": ["src/**/*.ts"],
7 | "exclude": ["dist", "node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/docs/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import {docsSchema} from '@astrojs/starlight/schema'
2 | import {defineCollection} from 'astro:content'
3 |
4 | export const collections = {
5 | docs: defineCollection({schema: docsSchema()}),
6 | }
7 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/simple/input.html:
--------------------------------------------------------------------------------
1 |
2 | This is markdown with code, strong, and emphasis. And
3 | a link too!
4 |
5 |
--------------------------------------------------------------------------------
/packages/sanity-bridge/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "outDir": "./dist",
6 | "exactOptionalPropertyTypes": false
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/schema/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "outDir": "./dist",
6 | "noPropertyAccessFromIndexSignature": false
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/plugin-markdown-shortcuts/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "compilerOptions": {
4 | "rootDir": "src"
5 | },
6 | "include": ["src/**/*.ts"],
7 | "exclude": ["dist", "node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/block-tools/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | /logs
3 | *.log
4 |
5 | # Coverage directory used by tools like istanbul
6 | /coverage
7 |
8 | # Dependency directories
9 | /node_modules
10 |
11 | # Compiled code
12 | /lib
13 | /dist
14 |
--------------------------------------------------------------------------------
/packages/plugin-character-pair-decorator/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "compilerOptions": {
4 | "rootDir": "src"
5 | },
6 | "include": ["src/**/*.ts"],
7 | "exclude": ["dist", "node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/toolbar/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | babel: {reactCompiler: true},
6 | reactCompilerOptions: {target: '19'},
7 | })
8 |
--------------------------------------------------------------------------------
/packages/editor/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | /logs
3 | *.log
4 |
5 | # Coverage directory used by tools like istanbul
6 | /coverage
7 |
8 | # Dependency directories
9 | /node_modules
10 |
11 | # Compiled code
12 | /lib
13 | /dist
14 | .eslintcache
15 |
--------------------------------------------------------------------------------
/packages/plugin-one-line/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | babel: {reactCompiler: true},
6 | reactCompilerOptions: {target: '19'},
7 | })
8 |
--------------------------------------------------------------------------------
/packages/plugin-sdk-value/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | babel: {reactCompiler: true},
6 | reactCompilerOptions: {target: '19'},
7 | })
8 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/schema.ts:
--------------------------------------------------------------------------------
1 | import {Schema} from '@sanity/schema'
2 |
3 | export function compileType(rawType: any) {
4 | return Schema.compile({
5 | name: 'blockTypeSchema',
6 | types: [rawType],
7 | }).get(rawType.name)
8 | }
9 |
--------------------------------------------------------------------------------
/packages/patches/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/plugin-emoji-picker/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | babel: {reactCompiler: true},
6 | reactCompilerOptions: {target: '19'},
7 | })
8 |
--------------------------------------------------------------------------------
/packages/plugin-input-rule/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | babel: {reactCompiler: true},
6 | reactCompilerOptions: {target: '19'},
7 | })
8 |
--------------------------------------------------------------------------------
/packages/plugin-typography/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | babel: {reactCompiler: true},
6 | reactCompilerOptions: {target: '19'},
7 | })
8 |
--------------------------------------------------------------------------------
/packages/schema/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/whitespace3/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | This
5 | is
6 | a
7 | test
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/packages/markdown/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/plugin-markdown-shortcuts/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | babel: {reactCompiler: true},
6 | reactCompilerOptions: {target: '19'},
7 | })
8 |
--------------------------------------------------------------------------------
/packages/racejar/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/toolbar/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/patches/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | strictOptions: {
6 | noImplicitBrowsersList: 'off',
7 | },
8 | dts: 'rolldown',
9 | })
10 |
--------------------------------------------------------------------------------
/packages/plugin-character-pair-decorator/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | tsconfig: 'tsconfig.dist.json',
5 | babel: {reactCompiler: true},
6 | reactCompilerOptions: {target: '19'},
7 | })
8 |
--------------------------------------------------------------------------------
/packages/sanity-bridge/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/examples/basic/src/main.tsx:
--------------------------------------------------------------------------------
1 | import {StrictMode} from 'react'
2 | import {createRoot} from 'react-dom/client'
3 | import App from './App.tsx'
4 |
5 | createRoot(document.getElementById('root')!).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/packages/block-tools/test/test-key-generator.ts:
--------------------------------------------------------------------------------
1 | export function createTestKeyGenerator(prefix = 'randomKey') {
2 | let index = 0
3 |
4 | return function keyGenerator() {
5 | const key = `${prefix}${index}`
6 | index++
7 | return key
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/editor/src/behaviors/behavior.config.ts:
--------------------------------------------------------------------------------
1 | import type {EditorPriority} from '../priority/priority.types'
2 | import type {Behavior} from './behavior.types.behavior'
3 |
4 | export type BehaviorConfig = {
5 | behavior: Behavior
6 | priority: EditorPriority
7 | }
8 |
--------------------------------------------------------------------------------
/packages/keyboard-shortcuts/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/plugin-emoji-picker/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/plugin-input-rule/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/plugin-typography/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | public-hoist-pattern[]=sanity
2 | ; This is needed for prettier to be able to use the plugin specified by the `@sanity/prettier-config` preset
3 | public-hoist-pattern[]=prettier-plugin-packagejson
4 | prefer-workspace-packages = true
5 | link-workspace-packages = deep
6 |
--------------------------------------------------------------------------------
/examples/legacy/src/main.tsx:
--------------------------------------------------------------------------------
1 | import {StrictMode} from 'react'
2 | import {createRoot} from 'react-dom/client'
3 | import App from './App.tsx'
4 |
5 | createRoot(document.getElementById('root')!).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/packages/test/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings.json",
3 | "compilerOptions": {
4 | "noCheck": true,
5 | "rootDir": "src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["dist", "node_modules", "src/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/playground/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import {App} from './App.tsx'
4 |
5 | ReactDOM.createRoot(document.getElementById('root')!).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .next
2 | dist
3 | lib
4 | CHANGELOG.md
5 | pnpm-lock.yaml
6 |
7 | apps/docs/src/content/docs/api
8 | packages/block-tools/test/**/*.html
9 | .changeset/*.md
10 | packages/markdown/src/example-document.out.md
11 | packages/markdown/src/example-document.advanced.out.md
12 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.get-text-block-text.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextTextBlock} from '@portabletext/schema'
2 |
3 | /**
4 | * @public
5 | */
6 | export function getTextBlockText(block: PortableTextTextBlock) {
7 | return block.children.map((child) => child.text ?? '').join('')
8 | }
9 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.is-keyed-segment.ts:
--------------------------------------------------------------------------------
1 | import type {KeyedSegment} from '../types/paths'
2 |
3 | /**
4 | * @public
5 | */
6 | export function isKeyedSegment(segment: unknown): segment is KeyedSegment {
7 | return typeof segment === 'object' && segment !== null && '_key' in segment
8 | }
9 |
--------------------------------------------------------------------------------
/packages/test/src/test-key-generator.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @public
3 | */
4 | export function createTestKeyGenerator(prefix?: string) {
5 | let index = 0
6 |
7 | return function keyGenerator() {
8 | const key = `${prefix ?? ''}k${index}`
9 | index++
10 | return key
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/block-keys.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextBlock} from '@portabletext/schema'
2 |
3 | export function getBlockKeys(value: Array | undefined) {
4 | if (!value) {
5 | return []
6 | }
7 |
8 | return value.map((block) => block._key)
9 | }
10 |
--------------------------------------------------------------------------------
/.prettierrc.mjs:
--------------------------------------------------------------------------------
1 | import config from '@sanity/prettier-config'
2 |
3 | export default {
4 | ...config,
5 | printWidth: 80,
6 | plugins: [
7 | ...config.plugins,
8 | '@ianvs/prettier-plugin-sort-imports',
9 | 'prettier-plugin-astro',
10 | 'prettier-plugin-gherkin',
11 | ],
12 | }
13 |
--------------------------------------------------------------------------------
/packages/plugin-one-line/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist",
7 | "exactOptionalPropertyTypes": false,
8 | "noUncheckedIndexedAccess": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/blockTags/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Paragraph
4 | H1
5 | H2
6 | H3
7 | H4
8 | H5
9 | H6
10 | Blockquote
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/block-tools/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./tsconfig.base.json",
4 | "include": ["./src", "./test"],
5 | "compilerOptions": {
6 | "rootDir": ".",
7 | "outDir": "./lib",
8 |
9 | "isolatedModules": false
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/editor-context.tsx:
--------------------------------------------------------------------------------
1 | import type {Editor} from '../editor'
2 | import {createGloballyScopedContext} from '../internal-utils/globally-scoped-context'
3 |
4 | export const EditorContext = createGloballyScopedContext(
5 | '@portabletext/editor/context/editor',
6 | null,
7 | )
8 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/string-utils.ts:
--------------------------------------------------------------------------------
1 | export function splitString(string: string, searchString: string) {
2 | const index = string.indexOf(searchString)
3 | if (index === -1) {
4 | return [string, '']
5 | }
6 | return [string.slice(0, index), string.slice(index + searchString.length)]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/plugin-sdk-value/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist",
7 | "exactOptionalPropertyTypes": false,
8 | "noUncheckedIndexedAccess": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/plugin-markdown-shortcuts/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist",
7 | "exactOptionalPropertyTypes": false,
8 | "noUncheckedIndexedAccess": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/docs/src/components/EventTypesList.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import {getExportedArrayFromTsFile} from '@/lib/utils.ts'
3 |
4 | const eventTypes = await getExportedArrayFromTsFile(
5 | Astro.props.filePath,
6 | Astro.props.arrayName,
7 | )
8 | ---
9 |
10 |
11 | {eventTypes?.map((type) => - {type}
)}
12 |
13 |
--------------------------------------------------------------------------------
/packages/plugin-character-pair-decorator/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/tsconfig/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "rootDir": ".",
6 | "outDir": "./dist",
7 | "exactOptionalPropertyTypes": false,
8 | "noUncheckedIndexedAccess": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.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 |
15 | [*.snap]
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/apps/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "include": [".astro/types.d.ts", "**/*"],
4 | "exclude": ["dist"],
5 | "compilerOptions": {
6 | "jsx": "react-jsx",
7 | "jsxImportSource": "react",
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/decorators/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Strong text but
5 | this is also emphasized and
6 | striked
7 |
8 |
9 | Just emphasized
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/block-tools/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | projects: [
6 | {
7 | plugins: [],
8 | test: {
9 | name: 'unit',
10 | environment: 'node',
11 | },
12 | },
13 | ],
14 | },
15 | })
16 |
--------------------------------------------------------------------------------
/packages/markdown/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | projects: [
6 | {
7 | plugins: [],
8 | test: {
9 | name: 'unit',
10 | environment: 'node',
11 | },
12 | },
13 | ],
14 | },
15 | })
16 |
--------------------------------------------------------------------------------
/packages/patches/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | projects: [
6 | {
7 | plugins: [],
8 | test: {
9 | name: 'unit',
10 | environment: 'node',
11 | },
12 | },
13 | ],
14 | },
15 | })
16 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/slate-plugin.redoing.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextSlateEditor} from '../types/slate-editor'
2 |
3 | export function pluginRedoing(editor: PortableTextSlateEditor, fn: () => void) {
4 | const prev = editor.isRedoing
5 |
6 | editor.isRedoing = true
7 |
8 | fn()
9 |
10 | editor.isRedoing = prev
11 | }
12 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/slate-plugin.undoing.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextSlateEditor} from '../types/slate-editor'
2 |
3 | export function pluginUndoing(editor: PortableTextSlateEditor, fn: () => void) {
4 | const prev = editor.isUndoing
5 |
6 | editor.isUndoing = true
7 |
8 | fn()
9 |
10 | editor.isUndoing = prev
11 | }
12 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-selection.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelector} from '../editor/editor-selector'
2 | import type {EditorSelection} from '../types/editor'
3 |
4 | /**
5 | * @public
6 | */
7 | export const getSelection: EditorSelector = (snapshot) => {
8 | return snapshot.context.selection
9 | }
10 |
--------------------------------------------------------------------------------
/packages/editor/src/test/vitest/step-context.ts:
--------------------------------------------------------------------------------
1 | import type {Locator} from 'vitest/browser'
2 | import type {Editor} from '../../editor'
3 |
4 | /**
5 | * @internal
6 | */
7 | export type Context = {
8 | editor: Editor
9 | editorB: Editor
10 | locator: Locator
11 | locatorB: Locator
12 | keyMap?: Map
13 | }
14 |
--------------------------------------------------------------------------------
/packages/plugin-sdk-value/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | projects: [
6 | {
7 | plugins: [],
8 | test: {
9 | name: 'unit',
10 | environment: 'node',
11 | },
12 | },
13 | ],
14 | },
15 | })
16 |
--------------------------------------------------------------------------------
/packages/editor/src/operations/operation.insert.text.ts:
--------------------------------------------------------------------------------
1 | import {Transforms} from 'slate'
2 | import type {OperationImplementation} from './operation.types'
3 |
4 | export const insertTextOperationImplementation: OperationImplementation<
5 | 'insert.text'
6 | > = ({operation}) => {
7 | Transforms.insertText(operation.editor, operation.text)
8 | }
9 |
--------------------------------------------------------------------------------
/packages/keyboard-shortcuts/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | projects: [
6 | {
7 | plugins: [],
8 | test: {
9 | name: 'unit',
10 | environment: 'node',
11 | },
12 | },
13 | ],
14 | },
15 | })
16 |
--------------------------------------------------------------------------------
/packages/racejar/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | projects: [
6 | {
7 | plugins: [],
8 | test: {
9 | name: 'unit',
10 | exclude: ['example-playwright'],
11 | },
12 | },
13 | ],
14 | },
15 | })
16 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/without-normalizing-conditional.ts:
--------------------------------------------------------------------------------
1 | import {Editor} from 'slate'
2 |
3 | export function withoutNormalizingConditional(
4 | editor: Editor,
5 | predicate: () => boolean,
6 | fn: () => void,
7 | ) {
8 | if (predicate()) {
9 | Editor.withoutNormalizing(editor, fn)
10 | } else {
11 | fn()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/withoutPatching.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextSlateEditor} from '../types/slate-editor'
2 |
3 | export function withoutPatching(
4 | editor: PortableTextSlateEditor,
5 | fn: () => void,
6 | ): void {
7 | const prev = editor.isPatching
8 | editor.isPatching = false
9 | fn()
10 | editor.isPatching = prev
11 | }
12 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/delete.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import deleteFeature from '../gherkin-spec/delete.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: deleteFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-value.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextBlock} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 |
4 | /**
5 | * @public
6 | */
7 | export const getValue: EditorSelector> = (
8 | snapshot,
9 | ) => {
10 | return snapshot.context.value
11 | }
12 |
--------------------------------------------------------------------------------
/packages/block-tools/src/util/findBlockType.ts:
--------------------------------------------------------------------------------
1 | import type {BlockSchemaType, SchemaType} from '@sanity/types'
2 |
3 | export function findBlockType(type: SchemaType): type is BlockSchemaType {
4 | if (type.type) {
5 | return findBlockType(type.type)
6 | }
7 |
8 | if (type.name === 'block') {
9 | return true
10 | }
11 |
12 | return false
13 | }
14 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/selection.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import selectionFeature from '../gherkin-spec/selection.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: selectionFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/with-normalizing-node.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextSlateEditor} from '../types/slate-editor'
2 |
3 | export function withNormalizeNode(
4 | editor: PortableTextSlateEditor,
5 | fn: () => void,
6 | ) {
7 | const prev = editor.isNormalizingNode
8 | editor.isNormalizingNode = true
9 | fn()
10 | editor.isNormalizingNode = prev
11 | }
12 |
--------------------------------------------------------------------------------
/packages/editor/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./tsconfig.base.json",
4 | "include": ["./src", "./gherkin-tests", "./tests"],
5 | "compilerOptions": {
6 | "rootDir": ".",
7 | "outDir": "./lib",
8 |
9 | "isolatedModules": false,
10 | "types": ["@vitest/browser-playwright"]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/insert.text.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import insertTextFeature from '../gherkin-spec/insert.text.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: insertTextFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/block-objects.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import blockObjectsFeature from '../gherkin-spec/block-objects.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: blockObjectsFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/insert.block.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import insertBlockFeature from '../gherkin-spec/insert.block.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: insertBlockFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/insert.blocks.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import insertBlocksFeature from '../gherkin-spec/insert.blocks.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: insertBlocksFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/insert.child.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import insertChildFeature from '../gherkin-spec/insert.child.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: insertChildFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/examples/basic/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/examples/legacy/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/inline-objects.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import inlineObjectsFeature from '../gherkin-spec/inline-objects.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: inlineObjectsFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/lists.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import listsFeature from '../gherkin-spec/lists.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: listsFeature,
8 | stepDefinitions: stepDefinitions,
9 | parameterTypes: parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/paste.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import pasteFeature from '../gherkin-spec/paste.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: pasteFeature,
8 | stepDefinitions: stepDefinitions,
9 | parameterTypes: parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/slate-plugin.without-history.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextSlateEditor} from '../types/slate-editor'
2 |
3 | export function pluginWithoutHistory(
4 | editor: PortableTextSlateEditor,
5 | fn: () => void,
6 | ): void {
7 | const prev = editor.withHistory
8 |
9 | editor.withHistory = false
10 |
11 | fn()
12 |
13 | editor.withHistory = prev
14 | }
15 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/debug.ts:
--------------------------------------------------------------------------------
1 | import debug from 'debug'
2 |
3 | const rootName = 'sanity-pte:'
4 |
5 | export default debug(rootName)
6 | export function debugWithName(name: string): debug.Debugger {
7 | const namespace = `${rootName}${name}`
8 | if (debug && debug.enabled(namespace)) {
9 | return debug(namespace)
10 | }
11 | return debug(rootName)
12 | }
13 |
--------------------------------------------------------------------------------
/packages/editor/src/types/options.ts:
--------------------------------------------------------------------------------
1 | import type {BaseSyntheticEvent} from 'react'
2 | import type {PortableTextEditor} from '../editor/PortableTextEditor'
3 |
4 | /**
5 | * @beta
6 | */
7 | export type HotkeyOptions = {
8 | marks?: Record
9 | custom?: Record<
10 | string,
11 | (event: BaseSyntheticEvent, editor: PortableTextEditor) => void
12 | >
13 | }
14 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/annotations/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | I am a strong link
5 | and I am a emphasized link
6 |
7 |
8 | I am a totally different link
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/whitespace3/output.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "_key": "randomKey0",
4 | "_type": "block",
5 | "children": [
6 | {
7 | "_key": "randomKey1",
8 | "_type": "span",
9 | "marks": [],
10 | "text": "This is a test"
11 | }
12 | ],
13 | "markDefs": [],
14 | "style": "normal"
15 | }
16 | ]
17 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/withChanges.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextSlateEditor} from '../types/slate-editor'
2 |
3 | export function withRemoteChanges(
4 | editor: PortableTextSlateEditor,
5 | fn: () => void,
6 | ): void {
7 | const prev = editor.isProcessingRemoteChanges
8 | editor.isProcessingRemoteChanges = true
9 | fn()
10 | editor.isProcessingRemoteChanges = prev
11 | }
12 |
--------------------------------------------------------------------------------
/apps/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
23 | # generated TS docs files
24 | /src/content/docs/api
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/splitting-blocks.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import splittingBlocksFeature from '../gherkin-spec/splitting-blocks.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: splittingBlocksFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/src/behaviors/behavior.types.guard.ts:
--------------------------------------------------------------------------------
1 | import type {EditorDom} from '../editor/editor-dom'
2 | import type {EditorSnapshot} from '../editor/editor-snapshot'
3 |
4 | /**
5 | * @beta
6 | */
7 | export type BehaviorGuard = (payload: {
8 | snapshot: EditorSnapshot
9 | event: TBehaviorEvent
10 | dom: EditorDom
11 | }) => TGuardResponse | false
12 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.is-equal-selection-points.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelectionPoint} from '../types/editor'
2 | import {isEqualPaths} from './util.is-equal-paths'
3 |
4 | /**
5 | * @public
6 | */
7 | export function isEqualSelectionPoints(
8 | a: EditorSelectionPoint,
9 | b: EditorSelectionPoint,
10 | ) {
11 | return a.offset === b.offset && isEqualPaths(a.path, b.path)
12 | }
13 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.is-selection-expanded.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelection} from '../types/editor'
2 | import {isSelectionCollapsed} from './util.is-selection-collapsed'
3 |
4 | /**
5 | * @public
6 | */
7 | export function isSelectionExpanded(selection: EditorSelection) {
8 | if (!selection) {
9 | return false
10 | }
11 |
12 | return !isSelectionCollapsed(selection)
13 | }
14 |
--------------------------------------------------------------------------------
/apps/playground/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .eslintcache
26 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/customRules/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hei
4 | const foo = 'bar'
5 |
6 | Quote with emphasis
7 | Emphasis!
8 |
9 | 
10 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.is-selection-expanded.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelector} from '../editor/editor-selector'
2 | import {isSelectionCollapsed} from './selector.is-selection-collapsed'
3 |
4 | /**
5 | * @public
6 | */
7 | export const isSelectionExpanded: EditorSelector = (snapshot) => {
8 | return snapshot.context.selection !== null && !isSelectionCollapsed(snapshot)
9 | }
10 |
--------------------------------------------------------------------------------
/apps/playground/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5 | "skipLibCheck": true,
6 | "module": "ESNext",
7 | "moduleResolution": "bundler",
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "noEmit": true
11 | },
12 | "include": ["vite.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/insert.break.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import insertBreakFeature from '../gherkin-spec/insert.break.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: insertBreakFeature,
8 | stepDefinitions: stepDefinitions,
9 | parameterTypes: parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/selection-adjustment.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import selectionAdjustmentFeature from '../gherkin-spec/selection-adjustment.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: selectionAdjustmentFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/src/operations/operation.move.forward.ts:
--------------------------------------------------------------------------------
1 | import {Transforms} from 'slate'
2 | import type {OperationImplementation} from './operation.types'
3 |
4 | export const moveForwardOperationImplementation: OperationImplementation<
5 | 'move.forward'
6 | > = ({operation}) => {
7 | Transforms.move(operation.editor, {
8 | unit: 'character',
9 | distance: operation.distance,
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/apps/playground/src/plugins/looks-like-url.ts:
--------------------------------------------------------------------------------
1 | export function looksLikeUrl(text: string) {
2 | let looksLikeUrl = false
3 | try {
4 | const url = new URL(text)
5 |
6 | if (!sensibleProtocols.includes(url.protocol)) {
7 | return false
8 | }
9 |
10 | looksLikeUrl = true
11 | } catch {}
12 | return looksLikeUrl
13 | }
14 |
15 | const sensibleProtocols = ['http:', 'https:', 'mailto:', 'tel:']
16 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/asserters.ts:
--------------------------------------------------------------------------------
1 | import type {TypedObject} from '@portabletext/schema'
2 |
3 | export function isTypedObject(object: unknown): object is TypedObject {
4 | return isRecord(object) && typeof object._type === 'string'
5 | }
6 |
7 | export function isRecord(value: unknown): value is Record {
8 | return !!value && (typeof value === 'object' || typeof value === 'function')
9 | }
10 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/annotations-collaboration.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import annotationsCollaborationFeature from '../gherkin-spec/annotations-collaboration.feature?raw'
3 | import {parameterTypes} from '../src/test'
4 | import {stepDefinitions} from '../src/test/vitest'
5 |
6 | Feature({
7 | featureText: annotationsCollaborationFeature,
8 | stepDefinitions,
9 | parameterTypes,
10 | })
11 |
--------------------------------------------------------------------------------
/packages/editor/src/operations/operation.move.backward.ts:
--------------------------------------------------------------------------------
1 | import {Transforms} from 'slate'
2 | import type {OperationImplementation} from './operation.types'
3 |
4 | export const moveBackwardOperationImplementation: OperationImplementation<
5 | 'move.backward'
6 | > = ({operation}) => {
7 | Transforms.move(operation.editor, {
8 | unit: 'character',
9 | distance: operation.distance,
10 | reverse: true,
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.is-active-style.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelector} from '../editor/editor-selector'
2 | import {getActiveStyle} from './selector.get-active-style'
3 |
4 | /**
5 | * @public
6 | */
7 | export function isActiveStyle(style: string): EditorSelector {
8 | return (snapshot) => {
9 | const activeStyle = getActiveStyle(snapshot)
10 |
11 | return activeStyle === style
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/examples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/legacy/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/render.drop-indicator.tsx:
--------------------------------------------------------------------------------
1 | export function DropIndicator() {
2 | return (
3 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "//",
3 | "linter": {
4 | "rules": {
5 | "style": {
6 | "noRestrictedImports": {
7 | "level": "error",
8 | "options": {
9 | "paths": {
10 | "@sanity/types": "Import from 'types/sanity-types' instead to maintain visibility over @sanity/types usage."
11 | }
12 | }
13 | }
14 | }
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/with-performing-behavior-operation.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextSlateEditor} from '../types/slate-editor'
2 |
3 | export function withPerformingBehaviorOperation(
4 | editor: PortableTextSlateEditor,
5 | fn: () => void,
6 | ) {
7 | const prev = editor.isPerformingBehaviorOperation
8 |
9 | editor.isPerformingBehaviorOperation = true
10 |
11 | fn()
12 |
13 | editor.isPerformingBehaviorOperation = prev
14 | }
15 |
--------------------------------------------------------------------------------
/packages/block-tools/src/HtmlDeserializer/preprocessors/xpathResult.ts:
--------------------------------------------------------------------------------
1 | // We need this here if run server side
2 | export const _XPathResult = {
3 | ANY_TYPE: 0,
4 | NUMBER_TYPE: 1,
5 | STRING_TYPE: 2,
6 | BOOLEAN_TYPE: 3,
7 | UNORDERED_NODE_ITERATOR_TYPE: 4,
8 | ORDERED_NODE_ITERATOR_TYPE: 5,
9 | UNORDERED_NODE_SNAPSHOT_TYPE: 6,
10 | ORDERED_NODE_SNAPSHOT_TYPE: 7,
11 | ANY_UNORDERED_NODE_TYPE: 8,
12 | FIRST_ORDERED_NODE_TYPE: 9,
13 | }
14 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.is-active-list-item.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelector} from '../editor/editor-selector'
2 | import {getActiveListItem} from './selector.get-active-list-item'
3 |
4 | /**
5 | * @public
6 | */
7 | export function isActiveListItem(listItem: string): EditorSelector {
8 | return (snapshot) => {
9 | const activeListItem = getActiveListItem(snapshot)
10 |
11 | return activeListItem === listItem
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/plugin-input-rule/src/rule.stock-ticker.feature:
--------------------------------------------------------------------------------
1 | Feature: Stock Ticker Rule
2 |
3 | Scenario Outline: Transforms plain text into stock ticker
4 | Given the text
5 | When is inserted
6 | And "{ArrowRight}" is pressed
7 | And "new" is typed
8 | Then the text is
9 |
10 | Examples:
11 | | text | inserted text | new text |
12 | | "" | "{AAPL}" | ",{stock-ticker},new" |
13 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/split-string.ts:
--------------------------------------------------------------------------------
1 | export function splitString(string: string, searchString: string) {
2 | const searchStringIndex = string.indexOf(searchString)
3 |
4 | if (searchStringIndex === -1) {
5 | return [string, ''] as const
6 | }
7 |
8 | const firstPart = string.slice(0, searchStringIndex)
9 | const secondPart = string.slice(searchStringIndex + searchString.length)
10 |
11 | return [firstPart, secondPart] as const
12 | }
13 |
--------------------------------------------------------------------------------
/apps/playground/src/App.tsx:
--------------------------------------------------------------------------------
1 | import {useActorRef} from '@xstate/react'
2 | import {editorIdGenerator} from './editor-id-generator'
3 | import {Editors} from './editors'
4 | import {playgroundMachine} from './playground-machine'
5 |
6 | export function App() {
7 | const playgroundRef = useActorRef(playgroundMachine, {
8 | input: {
9 | editorIdGenerator: editorIdGenerator(),
10 | },
11 | })
12 |
13 | return
14 | }
15 |
--------------------------------------------------------------------------------
/.github/workflows/renovate.yml:
--------------------------------------------------------------------------------
1 | name: Add changeset to Renovate updates
2 |
3 | on:
4 | pull_request_target:
5 | types: [opened, synchronize]
6 |
7 | concurrency: ${{ github.workflow }}-${{ github.ref }}
8 |
9 | permissions:
10 | contents: read # for checkout
11 |
12 | jobs:
13 | call:
14 | uses: portabletext/.github/.github/workflows/changesets-from-conventional-commits.yml@main
15 | if: github.event.pull_request.user.login == 'renovate[bot]'
16 | secrets: inherit
17 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.is-selection-collapsed.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelection} from '../types/editor'
2 | import {isEqualPaths} from './util.is-equal-paths'
3 |
4 | /**
5 | * @public
6 | */
7 | export function isSelectionCollapsed(selection: EditorSelection) {
8 | if (!selection) {
9 | return false
10 | }
11 |
12 | return (
13 | isEqualPaths(selection.anchor.path, selection.focus.path) &&
14 | selection.anchor.offset === selection.focus.offset
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/blockTags/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/codeBlock/output.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "_key": "randomKey0",
4 | "_type": "block",
5 | "children": [
6 | {
7 | "_key": "randomKey1",
8 | "_type": "span",
9 | "marks": ["em"],
10 | "text": "Hei"
11 | }
12 | ],
13 | "markDefs": [],
14 | "style": "normal"
15 | },
16 | {
17 | "_key": "randomKey2",
18 | "_type": "code",
19 | "text": "const foo = 'bar'"
20 | }
21 | ]
22 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/complex/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/customSchema/index.ts:
--------------------------------------------------------------------------------
1 | import customSchema from '../../../fixtures/customSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = customSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/simple/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/sanity-bridge/src/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | createPortableTextMemberSchemaTypes,
3 | type PortableTextMemberSchemaTypes,
4 | } from './portable-text-member-schema-types'
5 | export {portableTextMemberSchemaTypesToSchema} from './portable-text-member-schema-types-to-schema'
6 | export {sanitySchemaToPortableTextSchema} from './sanity-schema-to-portable-text-schema'
7 | export {compileSchemaDefinitionToPortableTextMemberSchemaTypes} from './schema-definition-to-portable-text-member-schema-types'
8 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/annotations/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/decorators/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/fromTheWild1/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/fromTheWild4/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/gdocsLists/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/githubIssue/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/whitespace2/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/whitespace3/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/stegaUnicodeCleaner/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/whitespaceInPreTags/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, commonOptions)
10 | }
11 |
12 | export default testFn
13 |
--------------------------------------------------------------------------------
/packages/toolbar/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-toolbar-schema'
2 | export * from './use-annotation-button'
3 | export * from './use-annotation-popover'
4 | export * from './use-block-object-button'
5 | export * from './use-block-object-popover'
6 | export * from './use-decorator-button'
7 | export * from './use-history-buttons'
8 | export * from './use-inline-object-button'
9 | export * from './use-inline-object-popover'
10 | export * from './use-list-button'
11 | export * from './use-style-selector'
12 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/icon.tsx:
--------------------------------------------------------------------------------
1 | import {isValidElement} from 'react'
2 | import {isValidElementType} from 'react-is'
3 |
4 | export function Icon(props: {
5 | icon?: React.ReactNode | React.ComponentType
6 | fallback: string | null
7 | }) {
8 | const IconComponent = props.icon
9 |
10 | return isValidElement(IconComponent) ? (
11 | IconComponent
12 | ) : isValidElementType(IconComponent) ? (
13 |
14 | ) : (
15 | props.fallback
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/apps/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Portable Text Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/packages/editor/src/behaviors/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | effect,
3 | execute,
4 | forward,
5 | raise,
6 | type BehaviorAction,
7 | type BehaviorActionSet,
8 | } from './behavior.types.action'
9 | export {defineBehavior, type Behavior} from './behavior.types.behavior'
10 | export type {
11 | BehaviorEvent,
12 | CustomBehaviorEvent,
13 | InsertPlacement,
14 | NativeBehaviorEvent,
15 | SyntheticBehaviorEvent,
16 | } from './behavior.types.event'
17 | export type {BehaviorGuard} from './behavior.types.guard'
18 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-active-annotation-marks.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSnapshot} from '../editor/editor-snapshot'
2 | import {getMarkState} from './selector.get-mark-state'
3 |
4 | export function getActiveAnnotationsMarks(snapshot: EditorSnapshot) {
5 | const schema = snapshot.context.schema
6 | const markState = getMarkState(snapshot)
7 |
8 | return (markState?.marks ?? []).filter(
9 | (mark) =>
10 | !schema.decorators.map((decorator) => decorator.name).includes(mark),
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-first-block.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextBlock} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {BlockPath} from '../types/paths'
4 |
5 | /**
6 | * @public
7 | */
8 | export const getFirstBlock: EditorSelector<
9 | {node: PortableTextBlock; path: BlockPath} | undefined
10 | > = (snapshot) => {
11 | const node = snapshot.context.value[0]
12 |
13 | return node ? {node, path: [{_key: node._key}]} : undefined
14 | }
15 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/render.text.tsx:
--------------------------------------------------------------------------------
1 | import type {Editable} from 'slate-react'
2 |
3 | export type RenderTextProps = Parameters<
4 | NonNullable['renderText']>
5 | >[0]
6 |
7 | export function RenderText(props: RenderTextProps) {
8 | return (
9 |
15 | {props.children}
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/group.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | composeRenderProps,
3 | Group as RACGroup,
4 | type GroupProps,
5 | } from 'react-aria-components'
6 | import {tv} from 'tailwind-variants'
7 |
8 | const styles = tv({
9 | base: 'contents',
10 | })
11 |
12 | export function Group(props: GroupProps) {
13 | return (
14 |
17 | styles({...renderProps, className}),
18 | )}
19 | />
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | permissions:
11 | contents: read # for checkout
12 |
13 | jobs:
14 | release:
15 | uses: portabletext/.github/.github/workflows/changesets.yml@main
16 | permissions:
17 | contents: read # for checkout
18 | id-token: write # to enable use of OIDC for npm provenance
19 | with:
20 | TURBO_TEAM: ${{ vars.TURBO_TEAM }}
21 | secrets: inherit
22 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/types.ts:
--------------------------------------------------------------------------------
1 | import type {htmlToBlocks, normalizeBlock} from '../../../src'
2 | import type {TypedObject} from '../../../src/types'
3 |
4 | interface BlockContentFunctions {
5 | normalizeBlock: typeof normalizeBlock
6 | htmlToBlocks: typeof htmlToBlocks
7 | }
8 |
9 | export type BlockTestFn = (
10 | input: string,
11 | blockTools: BlockContentFunctions,
12 | commonOptions: {
13 | parseHtml: (html: string) => Document
14 | keyGenerator: () => string
15 | },
16 | ) => TypedObject[]
17 |
--------------------------------------------------------------------------------
/packages/editor/src/plugins/plugin.editor-ref.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type {Editor} from '../editor'
3 | import {useEditor} from '../editor/use-editor'
4 |
5 | /**
6 | * @beta
7 | */
8 | export const EditorRefPlugin = React.forwardRef((_, ref) => {
9 | const editor = useEditor()
10 |
11 | const portableTextEditorRef = React.useRef(editor)
12 |
13 | React.useImperativeHandle(ref, () => portableTextEditorRef.current, [])
14 |
15 | return null
16 | })
17 | EditorRefPlugin.displayName = 'EditorRefPlugin'
18 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.is-equal-selections.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelection} from '../types/editor'
2 | import {isEqualSelectionPoints} from './util.is-equal-selection-points'
3 |
4 | /**
5 | * @public
6 | */
7 | export function isEqualSelections(a: EditorSelection, b: EditorSelection) {
8 | if (!a && !b) {
9 | return true
10 | }
11 |
12 | if (!a || !b) {
13 | return false
14 | }
15 |
16 | return (
17 | isEqualSelectionPoints(a.anchor, b.anchor) &&
18 | isEqualSelectionPoints(a.focus, b.focus)
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/packages/editor/src/types/sanity-types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file centralizes all imports from @sanity/types.
3 | * Any types needed from @sanity/types should be re-exported here
4 | * to maintain visibility over the dependency.
5 | */
6 |
7 | export type {
8 | ArrayDefinition,
9 | ArraySchemaType,
10 | BlockDecoratorDefinition,
11 | BlockListDefinition,
12 | BlockStyleDefinition,
13 | ObjectSchemaType,
14 | // biome-ignore lint/style/noRestrictedImports: This is the designated file for @sanity/types imports
15 | } from '@sanity/types'
16 |
--------------------------------------------------------------------------------
/apps/docs/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.mjs",
8 | "css": "src/styles/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/gdocs/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, {
10 | ...commonOptions,
11 | unstable_whitespaceOnPasteMode: 'normalize',
12 | })
13 | }
14 |
15 | export default testFn
16 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-selection-end-point.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelector} from '../editor/editor-selector'
2 | import type {EditorSelectionPoint} from '../types/editor'
3 |
4 | /**
5 | * @public
6 | */
7 | export const getSelectionEndPoint: EditorSelector<
8 | EditorSelectionPoint | undefined
9 | > = (snapshot) => {
10 | if (!snapshot.context.selection) {
11 | return undefined
12 | }
13 |
14 | return snapshot.context.selection.backward
15 | ? snapshot.context.selection.anchor
16 | : snapshot.context.selection.focus
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor/tsconfig.typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./tsconfig.base.json",
4 | "compilerOptions": {
5 | "rootDir": ".",
6 | "outDir": "./lib",
7 | "isolatedModules": false,
8 | "types": ["node"]
9 | },
10 | "include": ["./src"],
11 | "exclude": [
12 | "./gherkin-tests",
13 | "./tests",
14 | "**/*.test.ts",
15 | "**/*.test.tsx",
16 | "**/*.spec.ts",
17 | "**/*.spec.tsx",
18 | "**/__tests__/**",
19 | "**/test/**",
20 | "**/tests/**"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/toolbar.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | composeRenderProps,
3 | Toolbar as RACToolbar,
4 | type ToolbarProps,
5 | } from 'react-aria-components'
6 | import {tv} from 'tailwind-variants'
7 |
8 | const styles = tv({
9 | base: 'flex gap-2 flex-wrap',
10 | })
11 |
12 | export function Toolbar(props: ToolbarProps) {
13 | return (
14 |
17 | styles({...renderProps, className}),
18 | )}
19 | />
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/customSchema/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | A heading that is not allowed
4 |
5 | Strong text but
6 | this is also emphasized and
7 | striked
8 | and subbed
9 | and supped
10 | and inserted
11 | and marked
12 | and deleted
13 | and shrunk
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-selection-start-point.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelector} from '../editor/editor-selector'
2 | import type {EditorSelectionPoint} from '../types/editor'
3 |
4 | /**
5 | * @public
6 | */
7 | export const getSelectionStartPoint: EditorSelector<
8 | EditorSelectionPoint | undefined
9 | > = (snapshot) => {
10 | if (!snapshot.context.selection) {
11 | return undefined
12 | }
13 |
14 | return snapshot.context.selection.backward
15 | ? snapshot.context.selection.focus
16 | : snapshot.context.selection.anchor
17 | }
18 |
--------------------------------------------------------------------------------
/packages/plugin-emoji-picker/src/match-emojis.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The base type representing an emoji match.
3 | *
4 | * @beta
5 | */
6 | export type BaseEmojiMatch =
7 | | {
8 | type: 'exact'
9 | emoji: string
10 | }
11 | | {
12 | type: 'partial'
13 | emoji: string
14 | }
15 |
16 | /**
17 | * A function that returns an array of emoji matches for a given keyword.
18 | *
19 | * @beta
20 | */
21 | export type MatchEmojis =
22 | (query: {keyword: string}) => ReadonlyArray
23 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | {"repo": "portabletext/editor"}
6 | ],
7 | "commit": false,
8 | "access": "public",
9 | "baseBranch": "main",
10 | "updateInternalDependencies": "patch",
11 | "ignore": ["docs", "playground", "example-basic", "example-legacy"],
12 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
13 | "updateInternalDependents": "always",
14 | "onlyUpdatePeerDependentsWhenOutOfRange": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/gdocsFirefox/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, {
10 | ...commonOptions,
11 | unstable_whitespaceOnPasteMode: 'normalize',
12 | })
13 | }
14 |
15 | export default testFn
16 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/gdocsWhitespaceRemove/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, {
10 | ...commonOptions,
11 | unstable_whitespaceOnPasteMode: 'remove',
12 | })
13 | }
14 |
15 | export default testFn
16 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/gdocsStrikethroughLink/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, {
10 | ...commonOptions,
11 | unstable_whitespaceOnPasteMode: 'normalize',
12 | })
13 | }
14 |
15 | export default testFn
16 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/gdocsWhitespaceRemoveFirefox/index.ts:
--------------------------------------------------------------------------------
1 | import defaultSchema from '../../../fixtures/defaultSchema'
2 | import type {BlockTestFn} from '../types'
3 |
4 | const blockContentType = defaultSchema
5 | .get('blogPost')
6 | .fields.find((field: any) => field.name === 'body').type
7 |
8 | const testFn: BlockTestFn = (html, blockTools, commonOptions) => {
9 | return blockTools.htmlToBlocks(html, blockContentType, {
10 | ...commonOptions,
11 | unstable_whitespaceOnPasteMode: 'remove',
12 | })
13 | }
14 |
15 | export default testFn
16 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-tests/decorators.test.ts:
--------------------------------------------------------------------------------
1 | import {Feature} from 'racejar/vitest'
2 | import decoratorsOverlappingFeature from '../gherkin-spec/decorators-overlapping.feature?raw'
3 | import decoratorsFeature from '../gherkin-spec/decorators.feature?raw'
4 | import {parameterTypes} from '../src/test'
5 | import {stepDefinitions} from '../src/test/vitest'
6 |
7 | Feature({
8 | featureText: decoratorsFeature,
9 | stepDefinitions,
10 | parameterTypes,
11 | })
12 |
13 | Feature({
14 | featureText: decoratorsOverlappingFeature,
15 | stepDefinitions,
16 | parameterTypes,
17 | })
18 |
--------------------------------------------------------------------------------
/apps/docs/src/components/editor/defaultSchema.ts:
--------------------------------------------------------------------------------
1 | import {defineSchema} from '@portabletext/editor'
2 |
3 | export const defaultSchema = defineSchema({
4 | decorators: [{name: 'strong'}, {name: 'em'}, {name: 'underline'}],
5 | annotations: [{name: 'link', fields: [{name: 'href', type: 'string'}]}],
6 | styles: [
7 | {name: 'normal'},
8 | {name: 'h1'},
9 | {name: 'h2'},
10 | {name: 'h3'},
11 | {name: 'blockquote'},
12 | ],
13 | lists: [{name: 'bullet'}, {name: 'number'}],
14 | inlineObjects: [{name: 'stock-ticker'}],
15 | blockObjects: [{name: 'image'}],
16 | })
17 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-spec/insert.text.feature:
--------------------------------------------------------------------------------
1 | Feature: Insert text
2 |
3 | Background:
4 | Given one editor
5 |
6 | Scenario Outline: Inserting text on expanded selection
7 | Given the text
8 | When is selected
9 | And "new" is typed
10 | Then the text is
11 |
12 | Examples:
13 | | text | selection | new text |
14 | | "foo\|bar" | "oo" | "fnew\|bar" |
15 | | "foo\|bar" | "b" | "foo\|newar" |
16 | | "foo\|bar" | "ooba" | "fnewr" |
17 | | "foo\|bar" | "foobar" | "new" |
18 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/move-range-by-operation.ts:
--------------------------------------------------------------------------------
1 | import {Point, type Operation, type Range} from 'slate'
2 |
3 | export function moveRangeByOperation(
4 | range: Range,
5 | operation: Operation,
6 | ): Range | null {
7 | const anchor = Point.transform(range.anchor, operation)
8 | const focus = Point.transform(range.focus, operation)
9 |
10 | if (anchor === null || focus === null) {
11 | return null
12 | }
13 |
14 | if (Point.equals(anchor, range.anchor) && Point.equals(focus, range.focus)) {
15 | return range
16 | }
17 |
18 | return {anchor, focus}
19 | }
20 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/selection-text.ts:
--------------------------------------------------------------------------------
1 | import {getTersePt} from '@portabletext/test'
2 | import type {EditorContext} from '../editor/editor-snapshot'
3 | import {sliceBlocks} from '../utils/util.slice-blocks'
4 |
5 | export function getSelectionText(
6 | context: Pick<
7 | EditorContext,
8 | 'keyGenerator' | 'schema' | 'value' | 'selection'
9 | >,
10 | ) {
11 | if (!context.selection) {
12 | return []
13 | }
14 |
15 | const slice = sliceBlocks({
16 | context,
17 | blocks: context.value,
18 | })
19 |
20 | return getTersePt({schema: context.schema, value: slice})
21 | }
22 |
--------------------------------------------------------------------------------
/packages/editor/src/plugins/plugin.internal.change-ref.tsx:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react'
2 | import {usePortableTextEditor} from '../editor/usePortableTextEditor'
3 | import type {EditorChange} from '../types/editor'
4 |
5 | export function InternalChange$Plugin(props: {
6 | onChange: (change: EditorChange) => void
7 | }) {
8 | const change$ = usePortableTextEditor().change$
9 |
10 | useEffect(() => {
11 | const subscription = change$.subscribe(props.onChange)
12 |
13 | return () => {
14 | subscription.unsubscribe()
15 | }
16 | }, [change$, props.onChange])
17 |
18 | return null
19 | }
20 |
--------------------------------------------------------------------------------
/apps/playground/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooks from 'eslint-plugin-react-hooks'
2 | import {globalIgnores} from 'eslint/config'
3 | import tseslint from 'typescript-eslint'
4 |
5 | export default tseslint.config([
6 | globalIgnores(['dist']),
7 | reactHooks.configs.flat.recommended,
8 | {
9 | files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
10 | languageOptions: {
11 | parser: tseslint.parser,
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | rules: {'react-hooks/exhaustive-deps': 'error'},
18 | },
19 | ])
20 |
--------------------------------------------------------------------------------
/packages/block-tools/src/HtmlDeserializer/preprocessors/index.ts:
--------------------------------------------------------------------------------
1 | import {preprocessWordOnline} from '../word-online/preprocessor.word-online'
2 | import {preprocessGDocs} from './preprocessor.gdocs'
3 | import {preprocessHTML} from './preprocessor.html'
4 | import {preprocessNotion} from './preprocessor.notion'
5 | import {preprocessWhitespace} from './preprocessor.whitespace'
6 | import {preprocessWord} from './preprocessor.word'
7 |
8 | export const preprocessors = [
9 | preprocessWhitespace,
10 | preprocessNotion,
11 | preprocessWord,
12 | preprocessWordOnline,
13 | preprocessGDocs,
14 | preprocessHTML,
15 | ]
16 |
--------------------------------------------------------------------------------
/packages/block-tools/test/html-to-blocks/nested-containers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Header
5 |
6 | I'm a quote.
7 |
8 | And I'm a quote within a quote.
9 |
10 | And I'm a quote within a quote within a quote.
11 |
12 | And I'm a quote within a quote within a quote within a quote.
13 | I am a stupid paragraph within with a stupid link
14 |
15 |
16 |
17 |
18 | Footer
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/whitespaceInPreTags/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | # 1. Clone the repository:
5 | git clone ...
6 | # 2. Navigate to the client directory:
7 | cd ...
8 | # 3. Create a new Python virtual environment:
9 | python3 -m venv ...
10 |
11 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/packages/editor/src/plugins/plugin.internal.slate-editor-ref.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {useSlateStatic} from 'slate-react'
3 | import type {PortableTextSlateEditor} from '../types/slate-editor'
4 |
5 | export const InternalSlateEditorRefPlugin =
6 | React.forwardRef((_, ref) => {
7 | const slateEditor = useSlateStatic()
8 |
9 | const slateEditorRef = React.useRef(slateEditor)
10 |
11 | React.useImperativeHandle(ref, () => slateEditorRef.current, [])
12 |
13 | return null
14 | })
15 | InternalSlateEditorRefPlugin.displayName = 'InternalSlateEditorRefPlugin'
16 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/field.text.tsx:
--------------------------------------------------------------------------------
1 | import {TextField as RACTextField} from 'react-aria-components'
2 | import {Input, Label} from './field'
3 |
4 | export function TextField(props: {
5 | name: string
6 | label?: string
7 | autoFocus?: boolean
8 | defaultValue?: string
9 | }) {
10 | return (
11 |
17 |
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/packages/editor/src/priority/priority.types.ts:
--------------------------------------------------------------------------------
1 | import {defaultKeyGenerator} from '../utils/key-generator'
2 |
3 | export type EditorPriority = {
4 | id: string
5 | name?: string
6 | reference?: {
7 | priority: EditorPriority
8 | importance: 'higher' | 'lower'
9 | }
10 | }
11 |
12 | export function createEditorPriority(config?: {
13 | name?: string
14 | reference?: {
15 | priority: EditorPriority
16 | importance: 'higher' | 'lower'
17 | }
18 | }): EditorPriority {
19 | return {
20 | id: defaultKeyGenerator(),
21 | name: config?.name,
22 | reference: config?.reference,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/sanity-bridge/src/start-case.ts:
--------------------------------------------------------------------------------
1 | export function startCase(str: string): string {
2 | return (
3 | str
4 | // Insert space before uppercase letters in camelCase (e.g., 'fooBar' -> 'foo Bar')
5 | .replace(/([a-z])([A-Z])/g, '$1 $2')
6 | // Replace underscores and dashes with spaces
7 | .replace(/[_-]+/g, ' ')
8 | // Trim and split on whitespace
9 | .trim()
10 | .split(/\s+/)
11 | .filter(Boolean)
12 | // Capitalize first letter of each word, preserve rest
13 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
14 | .join(' ')
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/field.number.tsx:
--------------------------------------------------------------------------------
1 | import {NumberField as RACNumberField} from 'react-aria-components'
2 | import {Input, Label} from './field'
3 |
4 | export function NumberField(props: {
5 | name: string
6 | label?: string
7 | autoFocus?: boolean
8 | defaultValue?: number
9 | }) {
10 | return (
11 |
17 |
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-last-block.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextBlock} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {BlockPath} from '../types/paths'
4 |
5 | /**
6 | * @public
7 | */
8 | export const getLastBlock: EditorSelector<
9 | {node: PortableTextBlock; path: BlockPath} | undefined
10 | > = (snapshot) => {
11 | const node = snapshot.context.value[snapshot.context.value.length - 1]
12 | ? snapshot.context.value[snapshot.context.value.length - 1]
13 | : undefined
14 |
15 | return node ? {node, path: [{_key: node._key}]} : undefined
16 | }
17 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-spec/annotations-collaboration.feature:
--------------------------------------------------------------------------------
1 | Feature: Annotations Collaboration
2 |
3 | Background:
4 | Given two editors
5 | And a global keymap
6 |
7 | Scenario: Editor B inserts text after Editor A's half-deleted annotation
8 | When "foo" is typed
9 | And "foo" is selected
10 | And "comment" "c1" is toggled
11 | And the caret is put after "foo"
12 | And "{Backspace}" is pressed
13 | And Editor B is focused
14 | And the caret is put after "fo" in Editor B
15 | And "a" is typed in Editor B
16 | Then the text is "fo,a"
17 | And "fo" has marks "c1"
18 | And "a" has no marks
19 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.is-selection-collapsed.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelector} from '../editor/editor-selector'
2 | import {isEqualPaths} from '../utils/util.is-equal-paths'
3 |
4 | /**
5 | * @public
6 | */
7 | export const isSelectionCollapsed: EditorSelector = (snapshot) => {
8 | if (!snapshot.context.selection) {
9 | return false
10 | }
11 |
12 | return (
13 | isEqualPaths(
14 | snapshot.context.selection.anchor.path,
15 | snapshot.context.selection.focus.path,
16 | ) &&
17 | snapshot.context.selection.anchor.offset ===
18 | snapshot.context.selection.focus.offset
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/packages/plugin-one-line/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooks from 'eslint-plugin-react-hooks'
2 | import {globalIgnores} from 'eslint/config'
3 | import tseslint from 'typescript-eslint'
4 |
5 | export default tseslint.config([
6 | globalIgnores(['dist', '*.test.tsx']),
7 | reactHooks.configs.flat.recommended,
8 | {
9 | files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
10 | languageOptions: {
11 | parser: tseslint.parser,
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | rules: {'react-hooks/exhaustive-deps': 'error'},
18 | },
19 | ])
20 |
--------------------------------------------------------------------------------
/packages/plugin-sdk-value/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooks from 'eslint-plugin-react-hooks'
2 | import {globalIgnores} from 'eslint/config'
3 | import tseslint from 'typescript-eslint'
4 |
5 | export default tseslint.config([
6 | globalIgnores(['dist', '*.test.tsx']),
7 | reactHooks.configs.flat.recommended,
8 | {
9 | files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
10 | languageOptions: {
11 | parser: tseslint.parser,
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | rules: {'react-hooks/exhaustive-deps': 'error'},
18 | },
19 | ])
20 |
--------------------------------------------------------------------------------
/apps/docs/src/content/docs/reference/toolbar.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Toolbar API Overview
3 | description: Toolbar reference
4 | prev: false
5 | next: false
6 | ---
7 |
8 | import {CardGrid, LinkCard} from '@astrojs/starlight/components'
9 | import {PackageManagers} from 'starlight-package-managers'
10 |
11 | The Toolbar API offers assorted hooks and types for building your own toolbars.
12 |
13 |
14 |
15 |
16 |
21 |
22 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/toggle-button.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | composeRenderProps,
3 | ToggleButton as RACToggleButton,
4 | type ToggleButtonProps,
5 | } from 'react-aria-components'
6 | import {button} from './button'
7 |
8 | export function ToggleButton(props: ToggleButtonProps & {size?: 'sm'}) {
9 | return (
10 |
13 | button({
14 | ...renderProps,
15 | size: props.size,
16 | className,
17 | variant: 'secondary',
18 | }),
19 | )}
20 | />
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/packages/toolbar/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooks from 'eslint-plugin-react-hooks'
2 | import {globalIgnores} from 'eslint/config'
3 | import tseslint from 'typescript-eslint'
4 |
5 | export default tseslint.config([
6 | globalIgnores(['dist', 'lib', '**/__tests__/**']),
7 | reactHooks.configs.flat.recommended,
8 | {
9 | files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
10 | languageOptions: {
11 | parser: tseslint.parser,
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | rules: {'react-hooks/exhaustive-deps': 'error'},
18 | },
19 | ])
20 |
--------------------------------------------------------------------------------
/packages/plugin-emoji-picker/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooks from 'eslint-plugin-react-hooks'
2 | import {globalIgnores} from 'eslint/config'
3 | import tseslint from 'typescript-eslint'
4 |
5 | export default tseslint.config([
6 | globalIgnores(['dist', 'lib', '**/*.test.*']),
7 | reactHooks.configs.flat.recommended,
8 | {
9 | files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
10 | languageOptions: {
11 | parser: tseslint.parser,
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | rules: {'react-hooks/exhaustive-deps': 'error'},
18 | },
19 | ])
20 |
--------------------------------------------------------------------------------
/packages/plugin-input-rule/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooks from 'eslint-plugin-react-hooks'
2 | import {globalIgnores} from 'eslint/config'
3 | import tseslint from 'typescript-eslint'
4 |
5 | export default tseslint.config([
6 | globalIgnores(['dist', 'lib', '**/*.test.*']),
7 | reactHooks.configs.flat.recommended,
8 | {
9 | files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
10 | languageOptions: {
11 | parser: tseslint.parser,
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | rules: {'react-hooks/exhaustive-deps': 'error'},
18 | },
19 | ])
20 |
--------------------------------------------------------------------------------
/packages/plugin-markdown-shortcuts/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooks from 'eslint-plugin-react-hooks'
2 | import {globalIgnores} from 'eslint/config'
3 | import tseslint from 'typescript-eslint'
4 |
5 | export default tseslint.config([
6 | globalIgnores(['dist', '**/*.test.tsx']),
7 | reactHooks.configs.flat.recommended,
8 | {
9 | files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
10 | languageOptions: {
11 | parser: tseslint.parser,
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | rules: {'react-hooks/exhaustive-deps': 'error'},
18 | },
19 | ])
20 |
--------------------------------------------------------------------------------
/packages/plugin-typography/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooks from 'eslint-plugin-react-hooks'
2 | import {globalIgnores} from 'eslint/config'
3 | import tseslint from 'typescript-eslint'
4 |
5 | export default tseslint.config([
6 | globalIgnores(['dist', 'lib', '**/*.test.*']),
7 | reactHooks.configs.flat.recommended,
8 | {
9 | files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
10 | languageOptions: {
11 | parser: tseslint.parser,
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | rules: {'react-hooks/exhaustive-deps': 'error'},
18 | },
19 | ])
20 |
--------------------------------------------------------------------------------
/packages/plugin-character-pair-decorator/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooks from 'eslint-plugin-react-hooks'
2 | import {globalIgnores} from 'eslint/config'
3 | import tseslint from 'typescript-eslint'
4 |
5 | export default tseslint.config([
6 | globalIgnores(['dist', '*.test.tsx']),
7 | reactHooks.configs.flat.recommended,
8 | {
9 | files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
10 | languageOptions: {
11 | parser: tseslint.parser,
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | rules: {'react-hooks/exhaustive-deps': 'error'},
18 | },
19 | ])
20 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/render.default-object.tsx:
--------------------------------------------------------------------------------
1 | import type {PortableTextChild, PortableTextObject} from '@portabletext/schema'
2 |
3 | export function RenderDefaultBlockObject(props: {
4 | blockObject: PortableTextObject
5 | }) {
6 | return (
7 |
8 | [{props.blockObject._type}: {props.blockObject._key}]
9 |
10 | )
11 | }
12 |
13 | export function RenderDefaultInlineObject(props: {
14 | inlineObject: PortableTextObject | PortableTextChild
15 | }) {
16 | return (
17 |
18 | [{props.inlineObject._type}: {props.inlineObject._key}]
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/text-marks.ts:
--------------------------------------------------------------------------------
1 | import {isSpan, isTextBlock} from '@portabletext/schema'
2 | import type {EditorContext} from '../editor/editor-snapshot'
3 |
4 | export function getTextMarks(
5 | context: Pick,
6 | text: string,
7 | ) {
8 | let marks: Array = []
9 |
10 | for (const block of context.value) {
11 | if (isTextBlock(context, block)) {
12 | for (const child of block.children) {
13 | if (isSpan(context, child) && child.text === text) {
14 | marks = child.marks ?? []
15 | break
16 | }
17 | }
18 | }
19 | }
20 |
21 | return marks
22 | }
23 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-focus-span.ts:
--------------------------------------------------------------------------------
1 | import {isSpan, type PortableTextSpan} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {ChildPath} from '../types/paths'
4 | import {getFocusChild} from './selector.get-focus-child'
5 |
6 | /**
7 | * @public
8 | */
9 | export const getFocusSpan: EditorSelector<
10 | {node: PortableTextSpan; path: ChildPath} | undefined
11 | > = (snapshot) => {
12 | const focusChild = getFocusChild(snapshot)
13 |
14 | return focusChild && isSpan(snapshot.context, focusChild.node)
15 | ? {node: focusChild.node, path: focusChild.path}
16 | : undefined
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor/eslint.config.js:
--------------------------------------------------------------------------------
1 | import reactHooks from 'eslint-plugin-react-hooks'
2 | import {globalIgnores} from 'eslint/config'
3 | import tseslint from 'typescript-eslint'
4 |
5 | export default tseslint.config([
6 | globalIgnores(['coverage', 'dist', 'lib', '**/__tests__/**']),
7 | reactHooks.configs.flat.recommended,
8 | {
9 | files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
10 | languageOptions: {
11 | parser: tseslint.parser,
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | rules: {
18 | 'react-hooks/exhaustive-deps': 'error',
19 | },
20 | },
21 | ])
22 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-anchor-span.ts:
--------------------------------------------------------------------------------
1 | import {isSpan, type PortableTextSpan} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {ChildPath} from '../types/paths'
4 | import {getAnchorChild} from './selector.get-anchor-child'
5 |
6 | /**
7 | * @public
8 | */
9 | export const getAnchorSpan: EditorSelector<
10 | {node: PortableTextSpan; path: ChildPath} | undefined
11 | > = (snapshot) => {
12 | const anchorChild = getAnchorChild(snapshot)
13 |
14 | return anchorChild && isSpan(snapshot.context, anchorChild.node)
15 | ? {node: anchorChild.node, path: anchorChild.path}
16 | : undefined
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/selection-block-keys.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelection} from '../types/editor'
2 | import {getBlockKeyFromSelectionPoint} from '../utils/util.selection-point'
3 |
4 | export function getSelectionBlockKeys(selection: EditorSelection) {
5 | if (!selection) {
6 | return undefined
7 | }
8 |
9 | const anchorBlockKey = getBlockKeyFromSelectionPoint(selection.anchor)
10 | const focusBlockKey = getBlockKeyFromSelectionPoint(selection.focus)
11 |
12 | if (anchorBlockKey === undefined || focusBlockKey === undefined) {
13 | return undefined
14 | }
15 |
16 | return {
17 | anchor: anchorBlockKey,
18 | focus: focusBlockKey,
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-focus-inline-object.ts:
--------------------------------------------------------------------------------
1 | import {isSpan, type PortableTextObject} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {ChildPath} from '../types/paths'
4 | import {getFocusChild} from './selector.get-focus-child'
5 |
6 | /**
7 | * @public
8 | */
9 | export const getFocusInlineObject: EditorSelector<
10 | {node: PortableTextObject; path: ChildPath} | undefined
11 | > = (snapshot) => {
12 | const focusChild = getFocusChild(snapshot)
13 |
14 | return focusChild && !isSpan(snapshot.context, focusChild.node)
15 | ? {node: focusChild.node, path: focusChild.path}
16 | : undefined
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.is-empty-text-block.ts:
--------------------------------------------------------------------------------
1 | import {isSpan, isTextBlock, type PortableTextBlock} from '@portabletext/schema'
2 | import type {EditorContext} from '../editor/editor-snapshot'
3 | import {getTextBlockText} from './util.get-text-block-text'
4 |
5 | /**
6 | * @public
7 | */
8 | export function isEmptyTextBlock(
9 | context: Pick,
10 | block: PortableTextBlock | unknown,
11 | ) {
12 | if (!isTextBlock(context, block)) {
13 | return false
14 | }
15 |
16 | const onlyText = block.children.every((child) => isSpan(context, child))
17 | const blockText = getTextBlockText(block)
18 |
19 | return onlyText && blockText === ''
20 | }
21 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/utils.ts:
--------------------------------------------------------------------------------
1 | import {composeRenderProps} from 'react-aria-components'
2 | import {twMerge} from 'tailwind-merge'
3 | import {tv} from 'tailwind-variants'
4 |
5 | export const focusRing = tv({
6 | base: 'outline outline-blue-600 forced-colors:outline-[Highlight] outline-offset-2',
7 | variants: {
8 | isFocusVisible: {
9 | false: 'outline-0',
10 | true: 'outline-2',
11 | },
12 | },
13 | })
14 |
15 | export function composeTailwindRenderProps(
16 | className: string | ((v: T) => string) | undefined,
17 | tw: string,
18 | ): string | ((v: T) => string) {
19 | return composeRenderProps(className, (className) => twMerge(tw, className))
20 | }
21 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/string-overlap.test.ts:
--------------------------------------------------------------------------------
1 | import {expect, test} from 'vitest'
2 | import {stringOverlap} from './string-overlap'
3 |
4 | test(stringOverlap.name, () => {
5 | expect(stringOverlap('', 'foobar')).toBe('')
6 | expect(stringOverlap('foo ', 'o bar b')).toBe('o ')
7 | expect(stringOverlap('foo', 'o')).toBe('o')
8 | expect(stringOverlap('foo bar baz', 'o')).toBe('o')
9 | expect(stringOverlap('bar', 'o bar b')).toBe('bar')
10 | expect(stringOverlap(' baz', 'o bar b')).toBe(' b')
11 | expect(stringOverlap('fofofo', 'fo')).toBe('fo')
12 | expect(stringOverlap('fofofo', 'fof')).toBe('fof')
13 | expect(stringOverlap('fofofo', 'fofofof')).toBe('fofofo')
14 | })
15 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.reverse-selection.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelection} from '../types/editor'
2 |
3 | /**
4 | * @public
5 | */
6 | export function reverseSelection<
7 | TEditorSelection extends NonNullable | null,
8 | >(selection: TEditorSelection): TEditorSelection {
9 | if (!selection) {
10 | return selection
11 | }
12 |
13 | if (selection.backward) {
14 | return {
15 | anchor: selection.focus,
16 | focus: selection.anchor,
17 | backward: false,
18 | } as TEditorSelection
19 | }
20 |
21 | return {
22 | anchor: selection.focus,
23 | focus: selection.anchor,
24 | backward: true,
25 | } as TEditorSelection
26 | }
27 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.selection-point.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelectionPoint} from '../types/editor'
2 | import {isKeyedSegment} from './util.is-keyed-segment'
3 |
4 | export function getBlockKeyFromSelectionPoint(point: EditorSelectionPoint) {
5 | const blockPathSegment = point.path.at(0)
6 |
7 | if (isKeyedSegment(blockPathSegment)) {
8 | return blockPathSegment._key
9 | }
10 |
11 | return undefined
12 | }
13 |
14 | export function getChildKeyFromSelectionPoint(point: EditorSelectionPoint) {
15 | const childPathSegment = point.path.at(2)
16 |
17 | if (isKeyedSegment(childPathSegment)) {
18 | return childPathSegment._key
19 | }
20 |
21 | return undefined
22 | }
23 |
--------------------------------------------------------------------------------
/packages/editor/src/plugins/plugin.behavior.tsx:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react'
2 | import type {Behavior} from '../behaviors/behavior.types.behavior'
3 | import {useEditor} from '../editor/use-editor'
4 |
5 | /**
6 | * @beta
7 | */
8 | export function BehaviorPlugin(props: {behaviors: Array}) {
9 | const editor = useEditor()
10 |
11 | useEffect(() => {
12 | const unregisterBehaviors = props.behaviors.map((behavior) =>
13 | editor.registerBehavior({behavior}),
14 | )
15 |
16 | return () => {
17 | unregisterBehaviors.forEach((unregister) => {
18 | unregister()
19 | })
20 | }
21 | }, [editor, props.behaviors])
22 |
23 | return null
24 | }
25 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-focus-block-object.ts:
--------------------------------------------------------------------------------
1 | import {isTextBlock, type PortableTextObject} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {BlockPath} from '../types/paths'
4 | import {getFocusBlock} from './selector.get-focus-block'
5 |
6 | /**
7 | * @public
8 | */
9 | export const getFocusBlockObject: EditorSelector<
10 | {node: PortableTextObject; path: BlockPath} | undefined
11 | > = (snapshot) => {
12 | const focusBlock = getFocusBlock(snapshot)
13 |
14 | return focusBlock && !isTextBlock(snapshot.context, focusBlock.node)
15 | ? {node: focusBlock.node, path: focusBlock.path}
16 | : undefined
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-focus-text-block.ts:
--------------------------------------------------------------------------------
1 | import {isTextBlock, type PortableTextTextBlock} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {BlockPath} from '../types/paths'
4 | import {getFocusBlock} from './selector.get-focus-block'
5 |
6 | /**
7 | * @public
8 | */
9 | export const getFocusTextBlock: EditorSelector<
10 | {node: PortableTextTextBlock; path: BlockPath} | undefined
11 | > = (snapshot) => {
12 | const focusBlock = getFocusBlock(snapshot)
13 |
14 | return focusBlock && isTextBlock(snapshot.context, focusBlock.node)
15 | ? {node: focusBlock.node, path: focusBlock.path}
16 | : undefined
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-selected-spans.ts:
--------------------------------------------------------------------------------
1 | import {isSpan, type PortableTextSpan} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {ChildPath} from '../types/paths'
4 | import {getSelectedChildren} from './selector.get-selected-children'
5 |
6 | /**
7 | * @public
8 | */
9 | export const getSelectedSpans: EditorSelector<
10 | Array<{
11 | node: PortableTextSpan
12 | path: ChildPath
13 | }>
14 | > = (snapshot) => {
15 | if (!snapshot.context.selection) {
16 | return []
17 | }
18 |
19 | return getSelectedChildren({
20 | filter: (child) => isSpan(snapshot.context, child),
21 | })(snapshot)
22 | }
23 |
--------------------------------------------------------------------------------
/packages/markdown/src/escape.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Escapes special characters in image alt texts and link texts.
3 | */
4 | export function escapeImageAndLinkText(text: string): string {
5 | return text.replace(/([[\]\\])/g, '\\$1')
6 | }
7 |
8 | /**
9 | * Unescapes special characters in image alt texts and link texts.
10 | */
11 | export function unescapeImageAndLinkText(text: string): string {
12 | return text.replace(/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g, '$1')
13 | }
14 |
15 | /**
16 | * Escapes special characters in image/link titles (the part inside quotes).
17 | */
18 | export function escapeImageAndLinkTitle(text: string): string {
19 | return text.replace(/([\\"])/g, '\\$1')
20 | }
21 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/create-placeholder-block.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextSpan} from '@portabletext/schema'
2 | import type {EditorContext} from '../editor/editor-snapshot'
3 |
4 | export function createPlaceholderBlock(
5 | context: Pick,
6 | ) {
7 | return {
8 | _type: context.schema.block.name,
9 | _key: context.keyGenerator(),
10 | style: context.schema.styles[0].name ?? 'normal',
11 | markDefs: [],
12 | children: [
13 | {
14 | _type: context.schema.span.name,
15 | _key: context.keyGenerator(),
16 | text: '',
17 | marks: [],
18 | } as PortableTextSpan,
19 | ],
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-anchor-text-block.ts:
--------------------------------------------------------------------------------
1 | import {isTextBlock, type PortableTextTextBlock} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {BlockPath} from '../types/paths'
4 | import {getAnchorBlock} from './selector.get-anchor-block'
5 |
6 | /**
7 | * @public
8 | */
9 | export const getAnchorTextBlock: EditorSelector<
10 | {node: PortableTextTextBlock; path: BlockPath} | undefined
11 | > = (snapshot) => {
12 | const anchorBlock = getAnchorBlock(snapshot)
13 |
14 | return anchorBlock && isTextBlock(snapshot.context, anchorBlock.node)
15 | ? {node: anchorBlock.node, path: anchorBlock.path}
16 | : undefined
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor/src/plugins/plugin.internal.portable-text-editor-ref.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type {PortableTextEditor} from '../editor/PortableTextEditor'
3 | import {usePortableTextEditor} from '../editor/usePortableTextEditor'
4 |
5 | export const InternalPortableTextEditorRefPlugin =
6 | React.forwardRef((_, ref) => {
7 | const portableTextEditor = usePortableTextEditor()
8 |
9 | const portableTextEditorRef = React.useRef(portableTextEditor)
10 |
11 | React.useImperativeHandle(ref, () => portableTextEditorRef.current, [])
12 |
13 | return null
14 | })
15 | InternalPortableTextEditorRefPlugin.displayName =
16 | 'InternalPortableTextEditorRefPlugin'
17 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.get-selection-end-point.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelection, EditorSelectionPoint} from '../types/editor'
2 |
3 | /**
4 | * @public
5 | */
6 | export function getSelectionEndPoint<
7 | TEditorSelection extends NonNullable | null,
8 | TEditorSelectionPoint extends EditorSelectionPoint | null =
9 | TEditorSelection extends NonNullable
10 | ? EditorSelectionPoint
11 | : null,
12 | >(selection: TEditorSelection): TEditorSelectionPoint {
13 | if (!selection) {
14 | return null as TEditorSelectionPoint
15 | }
16 |
17 | return (
18 | selection.backward ? selection.anchor : selection.focus
19 | ) as TEditorSelectionPoint
20 | }
21 |
--------------------------------------------------------------------------------
/examples/basic/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "Bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/examples/legacy/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "Bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.get-selection-start-point.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelection, EditorSelectionPoint} from '../types/editor'
2 |
3 | /**
4 | * @public
5 | */
6 | export function getSelectionStartPoint<
7 | TEditorSelection extends NonNullable | null,
8 | TEditorSelectionPoint extends EditorSelectionPoint | null =
9 | TEditorSelection extends NonNullable
10 | ? EditorSelectionPoint
11 | : null,
12 | >(selection: TEditorSelection): TEditorSelectionPoint {
13 | if (!selection) {
14 | return null as TEditorSelectionPoint
15 | }
16 |
17 | return (
18 | selection.backward ? selection.focus : selection.anchor
19 | ) as TEditorSelectionPoint
20 | }
21 |
--------------------------------------------------------------------------------
/AGENTS.md:
--------------------------------------------------------------------------------
1 | # Bash commands
2 |
3 | - pnpm build - Build package
4 | - pnpm check:types - Typecheck package
5 | - pnpm test:unit - Run package unit tests
6 | - pnpm test:browser - Run package browser tests
7 | - pnpm test:browser:chromium - Run specific browser tests
8 | - pnpm check:lint - Lint package
9 |
10 | # Code style
11 |
12 | - Avoid code comments unless they explain **why** a piece of code is needed
13 | - Comments for an if statement go inside the if statement, not above it
14 | - Place helper functions below main functions
15 | - Only use type casting as a last resort
16 | - Use backticks when referencing code in comments and other text
17 | - Don't use one-character variable names
18 | - Use full, easily-understood variable names
19 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/separator.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Separator as RACSeparator,
3 | type SeparatorProps,
4 | } from 'react-aria-components'
5 | import {tv} from 'tailwind-variants'
6 |
7 | const styles = tv({
8 | base: 'bg-gray-300 text-gray-300',
9 | variants: {
10 | orientation: {
11 | horizontal: 'h-px w-full',
12 | vertical: 'w-px',
13 | },
14 | },
15 | defaultVariants: {
16 | orientation: 'horizontal',
17 | },
18 | })
19 |
20 | export function Separator(props: SeparatorProps) {
21 | return (
22 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/packages/editor/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | define: {
5 | __DEV__: false,
6 | },
7 | dist: 'lib',
8 | extract: {
9 | customTags: [
10 | {
11 | name: 'group',
12 | allowMultiple: true,
13 | syntaxKind: 'block',
14 | },
15 | ],
16 | rules: {
17 | // Disable rules for now
18 | 'ae-incompatible-release-tags': 'off',
19 | },
20 | },
21 | tsconfig: 'tsconfig.dist.json',
22 | strictOptions: {
23 | noImplicitBrowsersList: 'off',
24 | noImplicitSideEffects: 'error',
25 | },
26 | babel: {reactCompiler: true},
27 | reactCompilerOptions: {target: '19'},
28 | dts: 'rolldown',
29 | })
30 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.is-point-after-selection.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelector} from '../editor/editor-selector'
2 | import type {EditorSelectionPoint} from '../types/editor'
3 | import {comparePoints} from '../utils/util.compare-points'
4 | import {getSelectionEndPoint} from '../utils/util.get-selection-end-point'
5 |
6 | /**
7 | * @public
8 | */
9 | export function isPointAfterSelection(
10 | point: EditorSelectionPoint,
11 | ): EditorSelector {
12 | return (snapshot) => {
13 | if (!snapshot.context.selection) {
14 | return false
15 | }
16 |
17 | const endPoint = getSelectionEndPoint(snapshot.context.selection)
18 |
19 | return comparePoints(snapshot, point, endPoint) === 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/racejar/src/create-parameter-type.ts:
--------------------------------------------------------------------------------
1 | import {ParameterType, type RegExps} from '@cucumber/cucumber-expressions'
2 |
3 | /**
4 | * @public
5 | */
6 | export type ParameterTypeConfig = {
7 | readonly name: string
8 | matcher: RegExps
9 | type?: (...args: unknown[]) => TType
10 | transform?: (...match: string[]) => TType
11 | }
12 |
13 | export type {ParameterType}
14 |
15 | /**
16 | * @public
17 | */
18 | export function createParameterType(
19 | config: ParameterTypeConfig,
20 | ): ParameterType {
21 | return new ParameterType(
22 | config.name,
23 | config.matcher,
24 | config.type ?? String,
25 | config.transform,
26 | false,
27 | true,
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/packages/block-tools/test/html-to-blocks/lists.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 | 1 a
6 |
7 | -
8 | 2 a
9 |
13 |
14 | -
15 | 2 b
16 |
17 |
18 |
19 | -
20 | 1 b
21 |
22 | -
23 | Link
24 |
25 | -
26 |
p in li.
27 | block children are still processed.
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/packages/editor/src/converters/converters.core.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextMemberSchemaTypes} from '../types/editor'
2 | import {converterJson} from './converter.json'
3 | import {converterPortableText} from './converter.portable-text'
4 | import {createConverterTextHtml} from './converter.text-html'
5 | import {converterTextMarkdown} from './converter.text-markdown'
6 | import {createConverterTextPlain} from './converter.text-plain'
7 |
8 | export function createCoreConverters(
9 | legacySchema: PortableTextMemberSchemaTypes,
10 | ) {
11 | return [
12 | converterJson,
13 | converterPortableText,
14 | converterTextMarkdown,
15 | createConverterTextHtml(legacySchema),
16 | createConverterTextPlain(legacySchema),
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/use-editor.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {EditorContext} from './editor-context'
3 |
4 | /**
5 | * @public
6 | * Get the current editor context from the `EditorProvider`.
7 | * Must be used inside the `EditorProvider` component.
8 | * @returns The current editor object.
9 | * @example
10 | * ```tsx
11 | * import { useEditor } from '@portabletext/editor'
12 | *
13 | * function MyComponent() {
14 | * const editor = useEditor()
15 | * }
16 | * ```
17 | * @group Hooks
18 | */
19 | export function useEditor() {
20 | const editor = React.useContext(EditorContext)
21 |
22 | if (!editor) {
23 | throw new Error('No Editor set. Use EditorProvider to set one.')
24 | }
25 |
26 | return editor
27 | }
28 |
--------------------------------------------------------------------------------
/packages/block-tools/test/html-to-blocks/from-the-wild-5.html:
--------------------------------------------------------------------------------
1 | TRANSFORM is currently supporting over
2 | 45 projects across 11 countries,
3 | which have reached more than a million people so far.
4 |
5 | Unlocking the power of markets
6 |
7 |
8 | TRANSFORM brings together the public and private sectors to address the world's most pressing development challenges
9 | Visit the TRANSFORM website to discover more
10 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.is-point-before-selection.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelector} from '../editor/editor-selector'
2 | import type {EditorSelectionPoint} from '../types/editor'
3 | import {comparePoints} from '../utils/util.compare-points'
4 | import {getSelectionStartPoint} from '../utils/util.get-selection-start-point'
5 |
6 | /**
7 | * @public
8 | */
9 | export function isPointBeforeSelection(
10 | point: EditorSelectionPoint,
11 | ): EditorSelector {
12 | return (snapshot) => {
13 | if (!snapshot.context.selection) {
14 | return false
15 | }
16 |
17 | const startPoint = getSelectionStartPoint(snapshot.context.selection)
18 |
19 | return comparePoints(snapshot, point, startPoint) === -1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/plugin-input-rule/src/emoji-picker-rules.feature:
--------------------------------------------------------------------------------
1 | Feature: Emoji Picker Rules
2 |
3 | Scenario: Trigger Rule
4 | Given the text ""
5 | When ":" is typed
6 | Then the keyword is ""
7 |
8 | Scenario: Partial Keyword Rule
9 | Given the text ""
10 | When ":jo" is typed
11 | Then the keyword is "jo"
12 |
13 | Scenario: Keyword Rule
14 | Given the text ""
15 | When ":joy:" is typed
16 | Then the keyword is "joy"
17 |
18 | Scenario Outline: Consecutive keywords
19 | Given the text ":joy:"
20 | When is typed
21 | Then the keyword is
22 |
23 | Examples:
24 | | text | keyword |
25 | | ":" | "" |
26 | | ":cat" | "cat" |
27 | | ":cat:" | "cat" |
28 |
--------------------------------------------------------------------------------
/apps/docs/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import {cn} from '@/lib/utils'
2 | import * as React from 'react'
3 |
4 | const Textarea = React.forwardRef<
5 | HTMLTextAreaElement,
6 | React.ComponentProps<'textarea'>
7 | >(({className, ...props}, ref) => {
8 | return (
9 |
17 | )
18 | })
19 | Textarea.displayName = 'Textarea'
20 |
21 | export {Textarea}
22 |
--------------------------------------------------------------------------------
/apps/playground/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5 | "target": "ES2020",
6 | "useDefineForClassFields": true,
7 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
8 | "module": "ESNext",
9 | "skipLibCheck": true,
10 |
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "moduleDetection": "force",
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 |
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true
23 | },
24 | "include": ["src"]
25 | }
26 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-focus-list-block.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextListBlock} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {BlockPath} from '../types/paths'
4 | import {isListBlock} from '../utils/parse-blocks'
5 | import {getFocusTextBlock} from './selector.get-focus-text-block'
6 |
7 | /**
8 | * @public
9 | */
10 | export const getFocusListBlock: EditorSelector<
11 | {node: PortableTextListBlock; path: BlockPath} | undefined
12 | > = (snapshot) => {
13 | const focusTextBlock = getFocusTextBlock(snapshot)
14 |
15 | return focusTextBlock && isListBlock(snapshot.context, focusTextBlock.node)
16 | ? {node: focusTextBlock.node, path: focusTextBlock.path}
17 | : undefined
18 | }
19 |
--------------------------------------------------------------------------------
/packages/block-tools/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | define: {
5 | __DEV__: false,
6 | },
7 | dist: 'lib',
8 | extract: {
9 | customTags: [
10 | {
11 | name: 'hidden',
12 | allowMultiple: true,
13 | syntaxKind: 'block',
14 | },
15 | {
16 | name: 'todo',
17 | allowMultiple: true,
18 | syntaxKind: 'block',
19 | },
20 | ],
21 | rules: {
22 | // Disable rules for now
23 | 'ae-incompatible-release-tags': 'off',
24 | },
25 | },
26 | tsconfig: 'tsconfig.dist.json',
27 | strictOptions: {
28 | noImplicitBrowsersList: 'off',
29 | noImplicitSideEffects: 'error',
30 | },
31 | dts: 'rolldown',
32 | })
33 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/text-block-key.ts:
--------------------------------------------------------------------------------
1 | import {isSpan, isTextBlock} from '@portabletext/schema'
2 | import type {EditorContext} from '../editor/editor-snapshot'
3 |
4 | export function getTextBlockKey(
5 | context: Pick,
6 | text: string,
7 | ) {
8 | let blockKey: string | undefined
9 |
10 | for (const block of context.value) {
11 | if (isTextBlock(context, block)) {
12 | for (const child of block.children) {
13 | if (isSpan(context, child) && child.text === text) {
14 | blockKey = block._key
15 | break
16 | }
17 | }
18 | }
19 | }
20 |
21 | if (!blockKey) {
22 | throw new Error(`Unable to find block key for text "${text}"`)
23 | }
24 |
25 | return blockKey
26 | }
27 |
--------------------------------------------------------------------------------
/packages/editor/src/types/block-with-optional-key.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | PortableTextObject,
3 | PortableTextSpan,
4 | PortableTextTextBlock,
5 | } from '@portabletext/schema'
6 |
7 | export type TextBlockWithOptionalKey = Omit & {
8 | _key?: PortableTextTextBlock['_key']
9 | }
10 |
11 | export type ObjectBlockWithOptionalKey = Omit & {
12 | _key?: PortableTextObject['_key']
13 | }
14 |
15 | export type BlockWithOptionalKey =
16 | | TextBlockWithOptionalKey
17 | | ObjectBlockWithOptionalKey
18 |
19 | export type SpanWithOptionalKey = Omit & {
20 | _key?: PortableTextSpan['_key']
21 | }
22 |
23 | export type ChildWithOptionalKey =
24 | | SpanWithOptionalKey
25 | | ObjectBlockWithOptionalKey
26 |
--------------------------------------------------------------------------------
/examples/basic/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "Bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src"]
26 | }
27 |
--------------------------------------------------------------------------------
/examples/basic/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 | import react from '@vitejs/plugin-react'
3 | import {defineConfig} from 'vite'
4 |
5 | // https://vite.dev/config/
6 | export default defineConfig({
7 | plugins: [
8 | react({
9 | babel: {plugins: [['babel-plugin-react-compiler', {target: '19'}]]},
10 | }),
11 | ],
12 | resolve: {
13 | alias: {
14 | '@portabletext/editor': path.resolve(
15 | __dirname,
16 | '../../packages/editor/src',
17 | ),
18 | '@portabletext/block-tools': path.resolve(
19 | __dirname,
20 | '../../packages/block-tools/src',
21 | ),
22 | '@portabletext/patches': path.resolve(
23 | __dirname,
24 | '../../packages/patches/src',
25 | ),
26 | },
27 | },
28 | })
29 |
--------------------------------------------------------------------------------
/examples/legacy/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 | import react from '@vitejs/plugin-react'
3 | import {defineConfig} from 'vite'
4 |
5 | // https://vite.dev/config/
6 | export default defineConfig({
7 | plugins: [
8 | react({
9 | babel: {plugins: [['babel-plugin-react-compiler', {target: '19'}]]},
10 | }),
11 | ],
12 | resolve: {
13 | alias: {
14 | '@portabletext/editor': path.resolve(
15 | __dirname,
16 | '../../packages/editor/src',
17 | ),
18 | '@portabletext/block-tools': path.resolve(
19 | __dirname,
20 | '../../packages/block-tools/src',
21 | ),
22 | '@portabletext/patches': path.resolve(
23 | __dirname,
24 | '../../packages/patches/src',
25 | ),
26 | },
27 | },
28 | })
29 |
--------------------------------------------------------------------------------
/examples/legacy/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "Bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src"]
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/check-lint.yml:
--------------------------------------------------------------------------------
1 | name: check-lint
2 |
3 | on:
4 | # Build on pushes branches that have a PR (including drafts)
5 | pull_request:
6 | # Build on commits pushed to branches without a PR if it's in the allowlist
7 | push:
8 | branches: [main]
9 |
10 | jobs:
11 | check-lint:
12 | runs-on: ubuntu-latest
13 | env:
14 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
15 | TURBO_TEAM: ${{ vars.TURBO_TEAM }}
16 |
17 | steps:
18 | - uses: actions/checkout@v6
19 | - uses: pnpm/action-setup@v4
20 | - uses: actions/setup-node@v6
21 | with:
22 | cache: pnpm
23 | node-version: lts/*
24 |
25 | - name: Install project dependencies
26 | run: pnpm install
27 |
28 | - name: Check lint
29 | run: pnpm check:lint
30 |
--------------------------------------------------------------------------------
/packages/plugin-typography/src/input-rule.multiplication.feature:
--------------------------------------------------------------------------------
1 | Feature: Multiplication Input Rule
2 |
3 | Scenario Outline: Inserting multiplication sign
4 | Given the text
5 | When is inserted
6 | Then the text is
7 |
8 | Examples:
9 | | text | inserted text | new text |
10 | | "" | "1*2" | "1×2" |
11 | | "" | "1*2*3" | "1×2×3" |
12 |
13 | Scenario Outline: Inserting multiplication sign and writing afterwards
14 | Given the text
15 | When is inserted
16 | And "new" is typed
17 | Then the text is
18 |
19 | Examples:
20 | | text | inserted text | new text |
21 | | "" | "1*2" | "1×2new" |
22 | | "" | "1*2*3" | "1×2×3new" |
23 |
--------------------------------------------------------------------------------
/.github/workflows/check-types.yml:
--------------------------------------------------------------------------------
1 | name: check-types
2 |
3 | on:
4 | # Build on pushes branches that have a PR (including drafts)
5 | pull_request:
6 | # Build on commits pushed to branches without a PR if it's in the allowlist
7 | push:
8 | branches: [main]
9 |
10 | jobs:
11 | check-types:
12 | runs-on: ubuntu-latest
13 | env:
14 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
15 | TURBO_TEAM: ${{ vars.TURBO_TEAM }}
16 |
17 | steps:
18 | - uses: actions/checkout@v6
19 | - uses: pnpm/action-setup@v4
20 | - uses: actions/setup-node@v6
21 | with:
22 | cache: pnpm
23 | node-version: lts/*
24 |
25 | - name: Install project dependencies
26 | run: pnpm install
27 |
28 | - name: check:types
29 | run: pnpm check:types
30 |
--------------------------------------------------------------------------------
/packages/toolbar/src/disable-listener.ts:
--------------------------------------------------------------------------------
1 | import type {Editor} from '@portabletext/editor'
2 | import {fromCallback, type AnyEventObject} from 'xstate'
3 |
4 | export type DisableListenerEvent = {type: 'enable'} | {type: 'disable'}
5 |
6 | export const disableListener = fromCallback<
7 | AnyEventObject,
8 | {editor: Editor},
9 | DisableListenerEvent
10 | >(({input, sendBack}) => {
11 | // Send back the initial state
12 | if (input.editor.getSnapshot().context.readOnly) {
13 | sendBack({type: 'disable'})
14 | } else {
15 | sendBack({type: 'enable'})
16 | }
17 |
18 | return input.editor.on('*', () => {
19 | if (input.editor.getSnapshot().context.readOnly) {
20 | sendBack({type: 'disable'})
21 | } else {
22 | sendBack({type: 'enable'})
23 | }
24 | }).unsubscribe
25 | })
26 |
--------------------------------------------------------------------------------
/packages/racejar/src/hooks.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @public
3 | */
4 | export type Hook = object> = {
5 | type: 'Before' | 'After'
6 | callback: HookCallback
7 | }
8 |
9 | /**
10 | * @public
11 | */
12 | export type HookCallback = object> = (
13 | context: TContext,
14 | ) => Promise | void
15 |
16 | /**
17 | * @public
18 | */
19 | export function Before = object>(
20 | callback: HookCallback,
21 | ): Hook {
22 | return {type: 'Before', callback}
23 | }
24 |
25 | /**
26 | * @public
27 | */
28 | export function After = object>(
29 | callback: HookCallback,
30 | ): Hook {
31 | return {type: 'After', callback}
32 | }
33 |
--------------------------------------------------------------------------------
/.github/workflows/check-format.yml:
--------------------------------------------------------------------------------
1 | name: check-format
2 |
3 | on:
4 | # Build on pushes branches that have a PR (including drafts)
5 | pull_request:
6 | # Build on commits pushed to branches without a PR if it's in the allowlist
7 | push:
8 | branches: [main]
9 |
10 | jobs:
11 | check-format:
12 | runs-on: ubuntu-latest
13 | env:
14 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
15 | TURBO_TEAM: ${{ vars.TURBO_TEAM }}
16 |
17 | steps:
18 | - uses: actions/checkout@v6
19 | - uses: pnpm/action-setup@v4
20 | - uses: actions/setup-node@v6
21 | with:
22 | cache: pnpm
23 | node-version: lts/*
24 |
25 | - name: Install project dependencies
26 | run: pnpm install --dev
27 |
28 | - name: Check format
29 | run: pnpm check:format
30 |
--------------------------------------------------------------------------------
/apps/playground/src/toolbar/button-tooltip.tsx:
--------------------------------------------------------------------------------
1 | import type {KeyboardShortcut} from '@portabletext/keyboard-shortcuts'
2 | import type React from 'react'
3 | import {TooltipTrigger} from 'react-aria-components'
4 | import {Tooltip} from '../primitives/tooltip'
5 | import {KeyboardShortcutPreview} from './keyboard-shortcut-preview'
6 |
7 | export function ButtonTooltip(props: {
8 | label: string
9 | shortcutKeys?: KeyboardShortcut['keys']
10 | children: React.ReactNode
11 | }) {
12 | return (
13 |
14 | {props.children}
15 |
16 | {props.label}
17 | {props.shortcutKeys ? (
18 |
19 | ) : null}
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/packages/block-tools/src/util/randomKey.ts:
--------------------------------------------------------------------------------
1 | export function keyGenerator() {
2 | return randomKey(12)
3 | }
4 |
5 | // WHATWG crypto RNG - https://w3c.github.io/webcrypto/Overview.html
6 | function whatwgRNG(length = 16) {
7 | const rnds8 = new Uint8Array(length)
8 | crypto.getRandomValues(rnds8)
9 | return rnds8
10 | }
11 |
12 | const byteToHex: string[] = []
13 | for (let i = 0; i < 256; ++i) {
14 | byteToHex[i] = (i + 0x100).toString(16).slice(1)
15 | }
16 |
17 | /**
18 | * Generate a random key of the given length
19 | *
20 | * @param length - Length of string to generate
21 | * @returns A string of the given length
22 | * @public
23 | */
24 | export function randomKey(length: number): string {
25 | return whatwgRNG(length)
26 | .reduce((str, n) => str + byteToHex[n], '')
27 | .slice(0, length)
28 | }
29 |
--------------------------------------------------------------------------------
/packages/markdown/src/from-portable-text/renderers/list-item.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextListItemRenderer} from '../types'
2 |
3 | /**
4 | * @public
5 | */
6 | export const DefaultListItemRenderer: PortableTextListItemRenderer = ({
7 | children,
8 | value,
9 | listIndex,
10 | }) => {
11 | const listStyle = value.listItem || 'bullet'
12 | const level = value.level || 1
13 |
14 | if (listStyle === 'number') {
15 | const indent = ' '.repeat(level - 1)
16 |
17 | return `${indent}${listIndex ?? 1}. ${children}`
18 | }
19 |
20 | const indent = ' '.repeat(level - 1)
21 |
22 | return `${indent}- ${children}`
23 | }
24 |
25 | /**
26 | * @public
27 | */
28 | export const DefaultUnknownListItemRenderer: PortableTextListItemRenderer = ({
29 | children,
30 | }) => {
31 | return `- ${children}\n`
32 | }
33 |
--------------------------------------------------------------------------------
/apps/playground/src/toolbar/keyboard-shortcut-preview.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {tv} from 'tailwind-variants'
3 |
4 | const styles = tv({
5 | base: 'inline-flex gap-1 font-mono text-sm',
6 | variants: {
7 | size: {
8 | small: 'text-xs',
9 | },
10 | },
11 | })
12 |
13 | export function KeyboardShortcutPreview(props: {
14 | shortcut: ReadonlyArray
15 | size?: 'small'
16 | }) {
17 | return (
18 |
19 | {props.shortcut.map((key, index) => (
20 |
21 | {index > 0 ? (
22 | +
23 | ) : null}
24 | {key}
25 |
26 | ))}
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-selection-text.ts:
--------------------------------------------------------------------------------
1 | import {isSpan, isTextBlock} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import {getSelectedValue} from './selector.get-selected-value'
4 |
5 | /**
6 | * @public
7 | */
8 | export const getSelectionText: EditorSelector = (snapshot) => {
9 | const selectedValue = getSelectedValue(snapshot)
10 |
11 | return selectedValue.reduce((text, block) => {
12 | if (!isTextBlock(snapshot.context, block)) {
13 | return text
14 | }
15 |
16 | return (
17 | text +
18 | block.children.reduce((text, child) => {
19 | if (isSpan(snapshot.context, child)) {
20 | return text + child.text
21 | }
22 |
23 | return text
24 | }, '')
25 | )
26 | }, '')
27 | }
28 |
--------------------------------------------------------------------------------
/packages/editor/src/type-utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @internal
3 | */
4 | export type PickFromUnion<
5 | TUnion,
6 | TTagKey extends keyof TUnion,
7 | TPickedTags extends TUnion[TTagKey],
8 | > = TUnion extends Record ? TUnion : never
9 |
10 | /**
11 | * @internal
12 | */
13 | export type OmitFromUnion<
14 | TUnion,
15 | TTagKey extends keyof TUnion,
16 | TOmittedTags extends TUnion[TTagKey],
17 | > = TUnion extends Record ? never : TUnion
18 |
19 | export type NamespaceEvent = TEvent extends {
20 | type: infer TEventType
21 | }
22 | ? {
23 | [K in keyof TEvent]: K extends 'type'
24 | ? `${TNamespace}.${TEventType & string}`
25 | : TEvent[K]
26 | }
27 | : never
28 |
29 | export type StrictExtract = U
30 |
--------------------------------------------------------------------------------
/packages/schema/README.md:
--------------------------------------------------------------------------------
1 | # `@portabletext/schema`
2 |
3 | A TypeScript library for defining and compiling Portable Text schemas with full type safety and editor support.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm install @portabletext/schema
9 | ```
10 |
11 | ## Usage
12 |
13 | ### Define a Schema
14 |
15 | ```ts
16 | import {defineSchema} from '@portabletext/schema'
17 |
18 | const schemaDefinition = defineSchema({
19 | styles: [{name: 'normal'}, {name: 'h1'}, {name: 'h2'}, {name: 'blockquote'}],
20 | decorators: [{name: 'strong'}, {name: 'em'}],
21 | annotations: [{name: 'link'}],
22 | lists: [{name: 'bullet'}, {name: 'numbered'}],
23 | })
24 | ```
25 |
26 | ### Compile Schema
27 |
28 | ```ts
29 | import {compileSchema} from '@portabletext/schema'
30 |
31 | const schema = compileSchema(schemaDefinition)
32 | ```
33 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/container.tsx:
--------------------------------------------------------------------------------
1 | import type {HTMLAttributes} from 'react'
2 | import {twMerge} from 'tailwind-merge'
3 | import {tv} from 'tailwind-variants'
4 |
5 | export const container = tv({
6 | base: 'bg-white',
7 | variants: {
8 | variant: {
9 | default: 'p-2 rounded-md shadow-sm',
10 | ghost: '',
11 | },
12 | },
13 | defaultVariants: {
14 | variant: 'default',
15 | },
16 | })
17 |
18 | export function Container(
19 | props: HTMLAttributes & {
20 | className?: string
21 | variant?: 'default' | 'ghost'
22 | },
23 | ) {
24 | const {className, children, ...rest} = props
25 | return (
26 |
30 | {children}
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/packages/sanity-bridge/src/key-generator.ts:
--------------------------------------------------------------------------------
1 | export const keyGenerator = (): string => randomKey(12)
2 |
3 | const getByteHexTable = (() => {
4 | let table: any[]
5 | return () => {
6 | if (table) {
7 | return table
8 | }
9 |
10 | table = []
11 | for (let i = 0; i < 256; ++i) {
12 | table[i] = (i + 0x100).toString(16).slice(1)
13 | }
14 | return table
15 | }
16 | })()
17 |
18 | // WHATWG crypto RNG - https://w3c.github.io/webcrypto/Overview.html
19 | function whatwgRNG(length = 16) {
20 | const rnds8 = new Uint8Array(length)
21 | crypto.getRandomValues(rnds8)
22 | return rnds8
23 | }
24 |
25 | function randomKey(length?: number): string {
26 | const table = getByteHexTable()
27 | return whatwgRNG(length)
28 | .reduce((str, n) => str + table[n], '')
29 | .slice(0, length)
30 | }
31 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.get-block-start-point.ts:
--------------------------------------------------------------------------------
1 | import {isTextBlock, type PortableTextBlock} from '@portabletext/schema'
2 | import type {EditorContext} from '../editor/editor-snapshot'
3 | import type {EditorSelectionPoint} from '../types/editor'
4 | import type {BlockPath} from '../types/paths'
5 |
6 | /**
7 | * @public
8 | */
9 | export function getBlockStartPoint({
10 | context,
11 | block,
12 | }: {
13 | context: Pick
14 | block: {
15 | node: PortableTextBlock
16 | path: BlockPath
17 | }
18 | }): EditorSelectionPoint {
19 | if (isTextBlock(context, block.node)) {
20 | return {
21 | path: [...block.path, 'children', {_key: block.node.children[0]._key}],
22 | offset: 0,
23 | }
24 | }
25 |
26 | return {
27 | path: block.path,
28 | offset: 0,
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.github/workflows/check-react-compiler.yml:
--------------------------------------------------------------------------------
1 | name: check-react-compiler
2 |
3 | on:
4 | # Build on pushes branches that have a PR (including drafts)
5 | pull_request:
6 | # Build on commits pushed to branches without a PR if it's in the allowlist
7 | push:
8 | branches: [main]
9 |
10 | jobs:
11 | check-lint:
12 | runs-on: ubuntu-latest
13 | env:
14 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
15 | TURBO_TEAM: ${{ vars.TURBO_TEAM }}
16 |
17 | steps:
18 | - uses: actions/checkout@v6
19 | - uses: pnpm/action-setup@v4
20 | - uses: actions/setup-node@v6
21 | with:
22 | cache: pnpm
23 | node-version: lts/*
24 |
25 | - name: Install project dependencies
26 | run: pnpm install
27 |
28 | - name: Check react compiler
29 | run: pnpm check:react-compiler
30 |
--------------------------------------------------------------------------------
/packages/editor/src/editor/usePortableTextEditor.ts:
--------------------------------------------------------------------------------
1 | import {createContext, useContext} from 'react'
2 | import type {PortableTextEditor} from './PortableTextEditor'
3 |
4 | /**
5 | * A React context for sharing the editor object.
6 | */
7 | export const PortableTextEditorContext =
8 | createContext(null)
9 |
10 | /**
11 | * @deprecated Use `useEditor` to get the current editor instance.
12 | * @public
13 | * Get the current editor object from the React context.
14 | */
15 | export const usePortableTextEditor = (): PortableTextEditor => {
16 | const editor = useContext(PortableTextEditorContext)
17 |
18 | if (!editor) {
19 | throw new Error(
20 | `The \`usePortableTextEditor\` hook must be used inside the component's context.`,
21 | )
22 | }
23 |
24 | return editor
25 | }
26 |
--------------------------------------------------------------------------------
/packages/editor/src/types/slate.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | PortableTextSpan,
3 | PortableTextTextBlock,
4 | } from '@portabletext/schema'
5 | import type {BaseEditor, Descendant} from 'slate'
6 | import type {ReactEditor} from 'slate-react'
7 | import type {PortableTextSlateEditor} from './slate-editor'
8 |
9 | export interface VoidElement {
10 | _type: string
11 | _key: string
12 | children: Descendant[]
13 | __inline: boolean
14 | value: Record
15 | }
16 |
17 | export interface SlateTextBlock extends Omit<
18 | PortableTextTextBlock,
19 | 'children'
20 | > {
21 | children: Descendant[]
22 | }
23 |
24 | declare module 'slate' {
25 | interface CustomTypes {
26 | Editor: BaseEditor & ReactEditor & PortableTextSlateEditor
27 | Element: SlateTextBlock | VoidElement
28 | Text: PortableTextSpan
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/playground/src/primitives/field.select.tsx:
--------------------------------------------------------------------------------
1 | import {Label} from './field'
2 | import {Select, SelectItem} from './select'
3 |
4 | export function SelectField(props: {
5 | name: string
6 | label?: string
7 | defaultOption?: string
8 | options: Array<{
9 | id: string
10 | value: string
11 | label: string
12 | }>
13 | }) {
14 | return (
15 |
16 |
17 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/packages/block-tools/src/HtmlDeserializer/preprocessors/preprocessor.notion.ts:
--------------------------------------------------------------------------------
1 | import {_XPathResult} from './xpathResult'
2 |
3 | export function preprocessNotion(html: string, doc: Document): Document {
4 | const NOTION_REGEX = //g
5 |
6 | if (html.match(NOTION_REGEX)) {
7 | // Tag every child with attribute 'is-notion' so that the Notion rule-set can
8 | // work exclusivly on these children
9 | const childNodes = doc.evaluate(
10 | '//*',
11 | doc,
12 | null,
13 | _XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
14 | null,
15 | )
16 |
17 | for (let i = childNodes.snapshotLength - 1; i >= 0; i--) {
18 | const elm = childNodes.snapshotItem(i) as HTMLElement
19 | elm?.setAttribute('data-is-notion', 'true')
20 | }
21 |
22 | return doc
23 | }
24 | return doc
25 | }
26 |
--------------------------------------------------------------------------------
/packages/block-tools/tsdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
3 | "tagDefinitions": [
4 | {
5 | "tagName": "@hidden",
6 | "syntaxKind": "block",
7 | "allowMultiple": true
8 | },
9 | {
10 | "tagName": "@todo",
11 | "syntaxKind": "block",
12 | "allowMultiple": true
13 | }
14 | ],
15 | "supportForTags": {
16 | "@hidden": true,
17 | "@beta": true,
18 | "@internal": true,
19 | "@public": true,
20 | "@experimental": true,
21 | "@see": true,
22 | "@link": true,
23 | "@example": true,
24 | "@deprecated": true,
25 | "@alpha": true,
26 | "@param": true,
27 | "@returns": true,
28 | "@remarks": true,
29 | "@throws": true,
30 | "@defaultValue": true,
31 | "@todo": true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/__tests__/ranges.test.ts:
--------------------------------------------------------------------------------
1 | import type {InsertTextOperation, Range} from 'slate'
2 | import {describe, expect, it} from 'vitest'
3 | import {moveRangeByOperation} from '../move-range-by-operation'
4 |
5 | describe('moveRangeByOperation', () => {
6 | it('should move range when inserting text in front of it', () => {
7 | const range: Range = {
8 | anchor: {path: [0, 0], offset: 1},
9 | focus: {path: [0, 0], offset: 3},
10 | }
11 | const operation: InsertTextOperation = {
12 | type: 'insert_text',
13 | path: [0, 0],
14 | offset: 0,
15 | text: 'foo',
16 | }
17 | const newRange = moveRangeByOperation(range, operation)
18 | expect(newRange).toEqual({
19 | anchor: {path: [0, 0], offset: 4},
20 | focus: {path: [0, 0], offset: 6},
21 | })
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/packages/markdown/src/key-generator.ts:
--------------------------------------------------------------------------------
1 | export function defaultKeyGenerator() {
2 | return randomKey(12)
3 | }
4 |
5 | const getByteHexTable = (() => {
6 | let table: any[]
7 | return () => {
8 | if (table) {
9 | return table
10 | }
11 |
12 | table = []
13 | for (let i = 0; i < 256; ++i) {
14 | table[i] = (i + 0x100).toString(16).slice(1)
15 | }
16 | return table
17 | }
18 | })()
19 |
20 | // WHATWG crypto RNG - https://w3c.github.io/webcrypto/Overview.html
21 | function whatwgRNG(length = 16) {
22 | const rnds8 = new Uint8Array(length)
23 | crypto.getRandomValues(rnds8)
24 | return rnds8
25 | }
26 |
27 | function randomKey(length?: number): string {
28 | const table = getByteHexTable()
29 | return whatwgRNG(length)
30 | .reduce((str, n) => str + table[n], '')
31 | .slice(0, length)
32 | }
33 |
--------------------------------------------------------------------------------
/apps/docs/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.block-offset-to-block-selection-point.ts:
--------------------------------------------------------------------------------
1 | import type {EditorContext} from '../editor/editor-snapshot'
2 | import type {BlockOffset} from '../types/block-offset'
3 | import type {EditorSelectionPoint} from '../types/editor'
4 |
5 | /**
6 | * @public
7 | */
8 | export function blockOffsetToBlockSelectionPoint({
9 | context,
10 | blockOffset,
11 | }: {
12 | context: Pick
13 | blockOffset: BlockOffset
14 | }): EditorSelectionPoint | undefined {
15 | let selectionPoint: EditorSelectionPoint | undefined
16 |
17 | for (const block of context.value) {
18 | if (block._key === blockOffset.path[0]._key) {
19 | selectionPoint = {
20 | path: [{_key: block._key}],
21 | offset: blockOffset.offset,
22 | }
23 | break
24 | }
25 | }
26 |
27 | return selectionPoint
28 | }
29 |
--------------------------------------------------------------------------------
/examples/basic/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import reactHooks from 'eslint-plugin-react-hooks'
3 | import reactRefresh from 'eslint-plugin-react-refresh'
4 | import globals from 'globals'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | {ignores: ['dist']},
9 | reactHooks.configs.flat.recommended,
10 | {
11 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
12 | files: ['**/*.{ts,tsx}'],
13 | languageOptions: {
14 | ecmaVersion: 2020,
15 | globals: globals.browser,
16 | },
17 | plugins: {
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | 'react-hooks/exhaustive-deps': 'error',
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | {allowConstantExport: true},
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/examples/legacy/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import reactHooks from 'eslint-plugin-react-hooks'
3 | import reactRefresh from 'eslint-plugin-react-refresh'
4 | import globals from 'globals'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | {ignores: ['dist']},
9 | reactHooks.configs.flat.recommended,
10 | {
11 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
12 | files: ['**/*.{ts,tsx}'],
13 | languageOptions: {
14 | ecmaVersion: 2020,
15 | globals: globals.browser,
16 | },
17 | plugins: {
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | 'react-hooks/exhaustive-deps': 'error',
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | {allowConstantExport: true},
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/key-generator.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @public
3 | */
4 | export const defaultKeyGenerator = (): string => randomKey(12)
5 |
6 | const getByteHexTable = (() => {
7 | let table: any[]
8 | return () => {
9 | if (table) {
10 | return table
11 | }
12 |
13 | table = []
14 | for (let i = 0; i < 256; ++i) {
15 | table[i] = (i + 0x100).toString(16).slice(1)
16 | }
17 | return table
18 | }
19 | })()
20 |
21 | // WHATWG crypto RNG - https://w3c.github.io/webcrypto/Overview.html
22 | function whatwgRNG(length = 16) {
23 | const rnds8 = new Uint8Array(length)
24 | crypto.getRandomValues(rnds8)
25 | return rnds8
26 | }
27 |
28 | function randomKey(length?: number): string {
29 | const table = getByteHexTable()
30 | return whatwgRNG(length)
31 | .reduce((str, n) => str + table[n], '')
32 | .slice(0, length)
33 | }
34 |
--------------------------------------------------------------------------------
/packages/block-tools/src/HtmlDeserializer/rules/index.ts:
--------------------------------------------------------------------------------
1 | import type {Schema} from '@portabletext/schema'
2 | import type {SchemaMatchers} from '../../schema-matchers'
3 | import type {DeserializerRule} from '../../types'
4 | import {createWordOnlineRules} from '../word-online/rules.word-online'
5 | import {createGDocsRules} from './rules.gdocs'
6 | import {createHTMLRules} from './rules.html'
7 | import {createNotionRules} from './rules.notion'
8 | import {createWordRules} from './rules.word'
9 |
10 | export function createRules(
11 | schema: Schema,
12 | options: {keyGenerator?: () => string; matchers?: SchemaMatchers},
13 | ): DeserializerRule[] {
14 | return [
15 | ...createWordRules(),
16 | ...createWordOnlineRules(schema, options),
17 | ...createNotionRules(),
18 | ...createGDocsRules(schema),
19 | ...createHTMLRules(schema, options),
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-anchor-block.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextBlock} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {BlockPath} from '../types/paths'
4 | import {getBlockKeyFromSelectionPoint} from '../utils/util.selection-point'
5 |
6 | /**
7 | * @public
8 | */
9 | export const getAnchorBlock: EditorSelector<
10 | {node: PortableTextBlock; path: BlockPath} | undefined
11 | > = (snapshot) => {
12 | if (!snapshot.context.selection) {
13 | return undefined
14 | }
15 |
16 | const key = getBlockKeyFromSelectionPoint(snapshot.context.selection.anchor)
17 | const index = key ? snapshot.blockIndexMap.get(key) : undefined
18 | const node =
19 | index !== undefined ? snapshot.context.value.at(index) : undefined
20 |
21 | return node && key ? {node, path: [{_key: key}]} : undefined
22 | }
23 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-focus-block.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextBlock} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {BlockPath} from '../types/paths'
4 | import {getBlockKeyFromSelectionPoint} from '../utils/util.selection-point'
5 |
6 | /**
7 | * @public
8 | */
9 | export const getFocusBlock: EditorSelector<
10 | {node: PortableTextBlock; path: BlockPath} | undefined
11 | > = (snapshot) => {
12 | if (!snapshot.context.selection) {
13 | return undefined
14 | }
15 |
16 | const key = getBlockKeyFromSelectionPoint(snapshot.context.selection.focus)
17 | const index = key ? snapshot.blockIndexMap.get(key) : undefined
18 |
19 | const node =
20 | index !== undefined ? snapshot.context.value.at(index) : undefined
21 |
22 | return node && key ? {node, path: [{_key: key}]} : undefined
23 | }
24 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.is-active-decorator.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelector} from '../editor/editor-selector'
2 | import {getActiveDecorators} from './selector.get-active-decorators'
3 | import {getSelectedSpans} from './selector.get-selected-spans'
4 | import {isSelectionExpanded} from './selector.is-selection-expanded'
5 |
6 | /**
7 | * @public
8 | */
9 | export function isActiveDecorator(decorator: string): EditorSelector {
10 | return (snapshot) => {
11 | if (isSelectionExpanded(snapshot)) {
12 | const selectedSpans = getSelectedSpans(snapshot)
13 |
14 | return (
15 | selectedSpans.length > 0 &&
16 | selectedSpans.every((span) => span.node.marks?.includes(decorator))
17 | )
18 | }
19 |
20 | const activeDecorators = getActiveDecorators(snapshot)
21 |
22 | return activeDecorators.includes(decorator)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/HtmlDeserializer/whitespace2/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | I should not have any whitepace in the start, and none in the end
4 | I should not have any whitepace in the start, and none in the end
5 | I should not have any whitepace in the start, and none in the end I have one in the beginning but none in the end
6 | I should not have any whitepace in the start, and one in the end I have none in the beginning and none in the end But I have one in the beginning
7 |
8 | I should have a space here: But none here.
9 |
10 | I should have a space between these two links.
11 | I should have a space between these two words.
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/schema/src/index.ts:
--------------------------------------------------------------------------------
1 | export {compileSchema} from './compile-schema'
2 | export {
3 | defineSchema,
4 | type AnnotationDefinition,
5 | type BlockObjectDefinition,
6 | type DecoratorDefinition,
7 | type InlineObjectDefinition,
8 | type ListDefinition,
9 | type SchemaDefinition,
10 | type StyleDefinition,
11 | } from './define-schema'
12 | export type {
13 | AnnotationSchemaType,
14 | BaseDefinition,
15 | BlockObjectSchemaType,
16 | DecoratorSchemaType,
17 | FieldDefinition,
18 | InlineObjectSchemaType,
19 | ListSchemaType,
20 | Schema,
21 | StyleSchemaType,
22 | } from './schema'
23 | export {isSpan, isTextBlock, isTypedObject} from './types'
24 | export type {
25 | PortableTextBlock,
26 | PortableTextChild,
27 | PortableTextListBlock,
28 | PortableTextObject,
29 | PortableTextSpan,
30 | PortableTextTextBlock,
31 | TypedObject,
32 | } from './types'
33 |
--------------------------------------------------------------------------------
/apps/docs/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import {cn} from '@/lib/utils'
2 | import * as SeparatorPrimitive from '@radix-ui/react-separator'
3 | import * as React from 'react'
4 |
5 | const Separator = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(
9 | (
10 | {className, orientation = 'horizontal', decorative = true, ...props},
11 | ref,
12 | ) => (
13 |
24 | ),
25 | )
26 | Separator.displayName = SeparatorPrimitive.Root.displayName
27 |
28 | export {Separator}
29 |
--------------------------------------------------------------------------------
/apps/docs/src/content/docs/reference/keyboard-shortcuts.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Keyboard Shortcuts API Overview
3 | description: Keyboard Shortcuts reference
4 | prev: false
5 | next: false
6 | ---
7 |
8 | import {CardGrid, LinkCard} from '@astrojs/starlight/components'
9 | import {PackageManagers} from 'starlight-package-managers'
10 |
11 | The Keyboard Shortcuts API offers drop-in keyboard shortcuts for the editor that can be paired with toolbar buttons, as well as a way to create your own custom keyboard shortcuts.
12 |
13 |
14 |
15 | See the Generated API reference for available keyboard shortcuts (variables) and functions for creating your own.
16 |
17 |
18 |
23 |
24 |
--------------------------------------------------------------------------------
/apps/playground/src/plugins/looks-like-url.test.ts:
--------------------------------------------------------------------------------
1 | import {expect, test} from 'vitest'
2 | import {looksLikeUrl} from './looks-like-url'
3 |
4 | test(looksLikeUrl.name, () => {
5 | expect(looksLikeUrl('https://example.com')).toBe(true)
6 | expect(looksLikeUrl('http://example.com')).toBe(true)
7 | expect(looksLikeUrl('mailto:foo@example.com')).toBe(true)
8 | expect(looksLikeUrl('tel:+123456789')).toBe(true)
9 | expect(looksLikeUrl('https://example')).toBe(true)
10 | expect(looksLikeUrl('http://example')).toBe(true)
11 | expect(looksLikeUrl('http:example')).toBe(true)
12 |
13 | expect(looksLikeUrl('http: example')).toBe(false)
14 | expect(looksLikeUrl('https://example. com')).toBe(false)
15 | expect(looksLikeUrl('example.com')).toBe(false)
16 | expect(looksLikeUrl('example. com')).toBe(false)
17 | expect(looksLikeUrl('a:b')).toBe(false)
18 | expect(looksLikeUrl('a: b')).toBe(false)
19 | })
20 |
--------------------------------------------------------------------------------
/packages/editor/gherkin-spec/removing-blocks.feature:
--------------------------------------------------------------------------------
1 | Feature: Removing Blocks
2 |
3 | Background:
4 | Given one editor
5 | And a global keymap
6 |
7 | Scenario: Pressing Delete in empty block with text below
8 | Given the text "foo" in block "b1"
9 | And the text "bar" in block "b2"
10 | And the text "baz" in block "b3"
11 | When the caret is put before "bar"
12 | And "Delete" is pressed 4 times
13 | Then the text is "foo|baz"
14 | And "foo" is in block "b1"
15 | And "baz" is in block "b3"
16 |
17 | Scenario: Pressing Backspace in empty block with text below
18 | Given the text "foo" in block "b1"
19 | And the text "bar" in block "b2"
20 | And the text "baz" in block "b3"
21 | When the caret is put after "bar"
22 | And "Backspace" is pressed 4 times
23 | Then the text is "foo|baz"
24 | And "foo" is in block "b1"
25 | And "baz" is in block "b3"
26 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'apps/*'
3 | - 'packages/*'
4 | - 'examples/*'
5 |
6 | catalog:
7 | typescript: 5.9.3
8 |
9 | minimumReleaseAge: 4320
10 | minimumReleaseAgeExclude:
11 | - '@portabletext/*'
12 | - '@sanity/*'
13 | - '@types/*'
14 | - '@typescript/*'
15 | - csstype
16 | - 'groq-js'
17 | - 'react'
18 | - 'react-dom'
19 | - 'react-is'
20 |
21 | trustPolicy: no-downgrade
22 |
23 | trustPolicyExclude:
24 | - '@octokit/endpoint@9.0.6'
25 | - '@octokit/plugin-paginate-rest@9.2.2'
26 | - '@reduxjs/toolkit@2.9.0 || 2.10.1 || 2.11.0'
27 | - '@sanity/mutate@0.11.0-canary.4'
28 | - '@sanity/sdk@2.1.2'
29 | - '@swc/core@1.13.5'
30 | - chokidar@4.0.3
31 | - groq@3.88.1-typegen-experimental.0
32 | - react-redux@9.2.0
33 | - reselect@5.1.1
34 | - rxjs@7.8.2
35 | - semver@5.7.2 || 6.3.1
36 | - undici@5.29.0
37 | - undici-types@6.21.0
38 | - vite@5.4.21 || 6.4.1
39 |
--------------------------------------------------------------------------------
/apps/playground/src/plugins/plugin.link.tsx:
--------------------------------------------------------------------------------
1 | import {useEditor} from '@portabletext/editor'
2 | import {useEffect} from 'react'
3 | import {createLinkBehaviors} from './behavior.links'
4 |
5 | export function LinkPlugin() {
6 | const editor = useEditor()
7 |
8 | useEffect(() => {
9 | const behaviors = createLinkBehaviors({
10 | linkAnnotation: ({schema, url}) => {
11 | const name = schema.annotations.find(
12 | (annotation) => annotation.name === 'link',
13 | )?.name
14 | return name ? {name, value: {href: url}} : undefined
15 | },
16 | })
17 |
18 | const unregisterBehaviors = behaviors.map((behavior) =>
19 | editor.registerBehavior({behavior}),
20 | )
21 |
22 | return () => {
23 | for (const unregisterBehavior of unregisterBehaviors) {
24 | unregisterBehavior()
25 | }
26 | }
27 | }, [editor])
28 |
29 | return null
30 | }
31 |
--------------------------------------------------------------------------------
/apps/playground/src/plugins/read-files.ts:
--------------------------------------------------------------------------------
1 | type ReadAs = 'text' | 'data-url'
2 |
3 | export function readFiles({
4 | files,
5 | readAs,
6 | }: {
7 | files: Array
8 | readAs: ReadAs
9 | }) {
10 | return Promise.allSettled(files.map((file) => readFile({file, readAs})))
11 | }
12 |
13 | function readFile({file, readAs}: {file: File; readAs: ReadAs}) {
14 | return new Promise<{file: File; result: string}>((resolve, reject) => {
15 | const reader = new FileReader()
16 |
17 | reader.onload = () => {
18 | if (typeof reader.result === 'string') {
19 | resolve({file, result: reader.result})
20 | } else {
21 | reject(new Error('FileReader result is not a string'))
22 | }
23 | }
24 |
25 | reader.onerror = reject
26 |
27 | if (readAs === 'text') {
28 | reader.readAsText(file)
29 | } else {
30 | reader.readAsDataURL(file)
31 | }
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/packages/patches/src/arrayInsert.ts:
--------------------------------------------------------------------------------
1 | export const BEFORE = 'before'
2 | export const AFTER = 'after'
3 |
4 | export default function insert(
5 | array: any[],
6 | position: string,
7 | index: number,
8 | ...args: any[]
9 | ) {
10 | if (position !== BEFORE && position !== AFTER) {
11 | throw new Error(
12 | `Invalid position "${position}", must be either ${BEFORE} or ${AFTER}`,
13 | )
14 | }
15 |
16 | const items = flatten(...args)
17 |
18 | if (array.length === 0) {
19 | return items
20 | }
21 |
22 | const len = array.length
23 | const idx = Math.abs((len + index) % len) % len
24 |
25 | const normalizedIdx = position === 'after' ? idx + 1 : idx
26 |
27 | const copy = array.slice()
28 | copy.splice(normalizedIdx, 0, ...flatten(items))
29 | return copy
30 | }
31 |
32 | function flatten(...values: any[]) {
33 | return values.reduce((prev, item) => prev.concat(item), [])
34 | }
35 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/compound-client-rect.ts:
--------------------------------------------------------------------------------
1 | export function getCompoundClientRect(nodes: Array): DOMRect {
2 | if (nodes.length === 0) {
3 | return new DOMRect(0, 0, 0, 0)
4 | }
5 |
6 | const elements = nodes.filter((node) => node instanceof Element)
7 |
8 | const firstRect = elements.at(0)?.getBoundingClientRect()
9 |
10 | if (!firstRect) {
11 | return new DOMRect(0, 0, 0, 0)
12 | }
13 |
14 | let left = firstRect.left
15 | let top = firstRect.top
16 | let right = firstRect.right
17 | let bottom = firstRect.bottom
18 |
19 | for (let i = 1; i < elements.length; i++) {
20 | const rect = elements[i].getBoundingClientRect()
21 | left = Math.min(left, rect.left)
22 | top = Math.min(top, rect.top)
23 | right = Math.max(right, rect.right)
24 | bottom = Math.max(bottom, rect.bottom)
25 | }
26 |
27 | return new DOMRect(left, top, right - left, bottom - top)
28 | }
29 |
--------------------------------------------------------------------------------
/packages/markdown/src/from-portable-text/renderers/block-spacing.ts:
--------------------------------------------------------------------------------
1 | import {
2 | isPortableTextBlock,
3 | isPortableTextListItemBlock,
4 | } from '@portabletext/toolkit'
5 | import type {TypedObject} from '@portabletext/types'
6 |
7 | /**
8 | * @public
9 | */
10 | export type BlockSpacingRenderer = (options: {
11 | current: TypedObject
12 | next: TypedObject
13 | }) => string | undefined
14 |
15 | /**
16 | * @public
17 | */
18 | export const DefaultBlockSpacingRenderer: BlockSpacingRenderer = ({
19 | current,
20 | next,
21 | }) => {
22 | if (
23 | isPortableTextListItemBlock(current) &&
24 | isPortableTextListItemBlock(next)
25 | ) {
26 | return '\n'
27 | }
28 |
29 | if (
30 | isPortableTextBlock(current) &&
31 | isPortableTextBlock(next) &&
32 | current.style === 'blockquote' &&
33 | next.style === 'blockquote'
34 | ) {
35 | return '\n>\n'
36 | }
37 |
38 | return '\n\n'
39 | }
40 |
--------------------------------------------------------------------------------
/packages/block-tools/test/tests/util/randomKey.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, expect, test} from 'vitest'
2 | import {randomKey} from '../../../src/util/randomKey'
3 |
4 | describe(randomKey.name, () => {
5 | test('returns a random string of specified length', () => {
6 | expect(randomKey(0)).toBe('')
7 | expect(randomKey(1).length).toBe(1)
8 | expect(randomKey(5).length).toBe(5)
9 | expect(randomKey(12).length).toBe(12)
10 | expect(randomKey(32).length).toBe(32)
11 | expect(randomKey(100).length).toBe(100)
12 | })
13 |
14 | test('returns unique values on multiple calls', () => {
15 | const keys = new Set()
16 | for (let i = 0; i < 100; i++) {
17 | keys.add(randomKey(16))
18 | }
19 | expect(keys.size).toBe(100)
20 | })
21 |
22 | test('returns valid hex characters only', () => {
23 | const hexPattern = /^[0-9a-f]*$/
24 | expect(randomKey(20)).toMatch(hexPattern)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/packages/editor/src/utils/util.at-the-beginning-of-block.ts:
--------------------------------------------------------------------------------
1 | import {isTextBlock, type PortableTextBlock} from '@portabletext/schema'
2 | import type {EditorContext} from '../editor/editor-snapshot'
3 | import {isSelectionCollapsed} from './util.is-selection-collapsed'
4 | import {getChildKeyFromSelectionPoint} from './util.selection-point'
5 |
6 | export function isAtTheBeginningOfBlock({
7 | context,
8 | block,
9 | }: {
10 | context: EditorContext
11 | block: PortableTextBlock
12 | }) {
13 | if (!isTextBlock(context, block)) {
14 | return false
15 | }
16 |
17 | if (!context.selection) {
18 | return false
19 | }
20 |
21 | if (!isSelectionCollapsed(context.selection)) {
22 | return false
23 | }
24 |
25 | const focusSpanKey = getChildKeyFromSelectionPoint(context.selection.focus)
26 |
27 | return (
28 | focusSpanKey === block.children[0]._key &&
29 | context.selection.focus.offset === 0
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/packages/editor/src/operations/operation.select.ts:
--------------------------------------------------------------------------------
1 | import {Transforms} from 'slate'
2 | import {IS_FOCUSED, IS_READ_ONLY} from 'slate-dom'
3 | import {toSlateRange} from '../internal-utils/to-slate-range'
4 | import type {OperationImplementation} from './operation.types'
5 |
6 | export const selectOperationImplementation: OperationImplementation<
7 | 'select'
8 | > = ({context, operation}) => {
9 | const newSelection = toSlateRange({
10 | context: {
11 | schema: context.schema,
12 | value: operation.editor.value,
13 | selection: operation.at,
14 | },
15 | blockIndexMap: operation.editor.blockIndexMap,
16 | })
17 |
18 | if (newSelection) {
19 | Transforms.select(operation.editor, newSelection)
20 | } else {
21 | Transforms.deselect(operation.editor)
22 | }
23 |
24 | if (IS_FOCUSED.get(operation.editor) && IS_READ_ONLY.get(operation.editor)) {
25 | IS_FOCUSED.set(operation.editor, false)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/editor/tests/event.ready.test.tsx:
--------------------------------------------------------------------------------
1 | import {describe, expect, test, vi} from 'vitest'
2 | import type {EditorEmittedEvent} from '../src'
3 | import {EventListenerPlugin} from '../src/plugins/plugin.event-listener'
4 | import {createTestEditor} from '../src/test/vitest'
5 |
6 | describe('event.ready', () => {
7 | test('emits for "undefined" initial value', async () => {
8 | const onEvent = vi.fn<(event: EditorEmittedEvent) => void>()
9 |
10 | await createTestEditor({
11 | children: ,
12 | })
13 |
14 | expect(onEvent).toHaveBeenCalledWith({type: 'ready'})
15 | })
16 |
17 | test('emits for "[]" initial value', async () => {
18 | const onEvent = vi.fn<(event: EditorEmittedEvent) => void>()
19 |
20 | await createTestEditor({
21 | children: ,
22 | })
23 |
24 | expect(onEvent).toHaveBeenCalledWith({type: 'ready'})
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/apps/playground/src/toolbar/button.focus.tsx:
--------------------------------------------------------------------------------
1 | import {useEditor, useEditorSelector} from '@portabletext/editor'
2 | import {SquareDashedMousePointerIcon} from 'lucide-react'
3 | import {TooltipTrigger} from 'react-aria-components'
4 | import {Button} from '../primitives/button'
5 | import {Tooltip} from '../primitives/tooltip'
6 |
7 | export function FocusButton() {
8 | const editor = useEditor()
9 | const readOnly = useEditorSelector(
10 | editor,
11 | (snapshot) => snapshot.context.readOnly,
12 | )
13 |
14 | return (
15 |
16 |
27 | Focus
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/packages/editor/src/selectors/selector.get-selection-end-block.ts:
--------------------------------------------------------------------------------
1 | import type {PortableTextBlock} from '@portabletext/schema'
2 | import type {EditorSelector} from '../editor/editor-selector'
3 | import type {BlockPath} from '../types/paths'
4 | import {getSelectionEndPoint} from '../utils/util.get-selection-end-point'
5 | import {getFocusBlock} from './selector.get-focus-block'
6 |
7 | /**
8 | * @public
9 | */
10 | export const getSelectionEndBlock: EditorSelector<
11 | | {
12 | node: PortableTextBlock
13 | path: BlockPath
14 | }
15 | | undefined
16 | > = (snapshot) => {
17 | const endPoint = getSelectionEndPoint(snapshot.context.selection)
18 |
19 | if (!endPoint) {
20 | return undefined
21 | }
22 |
23 | return getFocusBlock({
24 | ...snapshot,
25 | context: {
26 | ...snapshot.context,
27 | selection: {
28 | anchor: endPoint,
29 | focus: endPoint,
30 | },
31 | },
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/packages/editor/src/internal-utils/collapse-selection.ts:
--------------------------------------------------------------------------------
1 | import type {EditorSelection} from '../types/editor'
2 |
3 | export function collapseSelection(
4 | selection: EditorSelection,
5 | direction: 'start' | 'end',
6 | ): EditorSelection {
7 | if (!selection) {
8 | return selection
9 | }
10 |
11 | if (direction === 'start') {
12 | return selection.backward
13 | ? {
14 | anchor: selection.focus,
15 | focus: selection.focus,
16 | backward: false,
17 | }
18 | : {
19 | anchor: selection.anchor,
20 | focus: selection.anchor,
21 | backward: false,
22 | }
23 | }
24 |
25 | return selection.backward
26 | ? {
27 | anchor: selection.anchor,
28 | focus: selection.anchor,
29 | backward: false,
30 | }
31 | : {
32 | anchor: selection.focus,
33 | focus: selection.focus,
34 | backward: false,
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/apps/docs/src/content/docs/reference/editor.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Editor API Overview
3 | description: Editor API reference overview
4 | ---
5 |
6 | The editor API provides access to the editor instance and selectors for deriving state.
7 |
8 | Use the following hooks to access the editor from within your components.
9 |
10 | ## `useEditor`
11 |
12 | Access the editor instance. Commonly used when building toolbars or passing snapshot to selectors.
13 |
14 | ```tsx
15 | import {useEditor} from '@portabletext/editor'
16 |
17 | const editor = useEditor()
18 | ```
19 |
20 | ## `useEditorSelector`
21 |
22 | Access selectors in areas where the snapshot is available by combining with `useEditor`.
23 |
24 | ```tsx
25 | import {useEditor, useEditorSelector} from '@portabletext/editor'
26 | import * as selectors from '@portabletext/editor/selectors'
27 |
28 | const editor = useEditor()
29 | const isActive = useEditorSelector(editor, selectors.isActiveDecorator('em'))
30 | ```
31 |
--------------------------------------------------------------------------------
/packages/block-tools/test/html-to-blocks/lists.test.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs'
2 | import path from 'node:path'
3 | import {JSDOM} from 'jsdom'
4 | import {expect, test} from 'vitest'
5 | import {htmlToBlocks} from '../../src'
6 | import defaultSchema from '../fixtures/defaultSchema'
7 | import {createTestKeyGenerator} from '../test-key-generator'
8 |
9 | const blockContentType = defaultSchema
10 | .get('blogPost')
11 | .fields.find((field: any) => field.name === 'body').type
12 |
13 | const html = fs.readFileSync(path.resolve(__dirname, 'lists.html')).toString()
14 |
15 | const json = JSON.parse(
16 | fs.readFileSync(path.resolve(__dirname, 'lists.json'), 'utf-8'),
17 | )
18 |
19 | const keyGenerator = createTestKeyGenerator()
20 |
21 | test(htmlToBlocks.name, () => {
22 | expect(
23 | htmlToBlocks(html, blockContentType, {
24 | parseHtml: (html) => new JSDOM(html).window.document,
25 | keyGenerator,
26 | }),
27 | ).toEqual(json)
28 | })
29 |
--------------------------------------------------------------------------------