@clevertask/scribe
v0.1.2
Published
A Radix-based Tiptap rich text editor with a Notion-style block interface for viewing and creating content. Perfect for diverse needs like notes, documents, AI chat, Markdown parsing, and comments.
Maintainers
Readme
@clevertask/scribe
A versatile, block-based rich text editor for diverse applications, built with Tiptap and inspired by Notion's intuitive interface. @clevertask/scribe allows you to seamlessly view, create, and edit rich text content, with added support for Markdown parsing.
Features
- Block-based Editing: Enjoy a familiar and intuitive Notion-style editing experience.
- Markdown Support: Parse and render Markdown content effortlessly.
- Markdown Paste: Paste plain-text markdown into the editor and have it converted into rich content automatically.
- Versatile Integration: Easily integrate
@clevertask/scribeinto any project requiring rich text editing. - View and Edit: Seamlessly switch between viewing and editing modes.
- Experimental Table of Contents: Subscribe to heading changes and render an app-owned table of contents outside the editor.
Table of Contents
Installation
npm install @clevertask/scribeUsage
Basic usage
import "@radix-ui/themes/styles.css";
import "@clevertask/scribe/dist/main.css";
import { Theme } from "@radix-ui/themes";
import { Scribe, ScribeRef } from "@clevertask/scribe";
function App() {
const onContentChange = useCallback(
({ markdownContent, htmlContent, jsonContent }: ScribeOnChangeContents) => {
console.log(markdownContent, htmlContent, jsonContent);
},
[],
);
return (
<Theme>
<Scribe onContentChange={onContentChange} />
</Theme>
);
}With ref
import "@radix-ui/themes/styles.css";
import "@clevertask/scribe/dist/main.css";
import { Theme } from "@radix-ui/themes";
import { Scribe, ScribeOnChangeContents } from "@clevertask/scribe";
function App() {
const editor = useRef<ScribeRef>(null);
const resetContent = useCallback(() => {
editor.current.resetContent();
}, []);
return (
<>
<Theme>
<Scribe ref={editor} />
</Theme>
<button onClick={resetContent}>Reset content</button>
</>
);
}Using Your App Theme
import "@radix-ui/themes/styles.css";
import "@clevertask/scribe/dist/main.css";
import { Theme } from "@radix-ui/themes";
import { Scribe } from "@clevertask/scribe";
function App() {
return (
<Theme appearance="dark">
<Scribe />
</Theme>
);
}Experimental Table of Contents
Scribe can expose table-of-contents data without rendering a table-of-contents block inside the editable document. Enable the experimental API with enableTableOfContents, keep the latest items in your app state, and call scrollToTableOfContentsItem when a user selects an entry.
import { Scribe, ScribeRef, ScribeTableOfContentsItem } from "@clevertask/scribe";
import { useRef, useState } from "react";
function DocumentEditor() {
const scribe = useRef<ScribeRef>(null);
const [tableOfContentsItems, setTableOfContentsItems] = useState<ScribeTableOfContentsItem[]>([]);
return (
<>
<Scribe
ref={scribe}
enableTableOfContents
onTableOfContentsChange={setTableOfContentsItems}
/>
{tableOfContentsItems.length > 0 ? (
<nav aria-label="Table of contents">
{tableOfContentsItems.map((item) => (
<button
key={item.id}
type="button"
onClick={() => scribe.current?.scrollToTableOfContentsItem(item)}
>
{item.textContent}
</button>
))}
</nav>
) : null}
</>
);
}The table of contents currently tracks default TipTap heading nodes only. Each item includes the heading text, depth, document position, DOM node, and active/scrolled state. This API is marked experimental while we validate the contract in real document surfaces.
Math Expressions
Scribe's default UI is styled with Radix Themes components. Load @radix-ui/themes/styles.css once in your app alongside @clevertask/scribe/dist/main.css, and render Scribe somewhere inside a Radix <Theme>.
Scribe ships with @tiptap/extension-mathematics. The extension renders math when it receives math nodes in the HTML:
<span data-type="inline-math" data-latex="\alpha"></span>
<div data-type="block-math" data-latex="\sum_{i=1}^{n} x_i"></div>Typing Delimiters (Input Rules)
When typing directly in the editor, the built-in input rules use:
Inline: $$\alpha$$
Block: $$$\sum_{i=1}^{n} x_i$$$Markdown Delimiters
If you are parsing markdown with the Tiptap Markdown extension (not md2html), the tokenizer expects:
Inline: $\alpha$
Block: $$\sum_{i=1}^{n} x_i$$If your content arrives as HTML (for example from a server), use the helper below to convert legacy delimiters into the HTML nodes that the math extension understands.
Props
| Prop | Type | Default | Description |
| ------------------------- | ------------------------------------------------------------------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| content | string | undefined | The initial content of the editor. Controlled updates through this prop are deprecated; prefer ScribeRef.setContent for programmatic updates after mount. |
| onContentChange | (content: ScribeOnChangeContents) => void; | undefined | A callback function triggered whenever the editor's content changes. It receives an object containing the current content in various formats (jsonContent, htmlContent, markdownContent). Internal table-of-contents metadata updates are ignored. |
| editable | boolean | true | Controls whether the editor is editable. |
| autoFocus | boolean | false | Controls whether the editor should automatically focus when mounted. |
| extensions | Extension[] | undefined | You can set your own extensions for the text editor. For more information, check the tip tap extensions docs |
| editorProps | EditorProps | undefined | A tiptap-based prop to handle advanced use cases, you can read about it on their documentation |
| showBarMenu | boolean | true | Determines whether to show the text editor top menu bar or not. This menu bar shows options to format the text |
| placeholderText | string | Type "/" for commands... | Change the initial placeholder for your text editor |
| editorContentStyle | React.CSSProperties | undefined | You can send a CSS object to add styles to the editor content container. Useful if you want to limit the editor's height. |
| editorContentClassName | string | undefined | The same idea of editorContentStyle but with classes. |
| mainContainerStyle | React.CSSProperties | undefined | You can send a CSS object to style the main editor container |
| mainContainerClassName | string | undefined | The same idea of mainContainerStyle but with classes. |
| onKeyDown | KeyboardEventHandler | undefined | A callback function that is triggered when a key is pressed within the editor. This allows you to handle custom keyboard shortcuts. For example, you can use this prop to implement a "send message" functionality when Ctrl + Enter is pressed. |
| enableTableOfContents | boolean | false | Experimental. Enables the app-owned table-of-contents API for heading nodes. |
| onTableOfContentsChange | (items: ScribeTableOfContentsItem[], isCreate?: boolean) => void | undefined | Experimental. Receives table-of-contents items whenever heading text, structure, or active/scrolled state changes. |
Helper Functions
md2html
export declare function md2html(md: string): string;Convert markdown to html. Useful if you're rendering an AI-based response, or if you were storing content on markdown in your database and want to show it on the text editor. This function sanitizes the content to prevent XSS attacks.
Editable Scribe instances also use this conversion internally when you paste plain-text markdown into the editor.
Usage Example:
import { md2html, Scribe } from "@clevertask/scribe";
import { Flex, Heading } from "@radix-ui/themes";
import { Message, useChat } from "@ai-sdk/react";
const ChatMessages = () => {
const { messages } = useChat({
/* For more info, see https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-chat */
});
return messages.map((message) => (
<Flex key={message.id} direction="column" mb="4">
<Heading size="4">{`${message.role}: `}</Heading>
<Scribe editable={false} showBarMenu={false} content={md2html(message.content)} />
</Flex>
));
};html2md
export declare function html2md(html: string): string;Convert html to markdown. Useful if you want to send a text to an AI model by keeping the text format with markdown. This function sanitizes the content to prevent XSS attacks.
Usage Example:
import { html2md, Scribe } from "@clevertask/scribe";
const md = html2md("<h1>Hello world</h1>"); // Output: # Hello worldNote: The Scribe component already exposes a property called
markdownContentwhen theonContentChangeis used. In fact, themarkdownContentis the output of the usage of thehtml2mdfunction.
convertLegacyMathDelimiters
export declare function convertLegacyMathDelimiters(input: string): string;Convert legacy math delimiters into the HTML nodes required by the mathematics extension. This is useful when you receive HTML from a server that contains legacy math like \(...\) or \[...\].
This helper is primarily for consumers upgrading from older Scribe versions who stored math expressions using the legacy formats. It aims to be accurate, but the previous format was ambiguous (no explicit $$ delimiters), so conversion is best-effort and not guaranteed in every case.
Supported legacy delimiters:
\(...\)for inline math\[...\]for block math(...)and[...]when the content looks like LaTeX (contains\,^, or_)
Usage Example:
import { convertLegacyMathDelimiters, md2html, Scribe } from "@clevertask/scribe";
const html = md2html(convertLegacyMathDelimiters(rawMarkdown));
// OR
const html = convertLegacyMathDelimiters(htmlContent);
<Scribe editable={false} showBarMenu={false} content={html} />;If you already receive HTML from the server, call convertLegacyMathDelimiters directly on that HTML before rendering.
Roadmap
We're constantly working to improve @clevertask/scribe. Here are some features we're planning to implement:
- New default blocks/extensions: Such as image, video, callout, and table blocks
- E2E tests: It will ensure this component's working as expected.
We're excited about these upcoming features and welcome any feedback or contributions from the community. If you have any suggestions or would like to contribute to any of these features, please open an issue or submit a pull request on our GitHub repository.
Release Process
This package is automatically published to npm when a new release is created on GitHub. To create a new release:
- Update the version in
package.jsonaccording to semantic versioning rules. - Commit the version change:
git commit -am "Bump version to x.x.x" - Create a new tag:
git tag x.x.x - Push the changes and the tag:
git push && git push --tags - Go to the GitHub repository and create a new release, selecting the tag you just created.
The GitHub Action will automatically build, test, and publish the new version to npm.
License
MIT
Credits
This project is built on top of the excellent BlockEditor repository by Sachin Chaurasiya. We extend our sincere gratitude for their work. <3
