slate-serializers
View project on npmView project on GitHub

slateToMarkdown

From @slate-serializers/markdown. Converts Slate JSON to a GitHub Flavored Markdown string. Use it to export editor content to README files, issue trackers, chat tools or anything else that accepts Markdown.

npm install @slate-serializers/markdown
All output.md file content on this page is generated with the slateToMarkdown serializer.

Default

The default configuration understands the element names used by the other @slate-serializers packages (p, h1, ul, li, blockquote, link, …) and by the Slate examples (paragraph, heading-one, bulleted-list, list-item, block-quote, check-list-item, …), so you can often call it without a config.


import { slateToMarkdown } from '@slate-serializers/markdown'

const slate = [
{
"type": "h1",
"children": [
{
"text": "Heading 1"
}
]
},
{
"type": "p",
"children": [
{
"text": "A paragraph with "
},
{
"text": "bold",
"bold": true
},
{
"text": ", "
},
{
"text": "italic",
"italic": true
},
{
"text": " and "
},
{
"text": "inline code",
"code": true
},
{
"text": ", plus a "
},
{
"type": "link",
"url": "https://docs.slatejs.org",
"children": [
{
"text": "link"
}
]
},
{
"text": "."
}
]
},
{
"type": "ul",
"children": [
{
"type": "li",
"children": [
{
"text": "One"
}
]
},
{
"type": "li",
"children": [
{
"text": "Two"
}
]
}
]
},
{
"type": "blockquote",
"children": [
{
"text": "A quote."
}
]
}
]

const markdown = slateToMarkdown(slate)

output.md
# Heading 1

