Skip to content
Foliosodev
Menu
On this page

REST API

Folioso Markdown extensions

Folioso Markdown is ordinary Markdown plus a few extensions that carry what plain Markdown can't: page links, mentions, underline, alignment and other attributes, and any block with no Markdown form at all. This page is the reference for each extension, how Folioso writes Markdown when you read a page, and every error the extensions can produce.

None of the extensions is HTML, and none can be triggered by accident. Each is one of three ordinary Markdown constructs:

Construct Example Used for
A link whose address uses the reserved folioso: scheme [Deploy runbook](folioso:page?id=42&slug=…) Page links, mentions, mark spans, block attributes and the empty document
The word verbatim after a code fence's language ```js verbatim Exact code languages
A fenced block whose language is folioso ```folioso JSON blocks

You only need these to write what plain Markdown can't express. When you read a page with ?format=markdown, Folioso uses them for you, and sending that Markdown back unchanged keeps everything. See Content formats.

Parameters

A folioso: link takes its parameters as a query string after a ?, the way a web address does:

text
folioso:KIND?name=value&name=value
  • Encoding. Names and values are percent-decoded: %20 is a space, %26 an &, %3D an = and %23 a #. A + is a plain +, not a space. Characters that can't appear in a Markdown link address, such as spaces, must be percent-encoded, or the whole address wrapped in <…>. Otherwise Markdown doesn't see a link at all, and the carrier is kept on the page as plain text.
  • Typed values. A value that's valid JSON takes that JSON value. Anything else is a string.
  • Empty pairs (&&) are ignored. A pair with no = is refused. When a name is repeated, the last value wins.
Value as written Value stored
id=42 the number 42
id=%2242%22 or id="42" the string "42"
textAlign=center the string "center"
alt=null null
checked=true true
colwidth=%5B240%5D or colwidth=[240] the array [240]
label=Ada%20Lovelace the string "Ada Lovelace"

So to send a string that would otherwise read as JSON, such as "42", "true" or "null", put it in double quotes. Folioso does this itself when it writes a parameter: every value that isn't a string, and every string that would read as JSON, is written as JSON.

A link to another Folioso page, which the editor shows with the page's current title and lists in the target's Linked from. See Page links and mentions.

text
[label](folioso:page?id=ID&slug=SLUG)
Parameter Required Description
id Yes The target page's numeric id, as stored in its pageLink nodes. Usually a number.
slug No The target page's slug. Include it, as the editor does: the editor uses it to open the page.
markdown
Follow [Deploy runbook](folioso:page?id=42&slug=9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b) before releasing.
json
{
  "type": "doc",
  "content": [
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "Follow " },
        { "type": "pageLink", "attrs": { "id": 42, "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b" } },
        { "type": "text", "text": " before releasing." }
      ]
    }
  ]
}
  • The label is ignored when you send it. The page always shows the target's current title, so you can write anything between the brackets.
  • When Folioso writes a page link, the label is the target's title as you see it, Untitled for an untitled page, or Restricted page when you can't open the target. A title you can't see is never shown.
  • Nothing is looked up when you send one. A page link is stored as given, exactly as it would be from content. A target that doesn't exist, or that you can't open, isn't an error: the editor shows Restricted page to anyone who can't open it.
  • Bold, italic and the other marks around a page link apply to it: **[x](folioso:page?id=42&slug=…)** is a bold page link.
  • Any parameter other than id and slug, or a missing id, is refused.

Every page object the API returns carries the page's numeric id, so take it from Get a page, the page list or a search result.

Mentions

A mention of a person, which notifies them as a mention typed in the editor would.

text
[@Label](folioso:mention?id=ID)
Parameter Required Description
id Yes The person's user id, as stored in mention nodes.
char No The character the mention starts with. Leave it out for the usual @.
taggedBy No The user id of whoever added the mention.
label No The label, when it isn't the link text. Folioso writes this only for a label that isn't a string.
markdown
Ask [@Ada Lovelace](folioso:mention?id=5) to review.
json
{
  "type": "doc",
  "content": [
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "Ask " },
        { "type": "mention", "attrs": { "id": 5, "label": "Ada Lovelace" } },
        { "type": "text", "text": " to review." }
      ]
    }
  ]
}
  • The label is the link text without its first character, which must be the mention character: @, or the char you give. [Ada](folioso:mention?id=5) is refused because it doesn't start with @. With a label parameter, the link text isn't checked or used.
  • The label is display text. The mention is the person with that id. Whether they're notified follows the same rules as the editor. See Who you can mention.
  • Nothing is looked up. The mention is stored as given.
  • Marks around a mention apply to it, as for page links.

Your own user id is data.user.id in GET /me, so you can mention yourself or record yourself as taggedBy. The API doesn't list other people's ids: the only place to find one is an existing mention node in content you can read.

Mark spans

Applies marks to a run of text. Use it for underline, which Markdown has no syntax for, and for bold, italic or strikethrough where **, * or ~~ wouldn't work, such as around text that starts or ends with punctuation or a space.

text
[text](folioso:span?marks=MARK,MARK)
Parameter Required Description
marks Yes A comma-separated list of bold, italic, strike and underline, with no spaces.
markdown
This is [important](folioso:span?marks=underline) and [hello, ](folioso:span?marks=bold)world.
json
{
  "type": "doc",
  "content": [
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "This is " },
        { "type": "text", "text": "important", "marks": [{ "type": "underline" }] },
        { "type": "text", "text": " and " },
        { "type": "text", "text": "hello, ", "marks": [{ "type": "bold" }] },
        { "type": "text", "text": "world." }
      ]
    }
  ]
}
  • The text inside a span can hold other Markdown, such as *italic*, and the span's marks are added to it. Inline code inside a span stays plain inline code, because code can't carry other marks.
  • Links and inline code can't be listed in a span. Markdown doesn't allow a link inside a link, so a span can't hold a link, page link or mention, and can't go inside a link. Folioso writes underlined link text, and a page link or mention with marks, as a JSON block.
  • Any other parameter, or a mark not in the list, is refused.

Block attributes

Sets attributes on the block its line belongs to, for what Markdown can't say: alignment, image size, a missing alt text, a table column's width, or nothing at all, to make an empty paragraph.

text
[](folioso:attrs?name=value&name=value)

The carrier must have an empty label, [], and must be the first thing in its paragraph, heading or table cell. Each parameter becomes an attribute of that name with its typed value. Where it applies depends on where it sits:

Position Applies to
At the start of a paragraph The paragraph
After the # marks of a heading The heading
At the start of a line holding only an image The image
At the start of a table cell The cell, not the paragraph inside it
Alone on a line An empty paragraph, with any attributes given

Alignment

textAlign is center or right. Left is the default, stored as null, so it's never written. left is accepted too, and stored as "left". Any other value is refused.

markdown
[](folioso:attrs?textAlign=center)A centred paragraph.

## [](folioso:attrs?textAlign=right)A right-aligned heading
json
{
  "type": "doc",
  "content": [
    { "type": "paragraph", "attrs": { "textAlign": "center" }, "content": [{ "type": "text", "text": "A centred paragraph." }] },
    { "type": "heading", "attrs": { "level": 2, "textAlign": "right" }, "content": [{ "type": "text", "text": "A right-aligned heading" }] }
  ]
}

Image size and alt text

width and height are whole numbers of pixels. The editor's resize handles set width. textAlign places the image. alt=null stores no alt text at all, which ![](…) can't, because it reads as an empty alt text.

markdown
[](folioso:attrs?width=480&textAlign=center)![Architecture](https://example.com/architecture.png)
json
{
  "type": "doc",
  "content": [
    {
      "type": "image",
      "attrs": { "src": "https://example.com/architecture.png", "alt": "Architecture", "title": null, "width": 480, "textAlign": "center" }
    }
  ]
}

Column widths

At the start of a table cell, the carrier sets the cell's attributes. colwidth is an array holding the column's width in pixels. The editor sets it on every cell in a column you resize:

markdown
| [](folioso:attrs?colwidth=[240])Name | Value |
| --- | --- |
| [](folioso:attrs?colwidth=[240])timeout | 30 |
json
{
  "type": "doc",
  "content": [
    {
      "type": "table",
      "content": [
        {
          "type": "tableRow",
          "content": [
            { "type": "tableHeader", "attrs": { "colwidth": [240] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Name" }] }] },
            { "type": "tableHeader", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Value" }] }] }
          ]
        },
        {
          "type": "tableRow",
          "content": [
            { "type": "tableCell", "attrs": { "colwidth": [240] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "timeout" }] }] },
            { "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "30" }] }] }
          ]
        }
      ]
    }
  ]
}

Because a cell's carrier sets the cell, a paragraph inside a cell can't be aligned in Markdown. Folioso writes such a table as a JSON block.

Empty paragraphs

Markdown drops empty paragraphs. A carrier with no parameters, on a line of its own, keeps one. It works anywhere a paragraph can go, such as an empty list item or task item:

markdown
Above

[](folioso:attrs)

- [ ] [](folioso:attrs)
json
{
  "type": "doc",
  "content": [
    { "type": "paragraph", "content": [{ "type": "text", "text": "Above" }] },
    { "type": "paragraph" },
    { "type": "taskList", "content": [{ "type": "taskItem", "attrs": { "checked": false }, "content": [{ "type": "paragraph" }] }] }
  ]
}

Rules

  • Only the attributes the block has in the TipTap JSON reference can be set: a paragraph's textAlign, a heading's textAlign, an image's textAlign, alt, title, width and height, and a table cell's colspan, rowspan, colwidth and align. Any other name is refused, and so are an image's src and a heading's level, which the Markdown itself gives.
  • textAlign must be left, center or right.
  • A carrier with text inside its brackets, such as [x](folioso:attrs?…), is refused.
  • A carrier anywhere but the start of its paragraph, heading or table cell is refused. That includes the start of a second line of a paragraph, because the paragraph began on the line before.
  • A carrier on a line that splits into more than one block is refused. That happens when the line holds an image as well as text, because the image becomes a block of its own.

Empty document

A page whose document has no blocks at all:

markdown
[](folioso:empty)
json
{ "type": "doc", "content": [] }

It must be the whole of the Markdown. Anywhere else it's refused. It isn't the same as an empty page: sending "markdown": "" or null removes the body altogether. See Empty and blank bodies.

Exact code languages

Add the word verbatim after a code fence's language to store the language exactly as written, without the lower-casing and short names ordinary fences get:

markdown
```JS verbatim
let b = 2;
```
json
{ "type": "doc", "content": [{ "type": "codeBlock", "attrs": { "language": "JS" }, "content": [{ "type": "text", "text": "let b = 2;" }] }] }
  • Everything in the info string before verbatim is the language, spaces included, so ```c sharp verbatim stores c sharp.
  • verbatim counts only as the last of at least two words. A fence of just ```verbatim is a code block whose language is verbatim.
  • With verbatim, ```mermaid and ```plantuml stay code blocks rather than becoming diagrams, and ```folioso is an ordinary code block rather than a JSON block.

