Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/docs/content/docs/en/files/editor.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
title: Editor
description: A rich markdown editor for your files — type markdown and watch it render, or edit visually.
pageType: concept
---

import { Image } from '@/components/ui/image'
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Callout } from 'fumadocs-ui/components/callout'

Every markdown file in your workspace opens in a **rich editor**. Type markdown and it renders as you go, or format visually with the toolbar and slash menu. What you see is exactly what gets saved — plain markdown underneath, no lock-in.

<div className="mx-auto w-full overflow-hidden rounded-lg my-6">
<Image src="/static/files/editor/overview.png" alt="A markdown file rendered in the editor: headings, bold, italic, and a link, a nested bullet list, and a table" width={900} height={410} />
</div>

## Formatting text

Select any text to bring up the formatting toolbar — bold, italic, strikethrough, inline code, and links. The same marks appear instantly as you type the markdown for them, like `**bold**` or `*italic*`. Links show a hover card so you can open, copy, edit, or remove them without hunting through the source.

## Structure

Headings, blockquotes, and dividers keep long documents scannable. Type `# ` through `###### ` for headings, `> ` for a quote, and `---` for a divider.

## Lists and checklists

Bullet, ordered, and nested lists all work, plus task lists you can tick right in the document.

## Tables

Insert a table from the slash menu, then click any cell for the floating table toolbar — add or remove rows and columns, toggle the header row, or delete the table. Drag a column border to resize it.

## Code blocks

Fenced code blocks are syntax-highlighted, with a language picker in the corner. Pick `mermaid` to render a live diagram instead of code.

## Images

Paste or drag an image straight into the document, then drag a corner to resize it.

## Slash menu and shortcuts

Type `/` anywhere to insert any block — heading, list, table, code block, image, and more — without leaving the keyboard. Familiar shortcuts work too: **Cmd/Ctrl + B** for bold, **Cmd/Ctrl + I** for italic, and **Cmd/Ctrl + K** to add a link over selected text.

## Markdown fidelity

The editor round-trips your markdown exactly — it saves what you wrote, with no reformatting churn.

<Callout type="info">
A few constructs can't be represented visually without losing information on save — footnotes, raw HTML, and HTML comments. When a file contains one of these, it opens **read-only** so the original source is preserved untouched. Everything is still rendered faithfully; you just can't edit that file inline.
</Callout>

<Cards>
<Card title="Generating files" href="/files/generating" description="Have a workflow produce a document from a run." />
<Card title="Using files in workflows" href="/files/using-in-workflows" description="Read a file into a workflow and produce one from a run." />
</Cards>
1 change: 1 addition & 0 deletions apps/docs/content/docs/en/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"./tables/workflow-columns",
"---Files---",
"./files/index",
"./files/editor",
"./files/using-in-workflows",
"./files/generating",
"./files/passing-files",
Expand Down
Binary file added apps/docs/public/static/files/editor/overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ afterEach(() => {
editor = null
})

function mount(): Editor {
return new Editor({ extensions: [...createMarkdownContentExtensions(), MarkdownPaste] })
function mount(editable = true): Editor {
return new Editor({ extensions: [...createMarkdownContentExtensions(), MarkdownPaste], editable })
}

/** Run the plugin paste handlers the way ProseMirror would, with a mocked clipboard. */
Expand Down Expand Up @@ -93,4 +93,61 @@ describe('markdown paste', () => {
expect(editor.isActive('codeBlock')).toBe(true)
expect(paste(editor, '[link](https://example.com)')).toBe(false)
})

it('rejects the paste entirely in a read-only editor', () => {
editor = mount(false)
expect(paste(editor, '# heading\n\n- one\n- two')).toBe(false)
expect(editor.getText()).toBe('')
})

it.each([
['empty string', ''],
['whitespace only', ' \n\n '],
['a bare thematic break (ambiguous — needs another markdown signal)', '---'],
['inline-only italic (single asterisk would false-positive on e.g. *args)', 'an *italic* word'],
['inline-only strikethrough', 'a ~~struck~~ word'],
['inline-only code', 'some `code` here'],
])('leaves %s to the default handler', (_label, text) => {
editor = mount()
expect(paste(editor, text)).toBe(false)
})

// Only structural / unambiguous constructs gate the markdown parse. Inline-only marks that
// `looksLikeMarkdown` deliberately omits to avoid false positives — single-asterisk italic
// (`*args`), `~~`, single-backtick code — are covered by the Markdown extension's own paste path,
// not MarkdownPaste, so they belong to a different test surface.
it.each([
['heading', '# Heading', 'heading'],
['bold', 'a **bold** word', 'bold'],
['bullet list', '- one\n- two', 'bulletList'],
['ordered list', '1. one\n2. two', 'orderedList'],
['task list', '- [x] done\n- [ ] todo', 'taskList'],
['blockquote', '> a quote', 'blockquote'],
['fenced code block', '```ts\nconst x = 1\n```', 'codeBlock'],
['standalone image', '![alt](https://e.com/i.png)', 'image'],
['thematic break within a document', '# Title\n\n---\n\nbody', 'horizontalRule'],
])('renders pasted %s as rich content', (_label, md, nodeType) => {
editor = mount()
expect(paste(editor, md)).toBe(true)
expect(JSON.stringify(editor.getJSON())).toContain(`"type":"${nodeType}"`)
})
Comment thread
waleedlatif1 marked this conversation as resolved.

it('parses markdown-shaped plain text even when an HTML sibling is present', () => {
editor = mount()
const html = '<h1>Title</h1><ul><li>a</li><li>b</li></ul>'
expect(paste(editor, '# Title\n\n- a\n- b', html)).toBe(true)
const json = JSON.stringify(editor.getJSON())
expect(json).toContain('"type":"heading"')
expect(json).toContain('"type":"bulletList"')
expect(json).not.toContain('# Title')
})

it('preserves the structural blocks of a multi-block document, in order, on paste', () => {
editor = mount()
expect(paste(editor, '# Title\n\nA paragraph.\n\n- a\n- b\n\n> quote')).toBe(true)
const structural = (editor.getJSON().content ?? [])
.map((node) => node.type)
.filter((type) => type !== 'paragraph')
expect(structural).toEqual(['heading', 'bulletList', 'blockquote'])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,15 @@
pointer-events: none;
}

/*
* prosemirror-tables' column-resizing plugin toggles the `resize-cursor` class on the editor root
* while the pointer is over a column boundary; without this rule the handle shows but the cursor
* never changes to the resize affordance.
*/
.rich-markdown-prose.resize-cursor {
cursor: col-resize;
}

.rich-markdown-prose p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: var(--text-subtle);
Expand Down
Loading