A paragraph with **bold**, *italic* and `inline code`, plus a [link](https://docs.slatejs.org).

- One
- Two

> A quote.

Supported Markdown

  • Headings, paragraphs, block quotes, horizontal rules, line breaks, links and images.
  • Bullet and ordered lists, including nested lists and task lists.
  • Fenced code blocks and tables with column alignment.
  • Bold, italic, strikethrough and inline code. Underline, subscript and superscript have no Markdown syntax, so they are written as HTML tags, which GFM allows.
  • Text is escaped, so characters such as *, _, [ or a leading 1. stay literal.

Some constructs read extra properties from the Slate node:

ConstructProperties read from the node
linkurl (or href)
imageurl (or src), alt (or caption, or the node's text)
olstart
li, taskchecked: a boolean makes the item a task list item
code-blocklanguage (or lang). Child elements (e.g. code-line) become lines.
table-cellalign (or textAlign) on the first row sets the column alignment

import { slateToMarkdown } from '@slate-serializers/markdown'

const slate = [
{
"type": "ol",
"start": 3,
"children": [
{
"type": "li",
"children": [
{
"text": "Third"
}
]
},
{
"type": "li",
"children": [
{
"text": "Fourth, with a nested list"
},
{
"type": "ul",
"children": [
{
"type": "li",
"children": [
{
"text": "Nested"
}
]
}
]
}
]
}
]
},
{
"type": "ul",
"children": [
{
"type": "li",
"checked": true,
"children": [
{
"text": "Done"
}
]
},
{
"type": "li",
"checked": false,
"children": [
{
"text": "To do"
}
]
}
]
},
{
"type": "code-block",
"language": "ts",
"children": [
{
"type": "code-line",
"children": [
{
"text": "const greeting = 'Hello'"
}
]
},
{
"type": "code-line",
"children": [
{
"text": "console.log(greeting)"
}
]
}
]
},
{
"type": "table",
"children": [
{
"type": "tr",
"children": [
{
"type": "th",
"children": [
{
"text": "Package"
}
]
},
{
"type": "th",
"align": "right",
"children": [
{
"text": "Version"
}
]
}
]
},
{
"type": "tr",
"children": [
{
"type": "td",
"children": [
{
"text": "@slate-serializers/markdown"
}
]
},
{
"type": "td",
"children": [
{
"text": "2.8.1"
}
]
}
]
}
]
},
{
"type": "image",
"url": "https://example.com/diagram.png",
"alt": "Diagram",
"children": [
{
"text": ""
}
]
},
{
"type": "hr",
"children": [
{
"text": ""
}
]
},
{
"type": "p",
"children": [
{
"text": "Line one\nLine two"
}
]
}
]

const markdown = slateToMarkdown(slate)

output.md
3. Third
4. Fourth, with a nested list
- Nested

- [x] Done
- [ ] To do

```ts
const greeting = 'Hello'
console.log(greeting)
```

| Package | Version |
| --- | ---: |
| @slate-serializers/markdown | 2.8.1 |

![Diagram](https://example.com/diagram.png)

---

Line one\
Line two

Payload CMS

If you are using Slate Rich Text in Payload CMS, pass payloadSlateToMarkdownConfig. It adds support for Payload upload elements: images become ![alt](url) and other files become links.


import { slateToMarkdown, payloadSlateToMarkdownConfig } from '@slate-serializers/markdown'

const slate = [
{
"type": "h2",
"children": [
{
"text": "Uploads"
}
]
},
{
"type": "upload",
"relationTo": "media",
"value": {
"url": "/media/diagram.png",
"alt": "Architecture diagram",
"mimeType": "image/png"
},
"children": [
{
"text": ""
}
]
},
{
"type": "upload",
"relationTo": "media",
"value": {
"url": "/media/report.pdf",
"filename": "report.pdf",
"mimeType": "application/pdf"
},
"children": [
{
"text": ""
}
]
}
]

const markdown = slateToMarkdown(slate, payloadSlateToMarkdownConfig)

output.md
## Uploads

![Architecture diagram](/media/diagram.png)

[report.pdf](/media/report.pdf)

Options

Spread slateToMarkdownConfig and override what you need. The config type is exported as SlateToMarkdownConfig.

elementMap

Map a Slate element type to a Markdown construct: paragraph, h1–h6, blockquote, ul, ol, li, task, link, image, line-break, hr, code-block, table, table-section, table-row or table-cell.


import { slateToMarkdown, slateToMarkdownConfig } from '@slate-serializers/markdown'

const slate = [
{
"type": "title",
"children": [
{
"text": "Release notes"
}
]
},
{
"type": "p",
"children": [
{
"text": "A custom title element becomes a level 1 heading."
}
]
}
]

const markdown = slateToMarkdown(slate, {
...slateToMarkdownConfig,
elementMap: { ...slateToMarkdownConfig.elementMap, title: 'h1' },
})

output.md
# Release notes

A custom title element becomes a level 1 heading.

markMap

Map a leaf property such as bold to strong, emphasis, strikethrough or code, or to a pair of { open, close } strings. Earlier entries wrap later ones.


import { slateToMarkdown, slateToMarkdownConfig } from '@slate-serializers/markdown'

const slate = [
{
"type": "p",
"children": [
{
"text": "Markdown has no syntax for "
},
{
"text": "highlighted",
"highlight": true
},
{
"text": " or "
},
{
"text": "underlined",
"underline": true
},
{
"text": " text, so HTML tags are used."
}
]
}
]

const markdown = slateToMarkdown(slate, {
...slateToMarkdownConfig,
markMap: {
...slateToMarkdownConfig.markMap,
highlight: { open: '<mark>', close: '</mark>' },
},
})

output.md
Markdown has no syntax for <mark>highlighted</mark> or <u>underlined</u> text, so HTML tags are used.

elementTransforms

Custom output per element type. Each function receives the node and its children, already serialized to Markdown, and returns a string. Return undefined to fall back to elementMap.


import { slateToMarkdown, slateToMarkdownConfig } from '@slate-serializers/markdown'

const slate = [
{
"type": "p",
"children": [
{
"text": "Thanks "
},
{
"type": "mention",
"username": "thompsonsj",
"children": [
{
"text": ""
}
]
},
{
"text": " for the review."
}
]
},
{
"type": "callout",
"kind": "Note",
"children": [
{
"text": "Callouts become block quotes."
}
]
}
]

const markdown = slateToMarkdown(slate, {
...slateToMarkdownConfig,
elementTransforms: {
mention: ({ node }) => `@${node.username}`,
callout: ({ node, children }) => `> **${node.kind}:** ${children}`,
},
})

output.md
Thanks @thompsonsj for the review.

> **Note:** Callouts become block quotes.

emphasisDelimiter and bulletMarker

  • emphasisDelimiter: '*' (default) or '_'. * also works inside words.
  • bulletMarker: '-' (default), '*' or '+'.

import { slateToMarkdown, slateToMarkdownConfig } from '@slate-serializers/markdown'

const slate = [
{
"type": "p",
"children": [
{
"text": "Some "
},
{
"text": "emphasis",
"italic": true
},
{
"text": " and a literal *asterisk*."
}
]
},
{
"type": "ul",
"children": [
{
"type": "li",
"children": [
{
"text": "One"
}
]
},
{
"type": "li",
"children": [
{
"text": "Two"
}
]
}
]
}
]

const markdown = slateToMarkdown(slate, {
...slateToMarkdownConfig,
emphasisDelimiter: '_',
bulletMarker: '*',
})

output.md
Some _emphasis_ and a literal \*asterisk\*.

* One
* Two

escape

Escape text that Markdown would treat as syntax. Default: true. Set to false if your text already contains Markdown.


import { slateToMarkdown, slateToMarkdownConfig } from '@slate-serializers/markdown'

const slate = [
{
"type": "p",
"children": [
{
"text": "Text that already contains **Markdown**."
}
]
}
]

slateToMarkdown(slate)
// default: the asterisks are escaped

slateToMarkdown(slate, { ...slateToMarkdownConfig, escape: false })
// escape: false: the asterisks are written as given

output.md (default)
Text that already contains \*\*Markdown\*\*.
output.md (escape: false)
Text that already contains **Markdown**.

Behaviour

  • Blocks are separated by a blank line. Empty paragraphs are dropped, because Markdown cannot represent them.
  • A \n in text becomes a hard line break (\ at the end of the line). In table cells it becomes <br>.
  • The first table row is the header row. Content that tables cannot hold, such as lists, is joined with <br>.
  • Elements that are not in elementMap are treated as inline when they sit beside text, and as a wrapper around their children otherwise.
  • Link attributes that Markdown has no syntax for, such as newTab, are ignored. Use elementTransforms to output HTML instead.
  • When a *, ** or ~~ pair would not parse in its position (for example bold directly next to italic), the equivalent HTML tag is written instead.
  • Escaping keeps Markdown syntax in text literal. It is not HTML sanitization: elementTransforms, { open, close } marks and URLs are written as given. Sanitize the result if you render untrusted documents as HTML.

Try the interactive demo.