Skip to content
Foliosodev
Menu
On this page

REST API

Writing Markdown

Send a page body as Markdown in the markdown field and Folioso converts it into the same content the editor makes. This page lists every piece of Markdown that's understood, what each becomes on the page, and what plain Markdown can't express.

Folioso reads CommonMark with three GitHub additions: tables, task lists and strikethrough. On top of that it understands its own Folioso Markdown extensions, which carry the things Markdown has no syntax for. You never need them for ordinary writing.

Each example below shows the Markdown you send and the TipTap JSON the page then stores. To see how a page you already have reads as Markdown, fetch it with ?format=markdown. See Content formats.

Paragraphs and line breaks

Separate paragraphs with a blank line. A single line break inside a paragraph becomes a space. To keep a line break, end the line with a backslash or with two spaces:

markdown
First line\
second line
and the same line.
json
{
  "type": "doc",
  "content": [
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "First line" },
        { "type": "hardBreak" },
        { "type": "text", "text": "second line and the same line." }
      ]
    }
  ]
}

A backslash at the very end of a paragraph is kept as a backslash, as CommonMark says.

Headings

Both # headings, levels 1 to 6, and underlined headings work:

markdown
## Install

Upgrading
---------
json
{
  "type": "doc",
  "content": [
    { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Install" }] },
    { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Upgrading" }] }
  ]
}

Tip

A line of --- straight after a line of text turns that text into a heading. Leave a blank line before --- when you mean a horizontal rule.

Bold, italic, strikethrough and code

markdown
**Bold**, *italic*, ~~struck~~ and `inline code`.
json
{
  "type": "doc",
  "content": [
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "Bold", "marks": [{ "type": "bold" }] },
        { "type": "text", "text": ", " },
        { "type": "text", "text": "italic", "marks": [{ "type": "italic" }] },
        { "type": "text", "text": ", " },
        { "type": "text", "text": "struck", "marks": [{ "type": "strike" }] },
        { "type": "text", "text": " and " },
        { "type": "text", "text": "inline code", "marks": [{ "type": "code" }] },
        { "type": "text", "text": "." }
      ]
    }
  ]
}
  • __bold__ and _italic_ work too, and so does a single ~ for strikethrough.
  • Marks combine: ***bold italic*** is both.
  • Inline code can't carry other marks. To show a backtick inside it, use a longer run of backticks around it: `` a ` b ``.
  • Markdown has no underline. Use a mark span: [text](folioso:span?marks=underline).

Inline links, reference links and links in angle brackets all work:

markdown
See [the guide](https://example.com/guide "The guide") or <https://example.com>.

Read the [release notes][notes].

[notes]: https://example.com/notes
json
{
  "type": "doc",
  "content": [
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "See " },
        {
          "type": "text",
          "text": "the guide",
          "marks": [
            {
              "type": "link",
              "attrs": { "href": "https://example.com/guide", "target": "_blank", "rel": "noopener noreferrer", "class": null, "title": "The guide" }
            }
          ]
        },
        { "type": "text", "text": " or " },
        {
          "type": "text",
          "text": "https://example.com",
          "marks": [
            {
              "type": "link",
              "attrs": { "href": "https://example.com", "target": "_blank", "rel": "noopener noreferrer", "class": null, "title": null }
            }
          ]
        },
        { "type": "text", "text": "." }
      ]
    },
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "Read the " },
        {
          "type": "text",
          "text": "release notes",
          "marks": [
            {
              "type": "link",
              "attrs": { "href": "https://example.com/notes", "target": "_blank", "rel": "noopener noreferrer", "class": null, "title": null }
            }
          ]
        },
        { "type": "text", "text": "." }
      ]
    }
  ]
}

Links get the same attributes as links added in the editor: they open in a new tab with rel="noopener noreferrer".

Inline code can't carry a link in the editor, so any part of a link's text written as inline code loses the link. A link whose whole text is inline code, such as [`composer install`](https://getcomposer.org), becomes plain inline code with no link at all.

In Markdown you write by hand, a link is kept only when its address:

  • starts with http:, https: or mailto:, in any letter case;
  • starts with /, such as /engineering/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b; or
  • starts with #.

An address starting with //, such as //example.com/page, also starts with /, so it's kept. Browsers read it as a link to another site, using the page's own https:. The same goes for images.

Any other address, such as javascript:, ftp: or a bare relative path like notes.md, is dropped and its text is kept as plain text:

markdown
[Run me](javascript:alert(1))
json
{ "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Run me" }] }] }

This rule applies only to Markdown links and images. Links inside a JSON block, and links sent as TipTap JSON in content, are stored exactly as given, as content always has been.

Bare addresses stay text

