## File: README.md # :baby_bottle:  Milkdown [![ci][ci-badge]][ci-link] ![ts][ts-badge] [![download-badge]][download-link] ![version][version-badge] [![discord-badge]][discord-link] ![commit][commit-badge] ![license][license-badge] [](https://deepwiki.com/Milkdown/milkdown) A plugin-driven WYSIWYG markdown Editor, inspired by [Typora](https://typora.io/), built on top of [prosemirror](https://prosemirror.net/) and [remark](https://github.com/remarkjs/remark). The website is designed by [Meo](https://meo.cool/) and [Mirone](https://github.com/Saul-Mirone). Powered by [Theme Nord](https://www.nordtheme.com/) and [Material Design](https://material.io/design). # Documentation For more information, please check our [official documentation website](https://milkdown.dev/). # What's Next You can check our [Milkdown TODO](https://github.com/orgs/Milkdown/projects/1) project page to know what's on the plan. You can also check [milestones](https://github.com/Milkdown/milkdown/milestones) to know what's being worked on. # Community Welcome to join our [Discord community][discord-link]. # Contributing Follow our [contribution guide](https://github.com/Milkdown/milkdown/blob/main/CONTRIBUTING.md) to learn how to contribute to milkdown. # Contributor Special thanks to [@Meo](https://meo.cool/) for her work in design. # Sponsors If you like this project, please consider fund me to help the maintenance. # Thanks Thanks to the following projects and companies for their support to milkdown and the open source community.             # License [MIT](/LICENSE) [ci-badge]: https://github.com/Milkdown/milkdown/actions/workflows/ci.yml/badge.svg [ci-link]: https://github.com/Milkdown/milkdown/actions/workflows/ci.yml [ts-badge]: https://badgen.net/badge/-/TypeScript/blue?icon=typescript&label [download-badge]: https://img.shields.io/npm/dm/@milkdown/core [download-link]: https://www.npmjs.com/search?q=%40milkdown [version-badge]: https://img.shields.io/npm/v/@milkdown/core [commit-badge]: https://img.shields.io/github/commit-activity/m/Milkdown/milkdown [license-badge]: https://img.shields.io/github/license/Milkdown/milkdown [discord-badge]: https://img.shields.io/discord/870181036041060352 [discord-link]: https://discord.gg/SdMnrSMyBX [vercel-oss-badge]: https://vercel.com/oss/program-badge.svg --- ## File: .changeset/README.md # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) --- ## File: docs/api/component-code-block.md # Code Block Component The `codeBlock` component renders a code block with a [Codemirror](https://codemirror.net/) editor. The component provides following features: - [x] Language picker - [x] Syntax highlighting - [x] Line numbers - [x] Code auto-completion and folding - [x] Code search and replace > The component itself doesn't provide any styling. > > You need to write your own CSS to style it. # Usage ```typescript import { defaultKeymap } from '@codemirror/commands' import { languages } from '@codemirror/language-data' import { oneDark } from '@codemirror/theme-one-dark' import { keymap } from '@codemirror/view' import { codeBlockComponent, codeBlockConfig, } from '@milkdown/components/code-block' import { defaultValueCtx, Editor } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' import { basicSetup } from 'codemirror' await Editor.make() .config((ctx) => { ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, languages, extensions: [basicSetup, oneDark, keymap.of(defaultKeymap)], renderLanguage: (language, selected) => selected ? `βœ” ${language}` : language, })) }) .use(commonmark) .use(codeBlockComponent) .create() ``` ::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/component-code-block"} --- # Configuration You can configure the component by updating the `codeBlockConfig` ctx in `editor.config`. ## Configuration Options | Option | Type | Default | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------- | | `extensions` | `Extension[]` | `[]` | Codemirror extensions | | `languages` | `LanguageDescription[]` | `[]` | Codemirror language data | | `expandIcon` | `string` | `'⬇'` | Icon for expanding the language picker | | `searchIcon` | `string` | `'πŸ”'` | Icon for search | | `clearSearchIcon` | `string` | `'⌫'` | Icon for clearing the search input | | `searchPlaceholder` | `string` | `'Search language'` | Placeholder for the search input | | `noResultText` | `string` | `'No result'` | Text when no language matches | | `copyText` | `string` | `'Copy'` | Text for the copy button | | `copyIcon` | `string` | `'πŸ“‹'` | Icon for the copy button | | `onCopy` | `(text: string) => void` (optional) | `() => {}` | Callback when code is copied | | `renderLanguage` | `(language: string, selected: boolean) => string` | `(language) => language` | Function to render a language in the picker (must return a string) | | `renderPreview` | `renderPreview: (language: string, content: string, applyPreview: (value: null \| string \| HTMLElement) => void) => void \| null \| string \| HTMLElement` | `() => null` | Function to render a preview (return null to hide, reutrn undefined for async rendering) | | `previewToggleButton` | `(previewOnlyMode: boolean) => string` | `(mode) => mode ? 'Edit' : 'Hide'` | Function to render the preview toggle button (must return a string) | | `previewLabel` | `string` | `'Preview'` | Label for the preview panel | | `previewOnlyByDefault` | `boolean` | `true` for `readonly` | Whether to show the preview only by default | | `previewLoading` | `string \| HTMLElement` | `'Loading...'` | Content for the async preview loading | --- ## `languages` Codemirror language data list. You can either import the language data from `@codemirror/language-data` or provide your own language data. ```typescript import { LanguageDescription } from '@codemirror/language' import { languages } from '@codemirror/language-data' import { codeBlockConfig } from '@milkdown/components/code-block' const myLanguages = [ LanguageDescription.of({ name: 'JavaScript', alias: ['ecmascript', 'js', 'node'], extensions: ['js', 'mjs', 'cjs'], load() { return import('@codemirror/lang-javascript').then((m) => m.javascript()) }, }), LanguageDescription.of({ name: 'CSS', extensions: ['css', 'pcss'], load() { return import('@codemirror/lang-css').then((m) => m.css()) }, }), ] ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, languages: myLanguages, })) ``` ## `extensions` Codemirror extensions list. You can use the `basicSetup` extension to enable basic features like line numbers, syntax highlighting, theme, etc. ```typescript import { defaultKeymap, indentWithTab } from '@codemirror/commands' import { oneDark } from '@codemirror/theme-one-dark' import { codeBlockConfig } from '@milkdown/components/code-block' import { basicSetup } from 'codemirror' ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, extensions: [ keymap.of(defaultKeymap.concat(indentWithTab)), basicSetup, oneDark, ], })) ``` ## `renderLanguage` A function to render the language list item in the language picker. **Must return a string.** ```typescript import { codeBlockConfig } from '@milkdown/components/code-block' ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, renderLanguage: (language, selected) => selected ? `βœ” ${language}` : language, })) ``` ## `expandIcon`, `searchIcon`, `clearSearchIcon`, `copyIcon`, `copyText`, `searchPlaceholder`, `noResultText`, `previewLabel` All of these options are **strings**. You can use any string or emoji. ```typescript import { codeBlockConfig } from '@milkdown/components/code-block' ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, expandIcon: 'πŸ”½', searchIcon: 'πŸ”', clearSearchIcon: '❌', copyIcon: 'πŸ“„', copyText: 'Copy code', searchPlaceholder: 'Find a language...', noResultText: 'No language found', previewLabel: 'Preview', })) ``` ## `onCopy` A callback function that is called when the copy button is pressed. ```typescript import { codeBlockConfig } from '@milkdown/components/code-block' ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, onCopy: (text) => { alert('Copied: ' + text) }, })) ``` ## `renderPreview` A function to render the preview of the code block. Can return a string, HTMLElement, null (to hide the preview), or undefined (to show `previewLoading` and asyncly render the preview). ```typescript import { codeBlockConfig } from '@milkdown/components/code-block' ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, renderPreview: (language, content, applyPreview) => { // sync if (language === 'latex' && content.length > 0) { return renderLatexToDOM(content) } // async if (language === 'JavaScript') { compileJs(content).then((res) => applyPreview(res)) return } // hide the preview return null }, })) ``` ## `previewToggleButton` A function to render the text for the preview toggle button. **Must return a string.** ```typescript import { codeBlockConfig } from '@milkdown/components/code-block' ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, previewToggleButton: (previewOnlyMode) => previewOnlyMode ? 'Show code' : 'Hide code', })) ``` ## `previewOnlyByDefault` Whether to show the preview only by default. ```typescript import { codeBlockConfig } from '@milkdown/components/code-block' ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, previewOnlyByDefault: false, })) ``` ## `previewLoading` Content for the async preview loading. ```typescript import { codeBlockConfig } from '@milkdown/components/code-block' ctx.update(codeBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, previewLoading: 'Loading...', })) ``` --- ## File: docs/api/component-image-block.md # Image Block Component The `imageBlock` component renders an image into a block. In markdown, all images are rendered as inline images. This component allows you to render an image as a block. This component provides the following features: - [x] Image resize handle - [x] Image caption - [x] Image link input - [x] Empty image block placeholder - [x] Image upload > The component itself doesn't provide any styling. > > You need to write your own CSS to style it. # Usage ```typescript import { imageBlockComponent, imageBlockConfig, } from '@milkdown/components/image-block' import { defaultValueCtx, Editor } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' await Editor.make().use(commonmark).use(imageBlockComponent).create() ``` ::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/component-image-block"} --- # Configuration You can configure the component by updating the `imageBlockConfig` ctx in `editor.config`. ## Configuration Options | Option | Type | Default | Description | | ------------------------ | -------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | | `imageIcon` | `string \| undefined` | `'🌌'` | Icon for the empty image block placeholder | | `captionIcon` | `string \| undefined` | `'πŸ’¬'` | Icon for the caption toggle button | | `uploadButton` | `string \| undefined` | `'Upload file'` | Content for the upload button | | `confirmButton` | `string \| undefined` | `'Confirm ⏎'` | Content for the confirm button | | `uploadPlaceholderText` | `string` | `'or paste the image link ...'` | Placeholder text for the image block placeholder | | `captionPlaceholderText` | `string` | `'Image caption'` | Placeholder text for the caption input | | `onUpload` | `(file: File) => Promise` | `(file) => Promise.resolve(URL.createObjectURL(file))` | Function called when an image is uploaded; must return a Promise with the image URL | | `proxyDomURL` | `(url: string) => Promise \| string` | `undefined` | Optional function to proxy the image URL | | `onImageLoadError` | `(event: Event) => void \| Promise` | `undefined` | Optional callback when an image fails to load (e.g. invalid URL or network error) | | `maxWidth` | `number \| undefined` | `undefined` | Optional maximum display width in pixels for the image | | `maxHeight` | `number \| undefined` | `undefined` | Optional maximum display height in pixels for the image | --- ## `onUpload` A function that is called when the image is chosen by the file picker. You should return a promise that resolves to the URL of the uploaded image. ```typescript import { imageBlockConfig } from '@milkdown/components/image-block' ctx.update(imageBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, onUpload: async (file: File) => { const url = await YourUploadAPI(file) return url }, })) ``` ## `imageIcon`, `captionIcon`, `uploadButton`, `confirmButton`, `uploadPlaceholderText`, `captionPlaceholderText` All of these options are **strings**. You can use any string or emoji. ```typescript import { imageBlockConfig } from '@milkdown/components/image-block' ctx.update(imageBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, imageIcon: 'πŸ–ΌοΈ', captionIcon: 'πŸ“', uploadButton: 'Upload Image', confirmButton: 'Confirm', uploadPlaceholderText: 'or paste an image URL', captionPlaceholderText: 'Add a caption', })) ``` ## `proxyDomURL` Whether to proxy the image link to another URL when rendering. The value should be a function that returns a string or a promise of a string. ```typescript import { imageBlockConfig } from '@milkdown/components/image-block' ctx.update(imageBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, proxyDomURL: (originalURL: string) => { return `https://example.com/${originalURL}` }, })) // Promise is also supported ctx.update(imageBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, proxyDomURL: async (originalURL: string) => { const response = await fetch( `https://api.example.com/proxy?url=${originalURL}` ) const url = await response.text() return url }, })) ``` ## `onImageLoadError` Optional callback invoked when an image fails to load (invalid URL, CORS, 404, etc.). Use it to show a message, fallback UI, or report errors. May be sync or async (`Promise`). ```typescript import { imageBlockConfig } from '@milkdown/components/image-block' ctx.update(imageBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, onImageLoadError: (event: Event) => { console.error('Image failed to load', event) // e.g. show toast or replace with placeholder }, })) // Async is also supported ctx.update(imageBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, onImageLoadError: async (event: Event) => { await reportToAnalytics('image_load_error', event) }, })) ``` ## `maxWidth` and `maxHeight` Optional maximum dimensions (in pixels) for displayed images. Images exceeding these bounds will be scaled down while maintaining their aspect ratio. These constraints also apply during drag-to-resize. ```typescript import { imageBlockConfig } from '@milkdown/components/image-block' ctx.update(imageBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, maxWidth: 800, maxHeight: 600, })) ``` --- ## File: docs/api/component-image-inline.md # Image Inline Component The `imageInline` component provides placeholder and uploader features for inline images. - [x] Image placeholder - [x] Image upload - [x] Image link input > The component itself doesn't provide any styling. > > You need to write your own CSS to style it. # Usage ```typescript import { imageInlineComponent, inlineImageConfig, } from '@milkdown/components/image-inline' import { defaultValueCtx, Editor } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' await Editor.make().use(commonmark).use(imageInlineComponent).create() ``` ::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/component-image-inline"} --- # Configuration You can configure the component by updating the `inlineImageConfig` ctx in `editor.config`. ## Configuration Options | Option | Type | Default | Description | | ----------------------- | -------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | | `imageIcon` | `string \| undefined` | `'🌌'` | Icon for the empty inline image placeholder | | `uploadButton` | `string \| undefined` | `'Upload'` | Text for the upload button | | `confirmButton` | `string \| undefined` | `'⏎'` | Text for the confirm button | | `uploadPlaceholderText` | `string` | `'/Paste'` | Placeholder text for the upload button | | `onUpload` | `(file: File) => Promise` | `(file) => Promise.resolve(URL.createObjectURL(file))` | Function called when an image is uploaded; must return a Promise with the image URL | | `proxyDomURL` | `(url: string) => Promise \| string` | `undefined` | Optional function to proxy the image URL | --- ## `onUpload` A function that is called when the image is chosen by the file picker. You should return a promise that resolves to the URL of the uploaded image. ```typescript import { inlineImageConfig } from '@milkdown/components/image-inline' ctx.update(inlineImageConfig.key, (defaultConfig) => ({ ...defaultConfig, onUpload: async (file: File) => { const url = await YourUploadAPI(file) return url }, })) ``` ## `imageIcon`, `uploadButton`, `confirmButton`, `uploadPlaceholderText` All of these options are **strings**. You can use any string or emoji. ```typescript import { inlineImageConfig } from '@milkdown/components/image-inline' ctx.update(inlineImageConfig.key, (defaultConfig) => ({ ...defaultConfig, imageIcon: 'πŸ–ΌοΈ', uploadButton: 'Upload', confirmButton: 'Confirm', uploadPlaceholderText: 'Paste URL', })) ``` ## `proxyDomURL` Whether to proxy the image link to another URL when rendering. The value should be a function that returns a string or a promise of a string. ```typescript import { inlineImageConfig } from '@milkdown/components/image-inline' ctx.update(inlineImageConfig.key, (defaultConfig) => ({ ...defaultConfig, proxyDomURL: (originalURL: string) => { return `https://example.com/${originalURL}` }, })) // Promise is also supported ctx.update(inlineImageConfig.key, (defaultConfig) => ({ ...defaultConfig, proxyDomURL: async (originalURL: string) => { const response = await fetch( `https://api.example.com/proxy?url=${originalURL}` ) const url = await response.text() return url }, })) ``` --- ## File: docs/api/component-link-tooltip.md # Link Tooltip Component The `linkTooltip` component provides a tooltip for editing and previewing links. It provides the following features: - [x] Edit link - [x] Preview link - [x] Copy link - [x] Programmatic link API - [x] addLink - [x] editLink - [x] removeLink > The component itself doesn't provide any styling. > > You need to write your own CSS to style it. # Usage ```typescript import { configureLinkTooltip, linkTooltipPlugin, linkTooltipConfig, } from '@milkdown/components/link-tooltip' import { defaultValueCtx, Editor } from '@milkdown/kit/core' import { commonmark, linkSchema } from '@milkdown/kit/preset/commonmark' const editor = await Editor.make() .config(configureLinkTooltip) .use(commonmark) .use(linkTooltipPlugin) .create() ``` ::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/component-link-tooltip"} # Configuration You can configure the component by updating the `linkTooltipConfig` ctx in `editor.config`. ## Configuration Options | Option | Type | Default | Description | | ------------------ | ------------------------ | ----------------- | --------------------------------------------------- | | `linkIcon` | `string` | `'πŸ”—'` | Icon for the link preview (click to copy the link) | | `editButton` | `string` | `'✎'` | Icon/text for the edit button | | `removeButton` | `string` | `'⌫'` | Icon/text for the remove button | | `confirmButton` | `string` | `'Confirm ⏎'` | Icon/text for the confirm button in the link editor | | `onCopyLink` | `(link: string) => void` | `() => {}` | Callback triggered when the link is copied | | `inputPlaceholder` | `string` | `'Paste link...'` | Placeholder text in the link editor input | --- ## `linkIcon`, `editButton`, `removeButton`, `confirmButton`, `inputPlaceholder` All of these options are **strings**. You can use any string or emoji. ```typescript import { linkTooltipConfig } from '@milkdown/components/link-tooltip' ctx.update(linkTooltipConfig.key, (defaultConfig) => ({ ...defaultConfig, linkIcon: 'πŸ”—', editButton: '✎', removeButton: '❌', confirmButton: 'βœ”οΈ', inputPlaceholder: 'Paste link here', })) ``` ## `onCopyLink` A callback function triggered when the link is copied. ```typescript import { linkTooltipConfig } from '@milkdown/components/link-tooltip' ctx.update(linkTooltipConfig.key, (defaultConfig) => ({ ...defaultConfig, onCopyLink: (link: string) => { console.log('Link copied:', link) toast('Link copied') }, })) ``` # API The `linkTooltip` component provides the following API: ### `insertLink` Insert a link at the given range. > The following example is just a simple implementation, you can customize it according to your needs. ```typescript import { linkTooltipAPI, linkTooltipState, } from '@milkdown/components/link-tooltip' import { editorViewCtx } from '@milkdown/kit/core' function addLink(ctx: Ctx) { const view = ctx.get(editorViewCtx) const { selection, doc } = view.state // already in edit mode if (ctx.get(linkTooltipState.key).mode === 'edit') return const has = doc.rangeHasMark( selection.from, selection.to, linkSchema.type(ctx) ) // range already has link if (has) return ctx.get(linkTooltipAPI.key).addLink(selection.from, selection.to) } ``` ### `editLink` Edit the link at the given range and mark. > The following example is just a simple implementation, you can customize it according to your needs. ```typescript import { linkTooltipAPI, linkTooltipState, } from '@milkdown/components/link-tooltip' import { editorViewCtx } from '@milkdown/kit/core' function editLink(ctx: Ctx) { const view = ctx.get(editorViewCtx) const { selection, doc } = view.state const node = view.state.doc.nodeAt(selection.from) if (!node) return const mark = node.marks.find( (mark) => mark.type === linkSchema.mark.type(ctx) ) if (!mark) return ctx.get(linkTooltipAPI.key).editLink(mark, selection.from, selection.to) } ``` ### `removeLink` Remove the link at the given range. > The following example is just a simple implementation, you can customize it according to your needs. ```typescript import { linkTooltipAPI, linkTooltipState, } from '@milkdown/components/link-tooltip' import { editorViewCtx } from '@milkdown/kit/core' function removeLink(ctx: Ctx) { const view = ctx.get(editorViewCtx) const { selection, doc } = view.state ctx.get(linkTooltipAPI.key).removeLink(selection.from, selection.to) } ``` --- ## File: docs/api/component-list-item-block.md # List Item Component The `listItemBlock` component provides custom renderer for ordered/bullet/todo list items. > The component itself doesn't provide any styling. > > You need to write your own CSS to style it. # Usage ```typescript import { listItemBlockComponent, listItemBlockConfig, } from '@milkdown/components/list-item-block' import { Editor } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' import { gfm } from '@milkdown/kit/preset/gfm' await Editor.make() .use(commonmark) .use(gfm) .use(listItemBlockComponent) .create() ``` ::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/component-list-item"} --- # Customization You can write your own renderer for list items by updating the `listItemBlockConfig` ctx in `editor.config`. ## Configuration Options | Option | Type | Default | Description | | ------------- | ----------------------------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------------- | | `renderLabel` | `(props: { label: string; listType: string; readonly?: boolean; checked?: boolean }) => string` | See below | Function to render the label for each list item. Must return a string. | **Default:** ```typescript ;({ label, listType, checked }) => { const content = checked == null ? listType === 'bullet' ? 'β¦Ώ' : label : checked ? 'β˜‘' : 'β–‘' return content } ``` **Example:** ```typescript import { listItemBlockConfig } from '@milkdown/components/list-item-block' ctx.set(listItemBlockConfig.key, { renderLabel: ({ label, listType, checked, readonly }) => { if (checked == null) { if (listType === 'bullet') return 'β€’' return label // e.g. '1.', '2.', ... } return checked ? '[x]' : '[ ]' }, }) ``` --- ## File: docs/api/component-table-block.md # Table Block Component The `tableBlock` component provides a lot of features for working with tables. It provides the following features: - [x] Row and column drag and drop - [x] Row and column insert and delete - [x] Text alignment in columns > The component itself doesn't provide any styling. > > You need to write your own CSS to style it. # Usage ```typescript import { tableBlock, tableBlockConfig } from '@milkdown/components/table-block' import { Editor } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' import { gfm } from '@milkdown/kit/preset/gfm' await Editor.make().use(commonmark).use(gfm).use(tableBlock).create() ``` ::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/component-table-block"} --- # Configuration You can configure the component by updating the `tableBlockConfig` ctx in `editor.config`. ## Configuration Options | Option | Type | Default | Description | | -------------- | ------------------------------------ | --------- | -------------------------------------------------------------------------- | | `renderButton` | `(renderType: RenderType) => string` | See below | Function to render the button for each table action. Must return a string. | Where `RenderType` is one of: - `'add_row'` - `'add_col'` - `'delete_row'` - `'delete_col'` - `'align_col_left'` - `'align_col_center'` - `'align_col_right'` - `'col_drag_handle'` - `'row_drag_handle'` **Default:** ```typescript ;(renderType) => { switch (renderType) { case 'add_row': return '+' case 'add_col': return '+' case 'delete_row': return '-' case 'delete_col': return '-' case 'align_col_left': return 'left' case 'align_col_center': return 'center' case 'align_col_right': return 'right' case 'col_drag_handle': return '=' case 'row_drag_handle': return '=' } } ``` **Example:** ```typescript import { tableBlockConfig } from '@milkdown/components/table-block' ctx.update(tableBlockConfig.key, (defaultConfig) => ({ ...defaultConfig, renderButton: (renderType) => { switch (renderType) { case 'add_row': return 'βž• Row' case 'add_col': return 'βž• Col' case 'delete_row': return 'πŸ—‘οΈ Row' case 'delete_col': return 'πŸ—‘οΈ Col' case 'align_col_left': return '⬅️' case 'align_col_center': return '↔️' case 'align_col_right': return '➑️' case 'col_drag_handle': return '||' case 'row_drag_handle': return '==' } }, })) ``` --- ## File: docs/api/core.md # @milkdown/core The core module for milkdown. # Editor @Editor @EditorStatus @OnStatusChange --- # Internal Plugins ## Config @config ### Timer @ConfigReady ## Init @init ### Timer @InitReady ### Slice @initTimerCtx @editorCtx @prosePluginsCtx @inputRulesCtx @nodeViewCtx @markViewCtx @remarkPluginsCtx @remarkCtx @remarkStringifyOptionsCtx ## Schema @schema ### Timer @SchemaReady ### Slice @schemaTimerCtx @nodesCtx @marksCtx @schemaCtx ## Parser @parser ### Timer @ParserReady ### Slice @parserTimerCtx @parserCtx ## Serializer @serializer ### Timer @SerializerReady ### Slice @serializerTimerCtx @serializerCtx ## Commands @commands @CommandManager @createCmdKey @CommandChain ### Timer @CommandsReady ### Slice @commandsTimerCtx @commandsCtx ## Keymap @keymap @KeymapManager ### Timer @KeymapReady ### Slice @keymapTimerCtx @keymapCtx ## Paste Rules @pasteRule ### Timer @PasteRulesReady ### Slice @pasteRulesTimerCtx @pasteRulesCtx @PasteRule ## EditorState @editorState ### Timer @EditorStateReady ### Slice @editorStateTimerCtx @editorStateCtx @editorStateOptionsCtx ## EditorView @editorView ### Timer @EditorViewReady ### Ctx @editorViewTimerCtx @defaultValueCtx @rootCtx @rootDOMCtx @rootAttrsCtx @editorViewCtx @editorViewOptionsCtx --- ## File: docs/api/crepe.md # @milkdown/crepe The crepe editor, built on top of milkdown. ## Features Crepe provides a rich set of features that can be enabled or disabled through configuration. By default, most features are enabled except for `TopBar` and `AI`: ```typescript const defaultFeatures: Record = { [Crepe.Feature.Cursor]: true, [Crepe.Feature.ListItem]: true, [Crepe.Feature.LinkTooltip]: true, [Crepe.Feature.ImageBlock]: true, [Crepe.Feature.BlockEdit]: true, [Crepe.Feature.Placeholder]: true, [Crepe.Feature.Toolbar]: true, [Crepe.Feature.CodeMirror]: true, [Crepe.Feature.Table]: true, [Crepe.Feature.Latex]: true, [Crepe.Feature.TopBar]: false, [Crepe.Feature.AI]: false, } ``` You can disable specific features by setting them to `false` in the `features` configuration. ## Icon Configuration Many features allow customizing their icons. You can provide icons as strings: ```typescript const config: CrepeConfig = { featureConfigs: { [Crepe.Feature.Toolbar]: { boldIcon: '...', italicIcon: '...', }, }, } ``` ## Configuration The Crepe editor can be configured through the `CrepeConfig` interface: ```typescript interface CrepeConfig { features?: Partial> // Enable/disable specific features featureConfigs?: CrepeFeatureConfig // Configure individual features root?: Node | string | null // Root element for the editor defaultValue?: DefaultValue // Initial content } ``` ### Builder Configuration The `CrepeBuilder` can be configured through the `CrepeBuilderConfig` interface: ```typescript interface CrepeBuilderConfig { /// The root element for the editor. /// Supports both DOM nodes and CSS selectors, /// If not provided, the editor will be appended to the body. root?: Node | string | null /// The default value for the editor. defaultValue?: DefaultValue } ``` ### Feature Configurations Each feature can be configured individually. Here are the available configurations for each feature: #### Cursor Feature ```typescript interface CursorFeatureConfig { color?: string | false // Custom cursor color width?: number // Cursor width in pixels virtual?: boolean // Enable/disable virtual cursor } // Example: const config: CrepeConfig = { features: { [Crepe.Feature.Cursor]: true, }, featureConfigs: { [Crepe.Feature.Cursor]: { color: '#ff0000', width: 2, virtual: true, }, }, } ``` #### ListItem Feature ```typescript interface ListItemFeatureConfig { bulletIcon?: string // Custom bullet list icon checkBoxCheckedIcon?: string // Custom checked checkbox icon checkBoxUncheckedIcon?: string // Custom unchecked checkbox icon } // Example: const config: CrepeConfig = { features: { [Crepe.Feature.ListItem]: true, }, featureConfigs: { [Crepe.Feature.ListItem]: { bulletIcon: customBulletIcon, checkBoxCheckedIcon: customCheckedIcon, checkBoxUncheckedIcon: customUncheckedIcon, }, }, } ``` #### LinkTooltip Feature ```typescript interface LinkTooltipFeatureConfig { linkIcon?: string // Custom link icon editButton?: string // Custom edit button icon removeButton?: string // Custom remove button icon confirmButton?: string // Custom confirm button icon inputPlaceholder?: string // Placeholder text for link input onCopyLink?: (link: string) => void // Callback when link is copied } // Example: const config: CrepeConfig = { features: { [Crepe.Feature.LinkTooltip]: true, }, featureConfigs: { [Crepe.Feature.LinkTooltip]: { inputPlaceholder: 'Enter URL...', onCopyLink: () => console.log('Link copied'), }, }, } ``` #### ImageBlock Feature ```typescript interface ImageBlockFeatureConfig { // Inline image configuration inlineUploadButton?: string inlineImageIcon?: string inlineConfirmButton?: string inlineUploadPlaceholderText?: string inlineOnUpload?: (file: File) => Promise // Block image configuration blockUploadButton?: string blockImageIcon?: string blockCaptionIcon?: string blockConfirmButton?: string blockCaptionPlaceholderText?: string blockUploadPlaceholderText?: string blockOnUpload?: (file: File) => Promise // Common configuration onUpload?: (file: File) => Promise proxyDomURL?: string } // Example: const config: CrepeConfig = { features: { [Crepe.Feature.ImageBlock]: true, }, featureConfigs: { [Crepe.Feature.ImageBlock]: { inlineUploadButton: 'Upload Image', blockCaptionPlaceholderText: 'Add image caption...', onUpload: async (file) => { // Handle file upload return 'https://example.com/image.jpg' }, }, }, } ``` > **Note**: The `onUpload` callback is used for both the click-to-upload button and drag-and-drop file uploads. > Crepe has a built-in upload plugin (`@milkdown/plugin-upload`) that handles drag-and-drop and paste image uploads. > When the `ImageBlock` feature is enabled, the upload plugin will use the `onUpload` from the image block configuration to process files and create `image-block` nodes. > If no custom `onUpload` is provided, files will be converted to local blob URLs by default. #### BlockEdit Feature ``` /* Detailed source-code truncated for AI context efficiency. */ ``` > **Note**: Setting any group or item to `null` will prevent it from being displayed in the menu. This is useful for customizing which options are available to users. For example, setting `h2: null` will hide the H2 heading option, and setting `textGroup: null` will hide the entire text group. #### Toolbar Feature ```typescript interface ToolbarFeatureConfig { boldIcon?: string codeIcon?: string italicIcon?: string linkIcon?: string strikethroughIcon?: string latexIcon?: string aiIcon?: string // Override only the toolbar's AI button (only renders when AI is enabled and a provider is configured) // Accessible names, for localization. Each defaults to its English label. boldLabel?: string codeLabel?: string italicLabel?: string linkLabel?: string strikethroughLabel?: string latexLabel?: string aiLabel?: string buildToolbar?: (builder: GroupBuilder) => void } // Example: const config: CrepeConfig = { features: { [Crepe.Feature.Toolbar]: true, }, featureConfigs: { [Crepe.Feature.Toolbar]: { boldIcon: customBoldIcon, italicIcon: customItalicIcon, boldLabel: 'Fett', buildToolbar: (builder) => { // Custom toolbar building logic }, }, }, } ``` Each toolbar button is rendered with an accessible name and a stable key: ```typescript type ToolbarItem = { active: (ctx: Ctx) => boolean icon: string label?: string // title + aria-label keymap?: KeymapRef // where the shortcut is bound; both fields below derive from it shortcut?: string // display only, appended to the title ariaKeyshortcuts?: string // aria-keyshortcuts, in ARIA grammar } ``` `label` is what gives the button a name β€” its only other content is an SVG. A shortcut has two readers. `shortcut` is what a human reads in the tooltip, so it is `⌘B` or `Ctrl+B`. `ariaKeyshortcuts` goes into the `aria-keyshortcuts` attribute, which has a defined grammar: `+`-joined modifiers from `Alt`, `Control`, `Shift`, `Meta` and `AltGraph`, plus a `KeyboardEvent.key` value. Display glyphs and the abbreviation `Ctrl` are both invalid there, so one field cannot serve both. You should not spell either by hand. A shortcut is bound in exactly one place β€” its keymap β€” so point the item at that keymap with `keymap` and both strings are derived for you, per platform, and stay correct when the host rebinds the key. The built-in formatting buttons already do this, and a custom button for a command that has a keymap does the same: ```typescript import { keymapRef } from '@milkdown/crepe/feature/toolbar' import { strongKeymap } from '@milkdown/kit/preset/commonmark' // A second entry for an existing command reuses the one binding β€” no need to // re-spell ⌘B here or anywhere else it appears. builder.addGroup('custom', 'Custom').addItem('bold', { icon: boldIcon, label: 'Bold', keymap: keymapRef(strongKeymap.key, 'ToggleBold'), active: (ctx) => isBoldActive(ctx), onRun: (ctx) => toggleBold(ctx), }) ``` `shortcut` / `ariaKeyshortcuts`, when set explicitly, win over the derived values β€” the escape hatch for a shortcut that is not backed by a milkdown keymap: ```typescript builder.addGroup('custom', 'Custom').addItem('highlight', { icon: highlightIcon, label: 'Highlight', shortcut: isMac ? 'βŒ˜β‡§H' : 'Ctrl+Shift+H', // shown to the user ariaKeyshortcuts: isMac ? 'Meta+Shift+H' : 'Control+Shift+H', // for AT active: (ctx) => isHighlightActive(ctx), onRun: (ctx) => toggleHighlight(ctx), }) ``` Buttons also carry `data-toolbar-item=""`, so consumers can target a specific one without relying on its position. #### TopBar Feature A fixed toolbar at the top of the editor with heading selector, formatting buttons, insert actions, and block commands. Unlike the Toolbar feature (which appears as a floating tooltip on text selection), the TopBar is always visible. This feature is **disabled by default**. ```typescript interface TopBarFeatureConfig { // Heading selector options headingOptions?: HeadingOption[] // Icon overrides boldIcon?: string italicIcon?: string strikethroughIcon?: string codeIcon?: string linkIcon?: string imageIcon?: string tableIcon?: string codeBlockIcon?: string mathIcon?: string quoteIcon?: string hrIcon?: string bulletListIcon?: string orderedListIcon?: string taskListIcon?: string chevronDownIcon?: string // Custom toolbar building buildTopBar?: (builder: GroupBuilder) => void } // Example: const config: CrepeConfig = { features: { [Crepe.Feature.TopBar]: true, }, featureConfigs: { [Crepe.Feature.TopBar]: { // Customize heading options headingOptions: [ { label: 'Text', level: null }, { label: 'H1', level: 1 }, { label: 'H2', level: 2 }, { label: 'H3', level: 3 }, ], }, }, } ``` The TopBar supports configurable dropdown selectors. The heading selector is built-in, but you can add custom dropdowns via `buildTopBar`: ```typescript const config: CrepeConfig = { features: { [Crepe.Feature.TopBar]: true, }, featureConfigs: { [Crepe.Feature.TopBar]: { buildTopBar: (builder) => { builder.addGroup('custom', 'Custom').addItem('font-size', { icon: '', active: () => false, selector: { chevronIcon: '...', activeLabel: (ctx) => '16px', options: [ { label: '12px', onSelect: (ctx) => { /* set font size */ }, }, { label: '14px', onSelect: (ctx) => { /* set font size */ }, }, { label: '16px', onSelect: (ctx) => { /* set font size */ }, }, ], }, }) }, }, }, } ``` The default toolbar groups are: 1. **Heading** - Dropdown selector for Paragraph/H1-H6 2. **Formatting** - Bold, Italic, Strikethrough, Inline Code 3. **List** - Bullet list, Ordered list, Task list 4. **Insert** - Link, Image, Table 5. **Block** - Code block, Math (LaTeX) 6. **More** - Quote, Horizontal rule #### CodeMirror Feature ```typescript interface CodeMirrorFeatureConfig { extensions?: Extension[] // Custom CodeMirror extensions languages?: LanguageDescription[] // Available languages theme?: Extension // CodeMirror theme // UI customization expandIcon?: string searchIcon?: string clearSearchIcon?: string searchPlaceholder?: string noResultText?: string // Copy button customization copyIcon?: string // Custom copy button icon copyText?: string // Custom copy button text onCopy?: (content: string) => void // Callback when code is copied // Rendering customization renderLanguage?: (language: string, selected: boolean) => string renderPreview?: ( language: string, content: string ) => string | HTMLElement | null previewToggleIcon?: (previewOnlyMode: boolean) => string previewToggleText?: (previewOnlyMode: boolean) => string previewLabel?: () => string } // Example: const config: CrepeConfig = { features: { [Crepe.Feature.CodeMirror]: true, }, featureConfigs: { [Crepe.Feature.CodeMirror]: { searchPlaceholder: 'Search programming language...', noResultText: 'No matching language found', theme: oneDark, // Import from @codemirror/theme-one-dark }, }, } ``` It's also possible to configure the language list and theme: ```typescript import { oneDark } from '@codemirror/theme-one-dark' import { LanguageDescription } from '@codemirror/language' import { markdown } from '@codemirror/lang-markdown' const config: CrepeConfig = { features: { [Crepe.Feature.CodeMirror]: true, }, featureConfigs: { [Crepe.Feature.CodeMirror]: { theme: oneDark, languages: [ // Only load markdown language LanguageDescription.of({ name: 'Markdown', extensions: ['md', 'markdown'], load() { return import('@codemirror/lang-markdown').then((m) => m.markdown()) }, }), ], }, }, } ``` To learn which languages are available, you can refer to the [CodeMirror language data](https://github.com/codemirror/language-data). #### Latex Feature ```typescript interface LatexFeatureConfig { katexOptions?: KatexOptions // KaTeX rendering options inlineEditConfirm?: string // Custom confirm icon for inline math } // Example: const config: CrepeConfig = { features: { [Crepe.Feature.Latex]: true, }, featureConfigs: { [Crepe.Feature.Latex]: { katexOptions: { throwOnError: false, displayMode: true, }, }, }, } ``` #### AI Feature The AI feature combines streaming input and diff review into a single workflow. Users supply a `provider` (an async generator that yields markdown tokens) and Crepe handles the rest: a toolbar entry point, an instruction palette with built-in suggestions, an inline streaming indicator, and a floating diff actions panel for accepting or rejecting the result. When the user has a text selection, `runAICmd` replaces the selected text with the AI output. The provider receives the selected text in `AIPromptContext.selection` for context-aware generation. When the selection is empty, content is inserted at the cursor position. ```typescript import { Crepe } from '@milkdown/crepe' import type { AIFeatureConfig } from '@milkdown/crepe/feature/ai' import { runAICmd, abortAICmd } from '@milkdown/crepe/feature/ai' import { callCommand } from '@milkdown/kit/utils' const crepe = new Crepe({ root: '#editor', features: { [Crepe.Feature.AI]: true, }, featureConfigs: { [Crepe.Feature.AI]: { provider: async function* (context, signal) { // Yield markdown tokens from your LLM }, diffReviewOnEnd: true, diff: { acceptLabel: 'Yes', rejectLabel: 'No' }, streaming: { throttleMs: 150 }, onError: (error) => { // Handle AI errors (provider failures, buildContext errors). // Defaults to console.error if not provided. showToast(error.message) }, } satisfies AIFeatureConfig, }, }) await crepe.create() // Trigger AI programmatically: crepe.editor.action( callCommand(runAICmd.key, { instruction: 'Summarize this' }) ) // Abort: crepe.editor.action(callCommand(abortAICmd.key)) ``` ##### UX Surfaces When `Crepe.Feature.AI` is enabled and a `provider` is configured, the feature wires up four UI surfaces: 1. **Toolbar AI button** β€” appears in the selection toolbar's "Function" group. Hidden when no `provider` is configured. Override the icon via `AIFeatureConfig.aiIcon` (applies everywhere) or `ToolbarFeatureConfig.aiIcon` (toolbar only). 2. **Instruction palette** β€” a combobox dropdown that opens from the toolbar button. Users can pick a built-in suggestion, drill into a submenu (e.g. _Change tone…_, _Translate…_), or type a free-form instruction and submit it as a custom prompt. 3. **Streaming indicator** β€” an inline pill rendered at the streaming insertion point with a spinner, the active-form label (e.g. _Improving writing…_), and an _Esc to cancel_ hint. 4. **Diff actions panel** β€” a floating panel pinned to the bottom of the editor while diff review is active for an AI-owned session. Provides _Retry_ (re-run the same prompt on the original range), _Reject all_, and _Accept all_ buttons. _Accept all_ is also bound to Mod+Enter. ##### Localizing Strings & Overriding Icons Every label and icon used by the AI surfaces is configurable. All of the following live on `AIFeatureConfig`: ```typescript interface AIFeatureConfig { // ── Instruction palette strings ─────────────────────────────────── instructionPlaceholder?: string // Default: 'Tell AI what to do with the selection…' suggestionsHeaderLabel?: string // Default: 'SUGGESTIONS' sendAsPromptHeaderLabel?: string // Default: 'SEND AS PROMPT' sendAsPromptLabel?: string // Default: 'Ask AI:' submitButtonLabel?: string // aria-label, default: 'Send prompt' listboxLabel?: string // aria-label, default: 'AI suggestions' // ── Icon overrides ──────────────────────────────────────────────── aiIcon?: string // Toolbar entry + palette prefix sendIcon?: string // Round submit button sendPromptIcon?: string // "Ask AI: …" entry icon enterKeyIcon?: string // Shared by palette shortcut chip + diff panel chevronLeftIcon?: string // Submenu back arrow chevronRightIcon?: string // Submenu indicator // ── Streaming indicator ─────────────────────────────────────────── streamingIndicator?: { fallbackLabel?: string // Default: 'Generating' (used when runAICmd has no `label`) cancelHint?: string // Default: 'Esc to cancel' } // ── Diff actions panel ──────────────────────────────────────────── diffActions?: { retryLabel?: string // Default: 'Retry' rejectAllLabel?: string // Default: 'Reject all' acceptAllLabel?: string // Default: 'Accept all' retryIcon?: string rejectIcon?: string acceptIcon?: string modSymbol?: string // Default: '⌘' on macOS, 'Ctrl' elsewhere } } ``` ##### Customizing Suggestions The instruction palette ships with built-in suggestions: _Improve writing_, _Fix grammar & spelling_, _Make shorter_, _Make longer_, plus _Change tone…_ and _Translate…_ submenus. Customize the list via `buildAISuggestions`: ```typescript const config: AIFeatureConfig = { buildAISuggestions: (builder) => { // The builder is pre-populated with the defaults; mutate freely. builder.removeItem('grammar') // drop a built-in builder.addItem('summarize', { icon: '…', label: 'Summarize', streamingLabel: 'Summarizing', // shown in the streaming indicator prompt: 'Summarize this in one paragraph.', }) // Add a new submenu with its own items builder.addSubmenu( 'audience', { icon: '…', label: 'Rewrite for audience…', title: 'Rewrite for audience', searchPlaceholder: 'Search audiences…', }, (sub) => { sub.addItem('beginner', { icon: '…', label: 'Beginners', prompt: 'Rewrite this for a beginner audience.', }) } ) // To start from scratch instead, call builder.clear() first. }, } ``` The submitted prompt is wrapped in an `AIPromptContext` (with the serialized document and any selection) and passed to your `provider`. ##### Triggering Programmatically ```typescript import { runAICmd, abortAICmd } from '@milkdown/crepe/feature/ai' import { callCommand } from '@milkdown/kit/utils' // `label` is the active-form text shown in the streaming indicator. crepe.editor.action( callCommand(runAICmd.key, { instruction: 'Translate this to French', label: 'Translating to French', }) ) // Abort the in-flight session. `keep: true` preserves the partial // streamed output; `keep: false` (default) discards it. crepe.editor.action(callCommand(abortAICmd.key, { keep: true })) ``` ##### Built-in Providers Crepe ships two ready-made `AIProvider` factories so you don't have to hand-roll SSE parsing, system prompts, or auth headers. Both live under their own subpaths and have no SDK dependencies (just `fetch`). ```typescript import { createOpenAIProvider } from '@milkdown/crepe/llm-providers/openai' import { createAnthropicProvider } from '@milkdown/crepe/llm-providers/anthropic' // Server-side shape (no browser; `apiKey` reads from a real secret). // In the browser, see "Deployment modes" below β€” passing an `apiKey` // from a page or Worker throws unless you explicitly opt in. const openai = createOpenAIProvider({ apiKey: '', model: 'gpt-4o-mini', }) const anthropic = createAnthropicProvider({ apiKey: '', model: 'claude-sonnet-4-5', }) ``` There is no "secure" way to embed an API key in a browser bundle β€” build-time substitutions like Vite's `import.meta.env.VITE_*` end up as plain strings in the shipped JavaScript and are visible to anyone who can open DevTools. The two safe deployment modes are: - **BYOK**: each user provides their own key (typed into your UI, read from desktop-app keychain, etc.) and accepts the exposure for their own account. Set `dangerouslyAllowBrowser: true`. - **Backend proxy**: omit `apiKey` entirely and point `baseURL` at your own server, which holds the real key and forwards requests. This is the recommended pattern for multi-user web apps. `process.env` only works in Node/SSR; it won't be defined in a typical browser build. Both providers send a default system prompt that asks for raw markdown output (no preambles, no surrounding code fences) and assemble the user message from `AIPromptContext`: ``` {full markdown} ← only when non-empty {selected markdown} {user instruction} ``` ###### Deployment modes Pick the config combination that matches where the API key actually lives: ```typescript // 1. Desktop / BYOK (each user supplies their own key) // The key is in the page; opt in explicitly. createOpenAIProvider({ apiKey: userKey, model: 'gpt-4o-mini', dangerouslyAllowBrowser: true, }) // 2. Production: route through your own backend. // No `apiKey`; your server attaches the real key. The browser // sends a session token instead. No `dangerouslyAllowBrowser` // needed because the API key never reaches the client. createAnthropicProvider({ baseURL: '/api/anthropic', headers: { Authorization: `Bearer ${sessionToken}` }, model: 'claude-sonnet-4-5', }) // 3. Server-side / SSR // No browser, so no opt-in needed. createOpenAIProvider({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o-mini', }) ``` Setting `apiKey` from the main browser thread or from a Worker without `dangerouslyAllowBrowser: true` throws β€” the provider refuses to leak your key into a context where any visitor could read it. ###### Shared configuration The two providers share these fields (the actual exported types are `OpenAIProviderConfig` and `AnthropicProviderConfig`; the interface below is illustrative β€” there is no `BaseProviderConfig` public export to import directly): ```typescript // Shape shared by `OpenAIProviderConfig` and `AnthropicProviderConfig` interface BaseProviderConfig { apiKey?: string baseURL?: string // defaults to the provider's official endpoint headers?: Record model: string systemPrompt?: string | null // string β†’ use as-is (incl. ''); null β†’ omit; undefined β†’ default dangerouslyAllowBrowser?: boolean } ``` `systemPrompt` semantics: `undefined` keeps the markdown-only default, `null` sends no system message at all, and any string (including `''`) replaces the default verbatim. ###### Provider-specific options ```typescript // OpenAI: any chat-completions body fields (temperature, top_p, etc.) // can go in `body`. `buildMessages` lets you fully customize the // messages array β€” the defaults are passed in so you can wrap them. // `defaults.systemPrompt` is `string | null`: `null` means the user // asked to omit the system message, so don't coerce it to ''. createOpenAIProvider({ apiKey, model: 'gpt-4o-mini', body: { temperature: 0.2 }, buildMessages: (context, defaults) => [ ...(defaults.systemPrompt !== null ? [{ role: 'system' as const, content: defaults.systemPrompt }] : []), { role: 'user', content: defaults.userMessage }, ], }) // Anthropic: `maxTokens` (default 4096), `anthropicVersion` (default // '2023-06-01'), and any `/v1/messages` body fields via `body`. // `buildMessages` returns `{ system, messages }` since Anthropic puts // the system prompt in a top-level field rather than the messages array. createAnthropicProvider({ apiKey, model: 'claude-sonnet-4-5', maxTokens: 2048, body: { temperature: 0.5 }, }) ``` ###### CORS note for direct browser calls `api.openai.com/v1/chat/completions` doesn't return the `Access-Control-Allow-Origin` (ACAO) header that browsers require for cross-origin requests, and `api.anthropic.com/v1/messages` requires the `anthropic-dangerous-direct-browser-access` header (which the Anthropic provider sets automatically when `dangerouslyAllowBrowser: true`). Direct browser β†’ provider calls work in desktop apps (no CORS) but generally fail from regular web pages. The proxy mode above (`baseURL` pointing at your own backend) sidesteps CORS entirely and is the recommended deployment pattern. See [@milkdown/plugin-diff](./plugin-diff.md) and [@milkdown/plugin-streaming](./plugin-streaming.md) for the underlying plugin APIs. ##### Driving the AI feature from your own UI If you replace the toolbar, two helpers let you reproduce its AI button β€” one to decide whether to show it, one to run it. They are separate calls because they happen at different moments: visibility once when you build the toolbar, the range read on every click. ```typescript import { editorViewCtx } from '@milkdown/kit/core' import { CrepeFeature, useCrepeFeatures } from '@milkdown/crepe' import { defaultAIIcon, useAIInstructionTooltipAPI, useAIProviderConfig, } from '@milkdown/crepe/feature/ai' // Visibility β€” evaluate once while building your toolbar. Only offer the // action when a provider is actually configured: without one the palette // opens but every action is rejected. const showAIButton = crepe.editor.action((ctx) => { // Both helpers throw when the AI feature is disabled, so ask the feature // flags first. if (!useCrepeFeatures(ctx).get().includes(CrepeFeature.AI)) return false return Boolean(useAIProviderConfig(ctx).provider) }) // Action β€” read the selection at click time, never earlier. function onAIButtonClick() { crepe.editor.action((ctx) => { const { from, to } = ctx.get(editorViewCtx).state.selection useAIInstructionTooltipAPI(ctx).show(from, to) }) } ``` Use `defaultAIIcon` to match the built-in button's icon. `AIFeatureConfig.aiIcon` only overrides Crepe's own toolbar entry β€” it is `undefined` on `useAIProviderConfig(ctx)` unless the host set it, so don't rely on it as your default. Both helpers resolve their slice by name rather than by slice object, which is what makes them safe to import from `@milkdown/crepe/feature/ai` while `Crepe` comes from `@milkdown/crepe`: each package entry is bundled separately, so the two entries' slice _objects_ are not the same instance. ⚠️ `useAIProviderConfig(ctx)` returns the live config, whose `provider` is a closure over your API key in BYOK deployments. Read the field you need; don't log or serialize the whole object. ## Usage ### Using Crepe Editor The `Crepe` class provides a high-level interface with all features enabled by default: ```typescript import { Crepe } from '@milkdown/crepe' const editor = new Crepe({ root: '#editor', // DOM element or selector features: { [Crepe.Feature.Toolbar]: true, [Crepe.Feature.Latex]: true, }, featureConfigs: { [Crepe.Feature.Placeholder]: { text: 'Start writing...', mode: 'block', }, }, defaultValue: '# Hello World', }) // Get markdown content const markdown = editor.getMarkdown() // Set readonly mode editor.setReadonly(true) // Listen to editor events editor.on((listener) => { listener.markdownUpdated((ctx, markdown, prevMarkdown) => { // Handle updates }) }) ``` ### Using CrepeBuilder The `CrepeBuilder` class provides a more flexible way to build your editor by manually adding features. This approach is particularly useful for optimizing bundle size since you only include the features you actually need: ```typescript import { CrepeBuilder } from '@milkdown/crepe/builder' import { blockEdit } from '@milkdown/crepe/feature/block-edit' import { toolbar } from '@milkdown/crepe/feature/toolbar' import { topBar } from '@milkdown/crepe/feature/top-bar' // You may also want to import styles by feature import '@milkdown/crepe/theme/common/prosemirror.css' import '@milkdown/crepe/theme/common/reset.css' import '@milkdown/crepe/theme/common/block-edit.css' import '@milkdown/crepe/theme/common/toolbar.css' import '@milkdown/crepe/theme/common/top-bar.css' // And introduce the theme import '@milkdown/crepe/theme/crepe.css' const builder = new CrepeBuilder({ root: '#editor', defaultValue: '# Hello World', }) // Add features manually builder.addFeature(blockEdit).addFeature(toolbar).addFeature(topBar) // Create the editor const editor = await builder.create() // Get markdown content const markdown = builder.getMarkdown() // Set readonly mode builder.setReadonly(true) // Listen to editor events builder.on((listener) => { listener.markdownUpdated((ctx, markdown, prevMarkdown) => { // Handle updates }) }) ``` The `CrepeBuilder` is useful when you want to: - Reduce bundle size by only including the features you need - Have more control over which features are added and in what order - Add custom features or plugins - Configure features individually with their specific configurations This approach allows for better tree-shaking and results in a smaller bundle size compared to using the full `Crepe` editor with all features enabled. ## Themes Crepe comes with several built-in themes that can be imported: ```typescript // Light themes import '@milkdown/crepe/theme/crepe.css' import '@milkdown/crepe/theme/nord.css' import '@milkdown/crepe/theme/frame.css' // Dark themes import '@milkdown/crepe/theme/crepe-dark.css' import '@milkdown/crepe/theme/nord-dark.css' import '@milkdown/crepe/theme/frame-dark.css' ``` ### Customizing theme variables Every theme exposes CSS custom properties on the `.milkdown` element, so you can override them without touching the source. For example, to scale the whole editor's font size (default `16px`) in a single line: ```css .milkdown { --crepe-base-font-size: 14px; } ``` Other variables follow the same pattern, e.g. `--crepe-font-default`, `--crepe-font-title`, `--crepe-font-code` and the `--crepe-color-*` palette. ## API Reference @CrepeFeature @Crepe @CrepeConfig @CrepeBuilder @CrepeBuilderConfig @useCrepe @useCrepeFeatures