Folioso writes verbatim whenever an ordinary fence wouldn't give back the stored language. For example, a block stored with language js is written ```js verbatim, because ```js alone would become javascript.

JSON blocks

A fenced block whose language is folioso holds one top-level block as TipTap JSON. It's how anything without a Markdown form travels through Markdown unchanged.

markdown
Introduction.

```folioso
{"type":"orderedList","attrs":{"start":1,"type":"a"},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Lettered"}]}]}]}
```
json
{
  "type": "doc",
  "content": [
    { "type": "paragraph", "content": [{ "type": "text", "text": "Introduction." }] },
    {
      "type": "orderedList",
      "attrs": { "start": 1, "type": "a" },
      "content": [{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Lettered" }] }] }]
    }
  ]
}

Writing a JSON block

  • The info string is exactly folioso, in lower case, with nothing after it. ```folioso extra or ```FOLIOSO is an ordinary code block.
  • The block holds one JSON object with a string type: a single TipTap node, as described in the TipTap JSON reference. An array, a bare value, or JSON that doesn't parse is refused.
  • The node is stored exactly as written. Folioso doesn't check its type or attributes, as with content. Links and image addresses inside it aren't limited to the addresses ordinary Markdown allows.
  • Use one JSON block per top-level block, at the top level of the Markdown. Inside a list or a quote, a JSON block becomes a child of that list item or quote.
  • The JSON can span several lines and be indented however you like.
  • Fence it with more backticks than the longest run of backticks inside the JSON, and at least three.

When Folioso writes a JSON block

When you read a page as Markdown, a top-level block is written as a JSON block when its Markdown form wouldn't read back exactly. The block is written as it's stored, on one line. That includes:

  • a numbered list with a type, such as a lettered or Roman-numeral list;
  • a table with merged cells, cells holding more than one paragraph or a line break, header cells outside the first row, no header row, or an aligned paragraph in a cell;
  • a heading containing a line break;
  • a link with attributes other than the editor's defaults, a link title of "", or underline on link text;
  • a page link or mention that carries marks, such as a bold mention;
  • inline code containing a line break;
  • a code block with no language attribute at all;
  • an image whose address isn't one Markdown allows, or contains a line break;
  • a node or mark Folioso doesn't know, such as content written by a newer version of the editor;
  • an attribute the node doesn't have, on any block, or a textAlign other than left, center or right. A block attributes carrier only carries the attributes listed in its rules, so anything else on a paragraph, heading, image or cell makes its block a JSON block too.

Only the block that needs it becomes JSON. The rest of the page stays readable. Occasionally the block before it does too, when the two would otherwise run together as Markdown.

How Folioso writes Markdown

When you read a page with ?format=markdown, Folioso writes the Markdown in a fixed way, so that reading it back gives the stored document. You can write it the same way by hand, but you don't need to: any Markdown that means the same thing works.

Content Written as
Blocks Separated by one blank line.
Paragraph Its text, escaped (below). An empty paragraph is [](folioso:attrs).
Heading # to ######, a space, then its text.
Line break A backslash at the end of the line.
Bold, italic, strikethrough **…**, *…*, ~~…~~ when the text inside starts and ends with a letter or digit, otherwise a mark span.
Underline A mark span.
Inline code Backticks, more than the longest run inside, with a space inside each end when the code starts or ends with a backtick or space.
Link [text](<address> "title"), the address always in angle brackets.
Image ![alt](<address> "title") on its own line, after a block attributes carrier when needed.
Bulleted list -, or * when the block before it also used -, so that two lists side by side stay two lists.
Numbered list Numbered from its start, with ., or ) when the block before it also used ..
Task list - [ ] and - [x], alternating bullets as above. An empty item is - [ ] [](folioso:attrs).
Nested blocks Indented to line up with the item's text.
Quote > before every line.
Alert box > [!NOTE], [!TIP], [!WARNING] or [!CAUTION] and the title on the first line, then each body paragraph after a > line.
Code block A backtick fence longer than any run of backticks in the code, the stored language, and verbatim when needed.
Diagram A ```mermaid or ```plantuml fence holding its source.
Table A pipe table, when it has a header row and nothing a pipe table can't hold. Otherwise a JSON block.
Horizontal rule ---
Page link, mention As in Page links and Mentions.
Empty document [](folioso:empty)

Escaping

So that text reads back as text, Folioso escapes it as it writes:

  • These characters always get a backslash before them: \ ` * _ [ ] < > ~ | & #.
  • At the start of a line, so the line can't become a list, heading or other block, it also escapes a leading +, - or =, and the . or ) after a leading number: 1\. not a list.
  • A ! at the end of a run of text gets a backslash, so it can't combine with a following [ into an image.
  • Spaces, tabs, non-breaking spaces and other blank characters at the start or end of a line are written as character references, such as &#32; for a space, because Markdown would otherwise drop them.
  • A line break stored inside text, rather than as a line break node, is written as &#10;, a carriage return as &#13;, and the invisible character U+FEFF always as &#65279;.
json
{
  "type": "paragraph",
  "content": [{ "type": "text", "text": "Cost: 5 * 3 = 15 #1 <tag> [x] a|b & c\\d" }]
}
markdown
Cost: 5 \* 3 = 15 \#1 \<tag\> \[x\] a\|b \& c\\d

Bare web addresses in text need no escaping, because they aren't turned into links. See Bare addresses stay text.

Error messages

A malformed extension makes the whole request a 422, with the message under errors.markdown. Nothing is saved.

Message Cause
Unknown carrier folioso:KIND. A folioso: link of a kind that doesn't exist.
Malformed folioso: parameter "PAIR"; expected name=value. A parameter with no =.
A folioso:page link takes an id and an optional slug, and nothing else. A page link with no id, or another parameter.
A folioso:mention link takes an id and optional char, taggedBy and label, and nothing else. A mention with no id, or another parameter.
A folioso:mention label must start with its suggestion character (@). Mention text that doesn't start with @, or with its char.
A folioso:span link takes one parameter: marks. A span with no marks, or another parameter.
folioso:span marks must be among: bold, italic, strike, underline. A mark that can't go in a span, such as code.
A folioso:attrs carrier must have an empty label: [](folioso:attrs?…). Text inside a carrier's brackets.
A folioso:attrs carrier must be the first thing in its paragraph, heading or table cell. A carrier after other text in its paragraph, heading or cell.
A folioso:attrs carrier must start a paragraph, a heading or a line holding one image. A carrier on a line that splits into several blocks.
A folioso:attrs carrier can't set "NAME" on TYPE. An attribute the block doesn't have, or an image's src or a heading's level. TYPE is the node type, such as paragraph or tableCell.
textAlign must be one of: left, center, right. Any other alignment.
[](folioso:empty) must be the whole document. The empty-document carrier alongside other content.
A ```folioso block must hold one TipTap node as a JSON object with a string "type". A JSON block that doesn't parse, or isn't a single node.

For example:

markdown
Ask [Ada](folioso:mention?id=5) to review.
json
{
  "message": "A folioso:mention label must start with its suggestion character (@).",
  "errors": {
    "markdown": ["A folioso:mention label must start with its suggestion character (@)."]
  }
}

A carrier that isn't the first thing in its paragraph:

markdown
Some text [](folioso:attrs?textAlign=center)
json
{
  "message": "A folioso:attrs carrier must be the first thing in its paragraph, heading or table cell.",
  "errors": {
    "markdown": ["A folioso:attrs carrier must be the first thing in its paragraph, heading or table cell."]
  }
}