An address typed without <…> around it, such as https://example.com, isn't turned into a link. Put it in angle brackets, or use [text](address), to link it.

To link to another Folioso page so that its title stays up to date and it shows in Linked from, use a page link rather than an ordinary link to its address.

Images

An image becomes an image block of its own. The editor's images are blocks, so an image in the middle of a paragraph splits the paragraph around it:

markdown
Before ![Architecture diagram](https://example.com/architecture.png "Overview") after.
json
{
  "type": "doc",
  "content": [
    { "type": "paragraph", "content": [{ "type": "text", "text": "Before " }] },
    {
      "type": "image",
      "attrs": { "src": "https://example.com/architecture.png", "alt": "Architecture diagram", "title": "Overview" }
    },
    { "type": "paragraph", "content": [{ "type": "text", "text": " after." }] }
  ]
}
  • The image's text is its alt text, which the editor shows as the caption.
  • An image's address must start with http://, https:// or /. Otherwise, including a data: address, the image is dropped and its alt text is kept as plain text.
  • An image inside a link keeps the image but not the link.
  • To upload an image first and then use it, see Images.
  • To set an image's width, alignment or other attributes, use a block attributes carrier.

Lists

Bulleted lists use -, * or +. Numbered lists use 1. or 1), and start at the first item's number:

markdown
3. Three
4. Four
   - Nested bullet
json
{
  "type": "doc",
  "content": [
    {
      "type": "orderedList",
      "attrs": { "start": 3 },
      "content": [
        { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Three" }] }] },
        {
          "type": "listItem",
          "content": [
            { "type": "paragraph", "content": [{ "type": "text", "text": "Four" }] },
            {
              "type": "bulletList",
              "content": [
                { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Nested bullet" }] }] }
              ]
            }
          ]
        }
      ]
    }
  ]
}
  • Changing the bullet character, or switching between . and ), starts a new list. - a followed by * b is two lists.
  • A list item can hold more blocks, such as code blocks or further paragraphs, indented to line up with the item's text.
  • An item always starts with a paragraph, so an item that starts with something else, or is empty, gets an empty paragraph first.

Task lists

A bulleted list (-, * or +) in which every item starts with [ ] or [x], followed by a space and the item's text, becomes a task list:

markdown
- [x] Write the migration
- [ ] Deploy
json
{
  "type": "doc",
  "content": [
    {
      "type": "taskList",
      "content": [
        { "type": "taskItem", "attrs": { "checked": true }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Write the migration" }] }] },
        { "type": "taskItem", "attrs": { "checked": false }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Deploy" }] }] }
      ]
    }
  ]
}

If only some items have a checkbox, the list stays an ordinary bulleted list and the checkboxes are kept as the text [ ] or [x]. Task items can be nested, and can hold other blocks, like ordinary list items.

  • A numbered list is never a task list: 1. [ ] Deploy keeps [ ] as text.
  • A checkbox with nothing after it, such as - [ ] on its own, is an ordinary list item holding the text [ ]. To write an empty task item, add a block attributes carrier: - [ ] [](folioso:attrs).
  • [X] is accepted as ticked. Folioso writes it back as [x].

Quotes

markdown
> Quoted text
>
> - and a list
json
{
  "type": "doc",
  "content": [
    {
      "type": "blockquote",
      "content": [
        { "type": "paragraph", "content": [{ "type": "text", "text": "Quoted text" }] },
        { "type": "bulletList", "content": [{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "and a list" }] }] }] }
      ]
    }
  ]
}

A quote can hold any blocks, including other quotes. An empty quote gets one empty paragraph.

Alert boxes

A quote whose first line starts with [!NOTE], [!TIP], [!IMPORTANT], [!WARNING] or [!CAUTION] becomes an alert box. The rest of that first line is the box's title, and the paragraphs after it are its body:

markdown
> [!WARNING] Back up **first**
> This deletes the staging database.
json
{
  "type": "doc",
  "content": [
    {
      "type": "alertBox",
      "attrs": { "type": "warning" },
      "content": [
        {
          "type": "alertTitle",
          "content": [
            { "type": "text", "text": "Back up " },
            { "type": "text", "text": "first", "marks": [{ "type": "bold" }] }
          ]
        },
        { "type": "alertBody", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "This deletes the staging database." }] }] }
      ]
    }
  ]
}
You write Alert box type
[!NOTE] Info (info)
[!IMPORTANT] Info (info)
[!TIP] Success (success)
[!WARNING] Warning (warning)
[!CAUTION] Error (error)
  • The type is matched in any letter case, and must be followed by a space or the end of the line.
  • The title can hold bold, italic, code and the other marks. It can be left out, as in GitHub's form, where [!NOTE] is alone on its line.
  • The body can hold only paragraphs. Lists, code blocks, headings and images in the quote are moved out and placed straight after the box, in order. A box with no body gets one empty paragraph.
  • Alert boxes exist only at the top level of a page. [!NOTE] in a quote inside a list, or in a quote inside another quote, is an ordinary quote that starts with the text [!NOTE]. So is any other word in the brackets, such as [!INFO].

