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:
folioso:KIND?name=value&name=value- Encoding. Names and values are percent-decoded:
%20is a space,%26an&,%3Dan=and%23a#. 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.
Page links
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.
[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. |
Follow [Deploy runbook](folioso:page?id=42&slug=9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b) before releasing.{
"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,
Untitledfor an untitled page, orRestricted pagewhen 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
idandslug, or a missingid, 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.
[@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. |
Ask [@Ada Lovelace](folioso:mention?id=5) to review.{
"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 thecharyou give.[Ada](folioso:mention?id=5)is refused because it doesn't start with@. With alabelparameter, 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](folioso:span?marks=MARK,MARK)| Parameter | Required | Description |
|---|---|---|
marks |
Yes | A comma-separated list of bold, italic, strike and underline, with no spaces. |
This is [important](folioso:span?marks=underline) and [hello, ](folioso:span?marks=bold)world.{
"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.
[](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.
[](folioso:attrs?textAlign=center)A centred paragraph.
## [](folioso:attrs?textAlign=right)A right-aligned heading{
"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.
[](folioso:attrs?width=480&textAlign=center){
"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:
| [](folioso:attrs?colwidth=[240])Name | Value |
| --- | --- |
| [](folioso:attrs?colwidth=[240])timeout | 30 |{
"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:
Above
[](folioso:attrs)
- [ ] [](folioso:attrs){
"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'stextAlign, an image'stextAlign,alt,title,widthandheight, and a table cell'scolspan,rowspan,colwidthandalign. Any other name is refused, and so are an image'ssrcand a heading'slevel, which the Markdown itself gives. textAlignmust beleft,centerorright.- 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:
[](folioso:empty){ "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:
```JS verbatim
let b = 2;
```{ "type": "doc", "content": [{ "type": "codeBlock", "attrs": { "language": "JS" }, "content": [{ "type": "text", "text": "let b = 2;" }] }] }- Everything in the info string before
verbatimis the language, spaces included, so```c sharp verbatimstoresc sharp. verbatimcounts only as the last of at least two words. A fence of just```verbatimis a code block whose language isverbatim.- With
verbatim,```mermaidand```plantumlstay code blocks rather than becoming diagrams, and```foliosois 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.
Introduction.
```folioso
{"type":"orderedList","attrs":{"start":1,"type":"a"},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Lettered"}]}]}]}
```{
"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 extraor```FOLIOSOis 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
languageattribute 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
textAlignother thanleft,centerorright. 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 |  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
 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
, a carriage return as , and the invisible character U+FEFF always as.
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Cost: 5 * 3 = 15 #1 <tag> [x] a|b & c\\d" }]
}Cost: 5 \* 3 = 15 \#1 \<tag\> \[x\] a\|b \& c\\dBare 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:
Ask [Ada](folioso:mention?id=5) to review.{
"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:
Some text [](folioso:attrs?textAlign=center){
"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."]
}
}