{"id":20469908,"url":"https://github.com/beynar/edytor","last_synced_at":"2025-04-13T10:40:33.420Z","repository":{"id":122807002,"uuid":"410362978","full_name":"beynar/edytor","owner":"beynar","description":"Collaborative performant and extensible general purpose rich text editor","archived":false,"fork":false,"pushed_at":"2025-02-27T11:30:56.000Z","size":361,"stargazers_count":7,"open_issues_count":1,"forks_count":2,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-03-27T02:03:40.432Z","etag":null,"topics":["collaborative-editing","editor","realtime","rich-text-editor","richtexteditor","solid-js","vite","wysiwyg","yjs"],"latest_commit_sha":null,"homepage":"https://edytor.pages.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/beynar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-25T19:23:23.000Z","updated_at":"2025-03-18T09:49:03.000Z","dependencies_parsed_at":null,"dependency_job_id":"4c3303bf-ce5e-48a1-ada0-a36f954d1cde","html_url":"https://github.com/beynar/edytor","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beynar%2Fedytor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beynar%2Fedytor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beynar%2Fedytor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beynar%2Fedytor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/beynar","download_url":"https://codeload.github.com/beynar/edytor/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248700868,"owners_count":21147925,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["collaborative-editing","editor","realtime","rich-text-editor","richtexteditor","solid-js","vite","wysiwyg","yjs"],"created_at":"2024-11-15T14:10:53.596Z","updated_at":"2025-04-13T10:40:33.413Z","avatar_url":"https://github.com/beynar.png","language":"TypeScript","readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"cover.jpg\" alt=\"Edytor Logo\" width=\"60%\"/\u003e\n\n\u003cp\u003eA powerful, extensible rich text editor built with Svelte and Y.js\u003c/p\u003e\n\n[![npm version](https://badge.fury.io/js/edytor.svg)](https://badge.fury.io/js/edytor)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)\n[![Svelte v5](https://img.shields.io/badge/Svelte-v5-FF3E00.svg)](https://svelte.dev)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/)\n[![Bundle size](https://deno.bundlejs.com/badge?q=edytor@latest\u0026treeshake=%5B*%5D\u0026config=%7B%22esbuild%22:%7B%22external%22:%5B%22svelte%22,%22clx%22%5D%7D%7D)](https://deno.bundlejs.com/badge?q=edytor@latest\u0026treeshake=%5B*%5D\u0026config=%7B%22esbuild%22:%7B%22external%22:%5B%22svelte%22,%22clx%22%5D%7D%7D)\n\n\u003cp\u003e\n    \u003ca href=\"#features\"\u003eFeatures\u003c/a\u003e •\n    \u003ca href=\"#quick-start\"\u003eQuick Start\u003c/a\u003e •\n  \u003c/p\u003e\n\u003c/div\u003e\n\nEdytor aims to be the premier rich text editor for Svelte, providing the same level of power, flexibility and extensibility that Slate.js offers for React. Like Slate.js, Edytor strives to be heavily customizable and provide a powerful API to build any kind of collaborative rich text editor.\n\n## ⚠️ Work in progress\n\nEdytor is currently in the early stages of development. It is not yet ready for production use.\nI welcome early contributors to help us build a better editor for Svelte.\nJust run it, see what you can do with it, and open issues or PRs.\n\nIf you want to submit an issue please share the json value of the document. It will help understand and fix the issue.\n\n## ✨ Features\n\n- 📑 **Customizable with snippets**: Use snippets to render your own blocks and marks\n- 🎨 **Rich Text Formatting**: Full support for marks, blocks and inline blocks.\n- 🤝 **Real-time Collaboration**: Uses Y.js as data store, collaborative editing is built-in\n- 🔌 **Plugin System**: Extensible architecture for custom features. I try to make every action performed by the editor hackable and preventable to let you build your own features.\n- ⚡ **High Performance**: Optimized for large documents, fine grained update at the leaf level thanks to Y.js and Svelte's reactivity\n- 🔄 **Undo/Redo**: Built-in history management\n- 📦 **Lightweight**: Relatively small bundle size compared to other rich text editors\n- 📦 **AI copilot ready**: Support inline text suggestions for ai completions.\n\n## ✨ Things that are ready\n\n- [x] YJS backed editing\n- [x] Basic block operations and text operations.\n- [x] Stable data structure\n- [x] Undo/Redo\n- [x] Rich text formatting\n- [x] Customizable with snippets\n- [x] Plugin system\n- [x] Text suggestions\n- [x] Inline blocks\n- [x] Nesting\n- [x] Selection + movable blocks\n- [x] Content transformation\n- [x] Content normalization\n- [x] Island blocks\n- [x] Customizable hotkeys\n- [x] Void elements and editable void elements\n- [x] Text spanning deletion\n- [x] Block spanning deletion.\n- [x] Readable JSON data structure\n- [x] Readonly edytor to lightweightly render static content without the Y.js extra works.\n- [x] Children normalization\n\n## ✨ Things that are not ready\n\n- [ ] DND\n- [ ] Battle tested collaborative editing + awareness + providers\n- [ ] Block suggestions\n- [ ] Reactive data (inline)block properties with syncrostate.\n\n## 🧠 Concepts\n\nEdytor structure is built around this key concepts:\n\n- **Blocks**: Container elements like paragraphs, headings, and lists\n- **Content**: The content of a block is an array of inlines blocks or text and marks.\n- **Children**: Children are the blocks that are directly inside a block. They allow an infinite nesting.\n- **Marks**: Texts are simply text with marks that define the formatting.\n- **Inline blocks**: Inline blocks are inline elements that are no editable and render custom components like footnotes, equations, etc.\n\nSchematic example of a document:\n(content and children are not dom element, i put them here to help you understand the structure)\n\n```html\n\u003croot\u003e\n\t\u003cblock\u003e\n\t\t\u003ccontent\u003e\n\t\t\t\u003ctext mark=\"bold\"\u003eHello\u003c/text\u003e\n\t\t\t\u003ctext\u003eWorld\u003c/text\u003e\n\t\t\t\u003cinline-block type=\"footnote\"\u003e\n\t\t\t\t\u003c!-- Inline block are rendered by the user code --\u003e\n\t\t\t\u003c/inline-block\u003e\n\t\t\u003c/content\u003e\n\t\t\u003cchildren\u003e\n\t\t\t\u003cnested-block\u003e\n\t\t\t\t\u003ccontent\u003e\n\t\t\t\t\t\u003ctext\u003eWorld\u003c/text\u003e\n\t\t\t\t\u003c/content\u003e\n\t\t\t\u003c/nested-block\u003e\n\t\t\t\u003cnested-block\u003e\n\t\t\t\t\u003ccontent\u003e\n\t\t\t\t\t\u003ctext\u003eWorld\u003c/text\u003e\n\t\t\t\t\u003c/content\u003e\n\t\t\t\t\u003cchildren\u003e\n\t\t\t\t\t\u003cnested-block\u003e\n\t\t\t\t\t\t\u003ccontent\u003e\n\t\t\t\t\t\t\t\u003ctext\u003eWorld\u003c/text\u003e\n\t\t\t\t\t\t\u003c/content\u003e\n\t\t\t\t\t\u003c/nested-block\u003e\n\t\t\t\t\u003c/children\u003e\n\t\t\t\u003c/nested-block\u003e\n\t\t\u003c/children\u003e\n\t\u003c/block\u003e\n\u003c/root\u003e\n```\n\n### Blocks\n\nBlocks are the container elements like paragraphs, headings, and lists.\nThey have a content that is an array of inlines blocks or text.\nThey may have children that is an array of nested-blocks.\nBlocks can be nested unless they are void or inside an island\n\nAn island is a block that is editable but is structuraly stable and isolated from the rest of the document.\nIt is impossible to merge an island with another block. It is also impossible to move another block inside an island.\nYou may think of an island as a block that is editable but is not completely part of the document structure and isolated from the rest of the document.\n\nA void block is a block which does not have children or whose children are not editable and rendered outside of the edytor core logic.\nVoid blocks can render and edit their content anyway. That is usefull to render caption.\nYou may think of a void block as a block that is completely independent from the rest of the document.\nVoid blocks acts also like an island but are even less editables.\n\n### Text\n\nText is the basic text element that is rendered by the editor. At is core it is a Y.js text with any formatting attributes you want.\n\n### Inlines Block\n\nInlines are inline blocks, useful to render custom components like footnotes, equations, etc.\nThey are rendered by the user code and are not editable nor focusable.\nThey have a data property\n\n## 🚀 Quick Start\n\n### Installation (not published yet)\n\n```bash\nnpm install edytor\n# or\nyarn add edytor\n# or\npnpm add edytor\n```\n\n### Basic Usage\n\n```svelte\n\u003cscript\u003e\n\timport { Edytor } from 'edytor';\n\n\tlet value = {\n\t\tchildren: [\n\t\t\t{\n\t\t\t\ttype: 'paragraph',\n\t\t\t\tcontent: [{ text: 'Hello, World!' }]\n\t\t\t}\n\t\t]\n\t};\n\n\tfunction onChange(newValue) {\n\t\tconsole.log('Document changed:', newValue);\n\t}\n\u003c/script\u003e\n\n\u003cEdytor {value} {onChange} /\u003e\n```\n\n## 📦 Plugins\n\nPlugins are the primary way to extend Edytor's functionality. They allow you to add custom blocks, marks, inline blocks, hotkeys, and hook into various editor events. Each plugin is a function that receives the editor instance and returns a set of definitions and operations.\n\nPlugins are best written in Svelte files (`.svelte`) to take full advantage of Svelte's snippets system and template syntax when defining block and mark snippets. You can define snippets for blocks, marks, inline blocks, hotkeys, and operations and still be able to use them inside the `\u003cscript module\u003e` `\u003c/script\u003e`tag of your file that will export the whole plugin.\n\n### Plugin Structure\n\nA basic plugin structure looks like this:\n\n```typescript\nconst MyPlugin = (editor: Edytor) =\u003e ({\n  // Define custom blocks\n  blocks: {\n    myBlock: {\n      snippet: /* Svelte snippet */,\n      // ... block options\n    }\n  },\n  // Define custom marks\n  marks: {\n    myMark: {\n      snippet: /* Svelte snippet */,\n      // ... mark options\n    }\n  },\n  // Define custom inline blocks\n  inlineBlocks: {\n    myInline: {\n      snippet: /* Svelte snippet */,\n      // ... inline block options\n    }\n  },\n  // Define custom hotkeys\n  hotkeys: {\n    'mod+b': (e) =\u003e {\n      // Handle hotkey\n    }\n  },\n  // Define plugin operations\n  onBeforeOperation: (payload) =\u003e {\n    // Handle before operation\n  },\n  // ... other operations\n});\n```\n\n### Block Definitions\n\nBlocks are the fundamental building blocks of the editor. They can be paragraphs, headings, lists, or any custom block type.\n\n| Option              | Type       | Description                                                   | Example Use Case                                           |\n| ------------------- | ---------- | ------------------------------------------------------------- | ---------------------------------------------------------- |\n| `snippet`           | `Snippet`  | Svelte snippet for rendering the block                        | Defining how a code block renders with syntax highlighting |\n| `void`              | `boolean`  | If true, block is not editable but can have editable captions | Image blocks with editable captions                        |\n| `island`            | `boolean`  | If true, block is editable but structurally isolated          | Code blocks that should be merged with other blocks        |\n| `transformText`     | `Function` | Transform text content within the block                       | Adding syntax highlighting to code blocks in real-time     |\n| `onFocus`           | `Function` | Called when block receives focus                              | Showing a toolbar when focusing a heading block            |\n| `onBlur`            | `Function` | Called when block loses focus                                 | Make an indicator disapear                                 |\n| `onSelect`          | `Function` | Called when block is selected                                 | Showing resize handles when selecting an image block       |\n| `onDeselect`        | `Function` | Called when block is deselected                               | Hiding UI controls when deselecting a block                |\n| `normalizeContent`  | `Function` | Normalize block content after operations                      | Ensuring list items always start with a bullet point       |\n| `normalizeChildren` | `Function` | Normalize block children after operations                     | Ensuring table cells are properly structured               |\n| `schema`            | `any`      | Schema for synchronization state data                         | Defining the structure of a table block's metadata         |\n\n### Mark Definitions\n\nMarks are used for text formatting like bold, italic, or custom formatting.\n\n| Option    | Type      | Description                           | Example Use Case                                          |\n| --------- | --------- | ------------------------------------- | --------------------------------------------------------- |\n| `snippet` | `Snippet` | Svelte snippet for rendering the mark | Rendering highlighted text with a custom background color |\n\n### Plugin Operations\n\nOperations allow you to hook into various editor events and modify behavior.\n\n| Operation                | Description                                          | Example Use Case                                   |\n| ------------------------ | ---------------------------------------------------- | -------------------------------------------------- |\n| `onBeforeOperation`      | Called before any operation is executed              | Validating table cell merges before they happen    |\n| `onAfterOperation`       | Called after any operation is executed               | Updating a table of contents after heading changes |\n| `onChange`               | Called when editor value changes                     | Syncing content with external storage              |\n| `onSelectionChange`      | Called when selection changes                        | Updating a formatting toolbar position             |\n| `placeholder`            | Define placeholder content for empty blocks          | Showing \"Type '/' for commands\" in empty blocks    |\n| `onEdytorAttached`       | Called when editor is attached to DOM                | Initializing third-party libraries                 |\n| `onBlockAttached`        | Called when a block is attached to DOM               | Running some svelte action on the node             |\n| `onTextAttached`         | Called when text is attached to DOM                  | Running some svelte action on the node             |\n| `defaultBlock`           | Define default block type when a new one is inserted | Using different default blocks based on context    |\n| `onDeleteSelectedBlocks` | Called when selected blocks are deleted              | Cleaning up resources when deleting media blocks   |\n| `onBeforeInput`          | Called before input is processed                     | Converting markdown shortcuts as you type          |\n\n### Prevention in Plugin Operations\n\nMany plugin operations and hotkeys receive a `prevent` function as part of their payload. This function is a crucial part of Edytor's plugin system that allows you to:\n\n1. Stop the default behavior of an operation\n2. Register a callback to be executed after the default operation is aborted\n3. Control the flow of operations across multiple plugins\n\nHere's how it works:\n\n```typescript\n// In a hotkey handler precent will also doest a preventDefault on the keyboard event.\nhotkeys: {\n  'mod+b': ({ prevent }) =\u003e {\n    // Prevent default and do nothing\n    prevent();\n\n    // Or register a callback to be executed after the operation is aborted\n    prevent(() =\u003e {\n      // This code runs after the default operation is aborted\n      // Use this to implement your custom behavior\n    });\n  }\n}\n\n// In an operation handler\nonBeforeOperation: ({ prevent, operation, payload }) =\u003e {\n  if (operation === 'splitBlock') {\n    prevent(() =\u003e {\n      // This callback will be executed after the default split operation is aborted\n      // Implement your custom split logic here\n    });\n  }\n}\n```\n\nWhen using multiple plugins, prevention follows these rules:\n\n- If a plugin prevents an operation without providing a callback, the operation is completely stopped\n- If a plugin prevents an operation with a callback, the default operation is aborted and then the callback is executed\n- If multiple plugins try to prevent the same operation, only the first prevention (in plugin order) takes effect\n- If a plugin doesn't call prevent(), the operation continues to the next plugin or executes the default behavior\n\nThis system allows plugins to:\n\n- Completely stop operations when needed\n- Replace default behavior with custom logic\n- Ensure their custom logic runs only after the default behavior is properly aborted\n- Build complex features while maintaining predictable behavior\n\n### Example: Simple Bold Mark Plugin\n\n```svelte\n\n\u003cscript module\u003e\n\texport const boldPlugin = (editor: Edytor) =\u003e ({\n\t\tmarks: {\n\t\t\tbold: {\n\t\t\tsnippet: bold\n\t\t}\n\t},\n\thotkeys: {\n\t\t'mod+b': ({prevent}) =\u003e {\n\t\t\tprevent(()=\u003e{\n\t\t\t\t// Do something\n\t\t\t});\n\t\t}\n\t}\n});\n\u003c/script\u003e\n\n{#snippet bold({content}: MarkSnippetPayload)}\n\t\u003cstrong\u003e{@render content()}\u003c/strong\u003e\n{/snippet}\n```\n\n### Using Plugins\n\nTo use plugins, pass them to the Edytor component. The order of the plugins is important because the plugins are executed in the order they are passed. So if two plugins are trying to render the same block, the first plugin will win. If two pluggins defined the same hotkey and prevent it, the second plugin will not be executed.\n\n```svelte\n\u003cscript\u003e\n\timport { Edytor } from 'edytor';\n\timport { BoldPlugin, HeadingPlugin } from './plugins';\n\n\tconst plugins = [BoldPlugin, HeadingPlugin];\n\u003c/script\u003e\n\n\u003cEdytor {plugins} /\u003e\n```\n\n## Testing\n\nI'm welcome to any contribution to improve the testing.\nIn the end, every block operation should be tested.\nI've implemented a custom jsx parser to simplify testing the editor.\n\nSo instead of defining the value as a json object, you can define the value as a jsx element.\n\n```html\n\u003croot\u003e\n\t\u003cparagraph\u003eHello, World!\u003c/paragraph\u003e\n\u003c/root\u003e\n```\n\nis the same as\n\n```json\n{\n\t\"type\": \"root\",\n\t\"children\": [{ \"type\": \"paragraph\", \"content\": [{ \"text\": \"Hello, World!\" }] }]\n}\n```\n\nYou can also add one or two cursors with the `|` character into the jsx in order to simulate the cursor position\n\n```jsx\n\u003croot\u003e\n\t\u003cparagraph\u003eHello, |World!|\u003c/paragraph\u003e\n\u003c/root\u003e\n```\n\nI've also implemented the `createTestEdytor` that help with creating an edytor instance from a jsx element in order to test various operations on a virtual edytor and test the expected output.\n\n```jsx\ntest('split text', () =\u003e {\n\tconst { edytor, expect } = createTestEdytor(\n\t\t\u003croot\u003e\n\t\t\t\u003cparagraph\u003eHello, |World!\u003c/paragraph\u003e\n\t\t\u003c/root\u003e\n\t);\n\n\tedytor.selection.state.startBlock?.splitBlock({\n\t\tindex: edytor.selection.state.yStart,\n\t\ttext: edytor.selection.state.startText\n\t});\n\n\texpect(\n\t\t\u003croot\u003e\n\t\t\t\u003cparagraph\u003eHello, \u003c/paragraph\u003e\n\t\t\t\u003cparagraph\u003eWorld!\u003c/paragraph\u003e\n\t\t\u003c/root\u003e\n\t);\n});\n```\n\n### Writing plugins\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeynar%2Fedytor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeynar%2Fedytor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeynar%2Fedytor/lists"}