Code blocks

Fence code with three or more backticks or tildes. The word after the fence is the code block's language:

markdown
```js
const answer = 42;
```
json
{
  "type": "doc",
  "content": [
    { "type": "codeBlock", "attrs": { "language": "javascript" }, "content": [{ "type": "text", "text": "const answer = 42;" }] }
  ]
}

The language is worked out the way the editor does when you type a fence:

  1. Only the first word counts, lower-cased: ```PHP gives php.
  2. It's kept only if it's made of letters, digits and _ + # -. Otherwise the block has no language.
  3. Common short names are expanded:
You write Language stored
js javascript
ts typescript
py python
sh, shell, zsh, shellscript bash
yml yaml
docker dockerfile

A fence with no language, and a block indented by four spaces, give a code block whose language is null, which the editor shows as Plain text. Any other language is kept as written, as in the editor. See Code blocks.

To store a language exactly as written, with no lower-casing or short names, add the word verbatim after it, as in ```js verbatim. See Exact code languages.

To put three backticks inside a code block, fence it with four or more.

Diagrams

A fence whose language is mermaid or plantuml, in any letter case, becomes a diagram rather than a code block:

markdown
```mermaid
graph TD;
  A[Start] --> B[End];
```
json
{
  "type": "doc",
  "content": [
    { "type": "mermaid", "attrs": { "code": "graph TD;\n  A[Start] --> B[End];", "engine": "mermaid" } }
  ]
}

For PlantUML, write ```plantuml and include the @startuml and @enduml lines in the source. To keep Mermaid or PlantUML source as an ordinary code block instead, write ```mermaid verbatim.

Tables

GitHub pipe tables become tables. The first row is the header row:

markdown
| Setting | Value |
| :--- | ---: |
| `timeout` | 30 \| 60 |
json
{
  "type": "doc",
  "content": [
    {
      "type": "table",
      "content": [
        {
          "type": "tableRow",
          "content": [
            { "type": "tableHeader", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Setting" }] }] },
            { "type": "tableHeader", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Value" }] }] }
          ]
        },
        {
          "type": "tableRow",
          "content": [
            { "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "timeout", "marks": [{ "type": "code" }] }] }] },
            { "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "30 | 60" }] }] }
          ]
        }
      ]
    }
  ]
}
  • Write \| for a | inside a cell.
  • The colons that align columns are accepted but ignored. The editor has no column alignment.
  • Each cell holds one line. HTML such as <br> is kept as text. Table cells in the editor can hold several paragraphs, lists and more; to write those, use a JSON block.
  • To set a column's width, use a block attributes carrier at the start of a cell.

Horizontal rules

---, *** or ___ on a line of their own become a horizontal rule. Directly under a line of text, --- makes that text a level 2 heading instead, so put a blank line before it. *** and ___ are rules either way.

HTML

HTML is never interpreted. HTML tags are kept as plain text, exactly as typed, so Press <kbd>Enter</kbd> shows the tags on the page. A block of HTML becomes a paragraph of its lines, with line breaks between them.

Escapes and character references

A backslash before a punctuation character keeps it as that character: \*not italic\* is the text *not italic*. Character references such as &amp;, &copy; and &#35; become the characters they name.

What plain Markdown can't express

Some of what the editor can store has no plain Markdown syntax. The Folioso Markdown extensions carry each of these:

To write Use
A link to another Folioso page A page link
A mention of a person A mention
Underlined text A mark span
Paragraph, heading or image alignment Block attributes
An image's width and height Block attributes
A table column's width Block attributes
An empty paragraph or list item Block attributes with nothing set
A page with no blocks at all [](folioso:empty)
A code block language exactly as written verbatim
Anything else: lettered lists, merged table cells, cells with several blocks, headings with line breaks, links with other attributes A JSON block

Markdown features the editor has no equivalent for aren't supported, and are read as the CommonMark they are:

  • Footnotes: [^1] isn't a footnote.
  • Front matter: a --- block at the top of the text isn't metadata. It reads as a horizontal rule and a heading.
  • Heading ids, definition lists, maths and emoji codes stay as the text you typed.

Warning

A footnote definition whose text is a single word or address, such as [^1]: source or [^1]: https://example.com/paper, is read as a Markdown link reference definition. The definition line disappears, and its text is lost: [^1] then reads as ^1, or as a link to that address. Write footnotes as ordinary text, such as a numbered list at the end of the page, before sending them.