Markdown reference — every syntax this site understands
What you may write on this site, which of the two engines will read it, and what each one does with the parts you might expect and will not get. The block synta
What you may write on this site, which of the two engines will read it, and what each one does with the parts you might expect and will not get. The block syntax has its own page, 13 Markdown blocks; the engine that builds /docs is described in 11 Guides system, and the honest limits are in 23 Known gaps.
Two engines, one syntax
This is the single most misunderstood thing about writing on this site, so it comes first.
There are two renderers. Both wrap markdown-it@15 (the default preset: CommonMark, plus tables and strikethrough), markdown-it-container@4 and highlight.js@11. They are configured differently and they are not interchangeable.
| Surface | Module | Class prefix | Who writes it |
|---|---|---|---|
/docs guides — every file in docs/ — and the guides preview in the admin panel |
lib/docs/index.js |
d- |
staff, and us |
Forum threads, replies, in-place post edits, POST /api/forum/preview |
lib/markdown.js, renderForumPost |
fr- |
anybody with an account |
/updates entries |
lib/markdown.js via createRenderer({ prefix: 'up' }) in lib/updates.js |
up- |
staff |
The guides engine is more capable: wikilinks, relative links, callouts, tabs, front matter. The posts engine is deliberately stricter, because its authors are strangers and its HTML is injected into somebody else's thread. The posts rules are not an accident of implementation — read Posts-only rules before assuming a guide trick will paste into a forum post.
Everything below is labelled by engine. Where the two differ, the difference is the point.
Standard markdown
CommonMark, with GFM tables and strikethrough, on both engines.
| Syntax | Guides (/docs) |
Posts and updates | Notes |
|---|---|---|---|
# H1 … ###### H6 |
Every heading gets a slugified id and tabindex="-1". The leading H1 is moved into the page header and stripped from the body, so a guide's H1 is its title, not its first line. |
Every heading gets an id of the form fr-h-<slug> (or up-h-<slug>), slug truncated to 48 characters. No table of contents. |
Duplicate heading text: the first is slug, the second slug-1, the third slug-2. The counter is per render, so it restarts in every tab pane and on every page. Guides list level 2 and deeper in the outline; a posts heading is linkable but never listed. |
**bold**, __bold__ |
Yes | Yes | |
*italic*, _italic_ |
Yes | Yes | |
~~struck~~ |
Yes | Yes | GFM extension, enabled by the default preset. |
`inline code` |
Yes | Yes | The backticks are removed; the content is escaped. |
| Fenced code | Yes, with syntax highlighting | Yes, with syntax highlighting | See Syntax highlighting. |
> blockquote |
Yes | Yes | A second > level nests. |
- item |
Yes | Yes | * and + work too. |
1. item |
Yes | Yes | The numbers in the source are ignored; the list is numbered from 1. |
| Nested lists | Yes, indentation is preserved | No — see the warning below | |
| GFM tables | Yes | Yes | The delimiter row is mandatory: |---|---:|. |
--- (also ***, ___) |
Horizontal rule | Horizontal rule | |
 |
Yes, rewritten to an absolute /docs/… URL |
Yes, only http(s):// or a site path |
|
[text](href) |
Yes, relative targets are resolved to a guide | Yes, an allowlist decides | |
[text][ref] / [ref]: url |
Yes | Yes | |
<https://example.com> |
Yes | Yes | |
Bare https://example.com |
Linked (linkify) | Linked (linkify) | Linkify only fires with a scheme. www.example.com is not linked on either engine. |
Setext headings (Title\n=====) |
Yes | Yes | === is an H1, --- under a line is an H2 — and a --- under a non-blank line is a setext H2, not a rule. |
Hard line break: two trailing spaces, or a trailing \ |
Yes | Yes | |
©, &, < |
Decoded | Decoded | HTML entities are markdown, not HTML, so they survive html: false. |
Nested lists: the posts warning
Posts and updates are normalised before the markdown parser ever sees them. cleanMarkdown() in lib/forum.js (duplicated verbatim in lib/updates.js) does, in order:
CRLFandCRbecomeLF- control characters are removed
- every run of spaces and tabs becomes one space
- three or more blank lines become two
- leading indentation on every line is removed
- trailing whitespace is trimmed
- the result is capped — 8000 characters for a post, 32000 for an update
Steps 3 and 5 are why nesting does not survive in a post:
- A nested list arrives as two flat lists, one after the other.
- An indented code block — four spaces or a tab — arrives as an ordinary paragraph.
So: write code in a fenced block in a post or an update, never an indented one. In a guide, indentation is untouched and both work.
Tables
The delimiter row is always required. A table needs a header row, a delimiter row, and at least one body row:
| Syntax | Guides | Posts |
|---|---|---|
| `[[wikilink]]` | yes | no |
| `[link](./other.md)` | yes | refused unless it is a path |
What breaks it: a missing or empty delimiter row, a gap row in the middle, uneven column counts, or a cell whose text contains an unescaped |. Any of those stops the table and everything after the gap becomes paragraphs.
Syntax highlighting
Any language that hljs.getLanguage(lang) accepts is highlighted server-side into <pre class="hljs"><code>. There is no client-side highlighter and no language download: the fence is processed at render time.
Twenty-five ids that are known to work today:
bash, c, csharp, css, diff, dockerfile, elixir, go, graphql, haskell, html, ini, java, javascript, json, kotlin, lua, markdown, nginx, perl, php, powershell, python, ruby, rust, scala, scss, shell, sql, swift, toml, typescript, xml, yaml.
An unknown language still renders the fence, just unhighlighted — a fence tagged nonsense prints the code verbatim. There is no error and nothing in the page says the language was not recognised.
mermaid is not a highlight.js language, so a fence tagged mermaid prints as a plain code block of diagram source. There is no diagram renderer on this site; the way to show a flow is prose, a table, or an image.
On /docs only, two things are added around the HTML: every <pre> inside the prose gets a Copy button, and every image opens in a lightbox when it is clicked (public/js/docs.js). Neither happens on the forum.
Line breaks
| Guides | Posts and updates | |
|---|---|---|
breaks |
false |
true |
Guides: a single newline is not a line break. End a line with two spaces or a \, or leave a blank line between the two paragraphs.
Posts and updates: a single newline is a <br>, which is why a support answer can be written as loose lines and still look right.
Everything else about the two engines is in 11 Guides system.
Guides-only features
None of the following exist on the forum or in /updates:
| Feature | Where it is documented |
|---|---|
| YAML front matter | below |
| Wikilinks | below |
Obsidian > [!type] callouts |
below |
| Tabs | below |
| Relative links and relative images | below |
| Heading anchors, table of contents, prev/next, backlinks | below |
The guides preview (POST /api/admin/docs/preview) |
below and in 16 Admin panel |
YAML front matter
The block must start on line 1 with ---; there is nothing above it, not even a blank line. Keys are lower-cased before they are read, so Title: and title: are the same key.
Only four keys are read. Everything else in the block is preserved by the editor and ignored by the renderer.
title
title: Markdown reference — every syntax this site understands
Quotes around the value are stripped. Falls back to the first # H1 in the body, then to the filename with - and _ turned into spaces. The title drives the <title>, the page heading, the OpenGraph and Twitter title, the sidebar label, the prev/next cards, backlink labels, the notes-map label (the part before the em dash), and the per-guide OG card.
description
description: What you may write, and what each engine does with it.
Everything after the first colon is the value, so a colon inside the description is fine. Quotes are stripped. Falls back to the first paragraph after the H1, stripped of markdown punctuation and cut at 160 characters — and with no H1 at all the fallback is the empty string, so a guide without an H1 and without a description gets no meta description.
tags
Three accepted forms.
Block list — items must be indented, and this is the form the admin panel writes:
tags:
- reference
- markdown
Inline list:
tags: [reference, markdown]
Single value:
tags: reference
Tags are compared case- and whitespace-insensitively everywhere they are filtered or counted, so #Tilt and tilt are one tag and a filter that says "no guides" when three exist cannot happen.
order
order: 120
Read with parseInt. Absent means 999. Folders sort before files, and a folder's README.md sorts first within it, so the shipped ones carry order: 0. Integers only: order: soon parses to NaN, every comparison against NaN is false, and the sort silently falls back to name order for that folder.
A complete front-matter block, in the shape the admin panel writes it:
---
title: Markdown reference — every syntax this site understands
description: What you may write, and what each engine does with it.
tags:
- reference
- markdown
order: 120
---
Front matter on save from the admin panel
lib/docs/write.js rebuilds the block on every save (buildFront()):
| Key | What happens |
|---|---|
title |
always rewritten |
description |
rewritten only when non-empty; an empty one is dropped |
tags |
always rewritten as a block list, capped at 12 items |
order |
rewritten only when it parses as a finite number, rounded |
| any other key | kept verbatim, original line text and original position |
The body is capped at 200000 characters. A save that changes nothing writes nothing at all.
The inline tag form does not survive a round trip. splitFront() reads tags: [reference, markdown] as one item, and buildFront() writes it back as:
tags:
- [reference, markdown]
which the renderer then reads as the single tag [reference, markdown]. Use the block form.
Wikilinks
[[note]]
[[note|label]]
[[note#section]]
[[note#section|label]]
Resolution order, first hit wins, all case-insensitive:
| # | Map | Test |
|---|---|---|
| 1 | full slug | the target, .md stripped |
| 2 | full slug | <this guide's folder>/<target> |
| 3 | last path segment | only when the target has no / |
| 4 | title | the full title, first match wins |
The link is /docs/<slug> plus # plus the slugified anchor.
Three things that do not work:
#section— an empty target resolves to nothing, so the reader sees the literal text#section. Write[#section](#section)instead.- An unresolved target degrades to its label, or to the bare target text.
noperenders as the wordnope. It never becomes a link to nowhere. - A wikilink whose text contains a backtick is left completely alone. That is the escape hatch, and it is the reason the rest of this page writes every wikilink inside a code fence.
Expansion skips lines inside fenced code blocks, so a sample in a guide is safe if it is in a fence.
In the guides preview the resolver is () => null, so wikilinks in the admin preview do not resolve — they show as their label text. That is deliberate: the preview endpoint has no index to resolve against, and a link that would 404 on publish is better shown as text than as a link that lies.
Link extraction for backlinks and the notes map is not fence-aware. A wikilink written inside a code sample still creates a backlink and a graph edge.
Obsidian callouts
Two forms, and they are not the same thing.
Form A — the > [!type] form (guides only)
> [!note] Optional title
> The body of the note.
>
> A second paragraph.
>
> - a list
> - works too
Rewritten to the container form before parsing. Rules:
- the type is case-insensitive and must be one of the seven guides types
- text after
]becomes the title - a blank
>line is a paragraph break - a fenced code block inside stops the conversion — the callout ends there
- an indented
> [!note]is not converted - this form does not exist on the forum. On a post it is a plain blockquote whose first line reads
[!note].
This form is worth using in guides that get exported to Discord, because toDiscord() understands it and turns it into > **NOTE** — title.
Form B — the container form (both engines)
::: note Optional title
The body.
:::
Seven types on guides, four on posts:
| Type | Guides default title | Posts default title |
|---|---|---|
note |
Note | Note |
info |
Info | Good to know |
tip |
Tip | Tip |
warning |
Warning | Careful |
danger |
Danger | not registered |
success |
Success | not registered |
example |
Example | not registered |
On the forum and in updates, danger, success and example are not containers at all: the ::: lines print as literal text around ordinary markdown, which is a loud, obvious failure rather than a silent one.
Titles are taken literally, quotes and all — only the whitespace is trimmed. If you want the quotes off the title, do not write them.
Tabs (guides only)
::: tabs
::: tab "Linux"
Content for the first tab.
::: tab "macOS"
Content for the second.
:::
:::
The closing rules are unusual and worth writing down:
::: tabsmust be exactly that. The rule is^:::\s*tabs\s*$, so nothing may follow it — no title, no options, not even a comment.::: tabs "Linux"is not a tabs block.::: tab "Title"opens a pane. The quotes are optional and stripped; without a title the button readsTab.- a bare
:::closes the current pane, and the next non-blank line decides what happens: another::: tabopens the next pane, and anything else makes that:::the wrapper close as well. This is why the shape above has four:::lines and not three. - an unclosed wrapper eats the rest of the document. There is no auto-closer for tabs.
- three nesting levels render; a fourth prints
Nested tabs are not supported.and nothing else.
Each pane's body is rendered with a fresh environment, and three things follow from that, all of which are silent:
- Headings inside a tab get ids counted from zero again, and they are not collected, so they appear in no outline. Two tabs with the same
## Installheading produce the same id twice. - Relative links and relative images inside a tab resolve as if the guide were at the root of
docs/, because the pane render is called withoutslugDir. - A
::: stepsinside a tab is the one thing this does not affect: a recording is addressed indocs/recordings/and not against the guide, so it resolves from inside a tab exactly as it does outside one.
Inside a tab, use wikilinks or absolute /docs/... URLs.
Relative links and images (guides only)
resolveDocHref() tries, in order:
- a bare
#anchor— left alone - anything with a scheme (
https:,mailto:,data:…) — left alone /docs/...— already a doc link, turned into an absolute site URL- anything else starting with
/— left alone - relative to the guide's folder, with
.mdstripped; the target must exist as a file, or as<target>/README.md, and a..segment is refused
In this folder, a sibling is therefore just a plain relative link:
The SPCF file format is described in [14 SPCF format](14-spcf-format.md).
The .md extension is optional when the file exists: [14 SPCF format](14-spcf-format) works too.
Images are simpler and less careful. A relative src is joined to the guide's folder and rewritten to /docs/<path> — with no existence check and no extension check. The renderer always emits:
<img class="d-zoom" src="…" alt="…" loading="lazy" referrerpolicy="no-referrer" />
There is no way to set a width, a height or a class: the tag is built by hand, so  does not do what it does on a wiki. Only .png .jpg .jpeg .webp .gif .svg .avif are served from /docs/<path>; anything else is a 404, even if the file is on disk.
Anchors, TOC, prev/next, backlinks
Anchors. slugify() lowercases the heading, replaces every run of non-alphanumeric characters with a single -, trims leading and trailing dashes, and falls back to sec when nothing is left. Every heading carries tabindex="-1" so a link into the middle of a guide can move the keyboard focus there.
Outline. Captured while rendering, filtered to level 2 and deeper, and printed in the middle column as On this page. Headings inside tab panes are not in it, because tab bodies render with their own TOC array. An empty outline prints No sections. The outline highlights the current heading through an IntersectionObserver with a -20% 0px -70% 0px margin, and scrolls itself when the active entry moves out of view.
Prev/next come from the global reading order — st.flat, folders first, then by order — not from the folder the guide is in. The pager therefore crosses folder boundaries on purpose: the last guide in about/ is followed by the first guide in the next folder.
Backlinks are printed in the rail under Linked from, sorted by title. They are computed by scanning each guide's raw markdown for wikilinks and relative links — not from the rendered HTML — which is why a wikilink inside a code fence still counts.
Posts-only rules
createRenderer() in lib/markdown.js, in full:
new MarkdownIt({
html: false,
linkify: true,
breaks: true,
typographer: false,
highlight: /* server-side highlight.js */,
});
| Option | Consequence |
|---|---|
html: false |
Raw HTML is never parsed. <script>alert(1)</script> appears as escaped text on the page. There is no tag allowlist to maintain because there is no tag handling at all. |
linkify: true |
Bare http:// and https:// URLs become links. www. does not. |
breaks: true |
A single newline is a <br>. |
typographer: false |
No smart quotes, no -- to an en dash, no ... to an ellipsis. Type ’, — and … directly if you want them. |
Plus the normalisation described in Nested lists: cleanMarkdown(), capped at 8000 for a post and 32000 for an update.
Link and image safety in posts
safeUrl() allows exactly three shapes:
- a site-relative path starting with a single
/ - a fragment starting with
# http://orhttps://with a host
Everything else is refused: javascript:, data:, vbscript:, blob:, file:, mailto:, tel:, ftp:, and a protocol-relative //host.
What a refusal looks like:
- a refused link emits no tag at all. Not a tag without an href — no tag.
[click](javascript:alert(1))leaves the wordsclickas plain text. - a refused image becomes a visible span carrying the alt text, so
./shot.pngin a post shows whatever you wrote as alt text rather than disappearing. That is deliberate: a post that quietly loses its picture is a bug report nobody can file.
Attributes:
| Posts and updates | Guides | |
|---|---|---|
| External link | target="_blank" rel="noopener noreferrer nofollow ugc" |
rel="noopener", opened in the same tab |
| Internal link | rel="noopener" |
rel="noopener" |
| Image | src, alt, loading="lazy", referrerpolicy="no-referrer", class fr-img / up-img — nothing else |
class d-zoom, and a lightbox |
Guides rely on markdown-it's own validateLink for hrefs rather than on this allowlist.
Changelog blocks and who may use them
::: added, ::: changed, ::: fixed, ::: removed, ::: security — the same vocabulary on all three surfaces, imported from one list so a sixth kind would be added in one place.
| Type | Badge word | Guides | Posts, by a member | Posts, by staff | Updates |
|---|---|---|---|---|---|
added |
New | yes | text, no wrapper | badge | badge |
changed |
Changed | yes | text, no wrapper | badge | badge |
fixed |
Fixed | yes | text, no wrapper | badge | badge |
removed |
Removed | yes | text, no wrapper | badge | badge |
security |
Security | yes | text, no wrapper | badge | badge |
On /docs there is no gate. In a post the decision is made at render time, from the author's standing (env.changelog, set from isStaffPerson):
- moderators get the coloured entry with the badge
- members get their own text with no wrapper at all — the fence is still consumed, so the words still show, but there is no badge and no border
Nothing in the database is rewritten. Promoting somebody starts rendering their existing posts correctly, immediately.
The preview endpoint uses the viewer's flag, not the author's, so a member previewing their own post sees what a member would get — which is the honest thing for a preview to show.
Updates always render them; only staff can write an update.
Why the gate exists at all. Hiding the toolbar buttons stops nobody. Markdown is plain text: anybody can type ::: security into a post and get the red Security badge — the one badge on this site that reads as "the people who run this are telling you to act". The gate is on the renderer, where it cannot be bypassed by typing.
Unclosed containers are repaired automatically
Before parsing, closeDanglingContainers() walks the source line by line and closes any container this surface knows about that the author left open. The set is:
note, info, tip, warning, added, changed, fixed, removed, spoiler
Rules:
- any opening fence ends whatever was open, whether or not this surface manages the new one — a
::: fixedfollowed by a::: stepsis an entry and then an embed, not one thing inside the other - a closing
:::on its own line is the author's, not a synthetic one - a code fence — three backticks, or three tildes — inside a container ends that container's scope
- a container still open at the end of the text is closed at the end
So three changelog entries written the way a person writes a list — no closing fences — render as three entries:
::: added
The tilt gesture.
:::
::: changed
The OLED timeout is now per profile.
:::
::: fixed
The importer no longer loses the second group.
:::
This never helps ::: steps, ::: video or ::: tabs. Those are not in the set. steps and video are consumed whole by their own block rules, so an unclosed one eats the rest of the post the same way. tabs is a guides-only block and has no repair pass at all.
Excerpts
plainText(source, max) produces the one-line, no-markup preview used by the thread list (160 characters) and by update teasers (200). In order:
- images become
[image] - links become their label text
- block-opening lines (
::: …) become a space - heading markers,
>,-/*/+and1.list markers are stripped *,_and~are removed- all whitespace collapses to single spaces, and the text is trimmed
- anything longer than
maxis cut on a word boundary and gets an ellipsis
Note that step 3 removes the fence line but keeps the words inside it, so a changelog entry contributes its text to the excerpt.
What is NOT supported
Guides
| Not supported | What happens instead |
|---|---|
| Raw HTML, HTML comments | Escaped text on the page. <b>x</b> shows the tags. |
| Footnotes | [^1] is literal text. There is no footnote plugin. |
| Definition lists | Not a CommonMark construct; a paragraph followed by : term. |
Math ($…$, $…$) |
Literal text. No math renderer. |
| Mermaid | A fence tagged mermaid is a plain code block — see Syntax highlighting. |
Task lists (- [ ], - [x]) |
A bullet whose text starts with a literal [ ]. |
Emoji shortcodes (:tada:) |
Literal text. |
Nested containers — a ::: tip inside a ::: note |
Breaks. The closing ::: closes the outer container first, and the inner opener is left as text. Tabs, steps and video inside a callout break for the same reason. |
#section |
Literal text #section. |
::: tabs with anything after it |
Not a tabs block; prints as text. |
Multi-line titles on ::: tab |
Only the first line is the title. |
| A single newline as a line break | Use a blank line or a trailing \. |
| Image sizes, image classes, image titles | The <img> tag is fixed. |
Serving arbitrary files from /docs/… |
Seven image extensions and .rec.json, nothing else. |
Posts and updates, additionally
| Not supported | What happens instead |
|---|---|
> [!note] callouts |
A blockquote whose first line reads [!note]. |
| Wikilinks | Literal … text. |
::: tabs |
Literal ::: lines around ordinary markdown. |
| Relative links and relative images | A relative link target is refused as a URL; a relative image src is refused and shows its alt text. |
::: danger, ::: success, ::: example |
Literal ::: lines. |
| The guides preview | The preview endpoint is renderForumPost, not the guides engine. |
| Indentation for nesting or code | Normalised away before parsing. |
Cheat sheet
Everything that works, in one paste-able block:
# Heading through ###### heading (the first H1 becomes the page title)
**bold** *italic* ~~struck~~ `inline code`
> A blockquote.
- a bullet
- another bullet
1. a numbered item
| a | table |
|---|---|
| cell | cell |
- - -
[link](https://example.com) [reference link][ref] <https://example.com>
https://example.com becomes a link; www.example.com does not.

A hard break at the end of this line (two spaces)
[ref]: https://example.com
::: note Optional title
Body.
:::
::: tip
:::
::: warning
:::
::: info
:::
::: danger
:::
::: success
:::
::: example
:::
::: spoiler "Click to reveal"
Hidden text.
:::
::: added
:::
::: changed
:::
::: fixed
:::
::: removed
:::
::: security
:::
::: steps "Paint a trigger" recordings/paint.rec.json
:::
::: video "Title" https://youtu.be/VIDEOID at=1:20
:::
And the things that silently do not work — each of these renders, just not as what it looks like:
<script>alert(1)</script> escaped text, both engines
[^1] and [^1]: a footnote literal text
- [ ] a task a bullet reading "[ ] a task"
$t$ and $x$ literal text
:tada: literal text
[[#section]] the text "#section"
::: tabs Linux literal text — the rule wants exactly "::: tabs"
::: danger a callout on a guide, literal text on a post
indented code a paragraph in a post
a single newline as a break a space in a guide, a <br> in a post
 the size is part of the path
:::
The editor toolbar and keyboard shortcuts
public/js/markdown-editor.js exports one constructor, window.SPMarkdown.create({...}), and window.SPMarkdown.MAX (8000, the default). All four editors on the site are that same code, so a fix lands in all four.
| Surface | blocks |
max |
Preview endpoint | Draft key |
|---|---|---|---|---|
| New thread | false |
8000 | POST /api/forum/preview, field body |
sp:forum:draft |
| Reply | false |
8000 | POST /api/forum/preview, field body |
sp:forum:reply:<threadId> |
| In-place post edit | false |
8000 | POST /api/forum/preview, field body |
none |
| Updates form | true |
32000 | POST /api/forum/preview, field body |
sp:updates:draft |
| Guides editor (admin panel) | true |
32000 | POST /api/admin/docs/preview, field content plus slug |
sp:admin:guide |
blocks: true means the changelog row exists, and it also switches the bar to the two-row layout. A narrow forum composer keeps the single scrolling strip, which is the better use of the height there.
Row 1 — formatting
Every button wraps the selection and leaves the caret inside the markers, which is the one behaviour markdown toolbars usually get wrong. Wrapping something that is already wrapped unwraps it instead.
| Button | Inserts |
|---|---|
B |
**bold text** |
I |
*italic text* |
S |
~~struck text~~ |
</> |
`code` |
{ } |
a fence — see the caveat below |
Link |
prompts for the URL, then [text](https://) |
IMG |
prompts for the URL, then  |
Steps |
::: steps "Title" recordings/name.rec.json plus a closing ::: |
❝ |
> on every line the selection touches |
• |
- on every line the selection touches |
1. |
1. on every line the selection touches |
H |
## on every line the selection touches |
The Link and IMG buttons refuse anything that is not http:// or https:// with an alert, which matches the image allowlist rather than being a surprise.
The Steps button substitutes the current selection into the filename, slugified; with a selection it becomes the title instead. The caret lands between the fences, because a block with an empty line in it is a form and the typing starts there.
Row 2 — blocks
| Button | Inserts |
|---|---|
Note |
::: note … ::: |
Tip |
::: tip … ::: |
! |
::: warning … ::: |
Video |
::: video "Title" https://youtu.be/ … ::: |
Spoiler |
::: spoiler "Click to reveal" … ::: |
Two caveats worth knowing before you press them:
- the Video button inserts an empty
https://youtu.be/with no prompt. Until you paste an id there, the preview shows No YouTube video athttps://youtu.be/. - the
{ }button declaresblock: true, and that flag is never read — there is no code path that looks at it — so it behaves as an inline wrap and puts a three-backtick pair before and after the selection rather than opening a fence on its own line.
Row 3 — changelog
Only rendered when blocks: true, so only in the updates form and the guides editor:
New (::: added), Changed (::: changed), Fixed (::: fixed), Removed (::: removed), Security (::: security). Each inserts its fence with an empty line between, so the caret lands in the middle.
Write / Preview
The preview is rendered by the server, not by a JavaScript re-implementation, so what you see is what will be posted. It is debounced at 500 ms, guarded by a sequence number so a slow earlier render cannot land after a newer one, and aborted outright when you switch back to Write. While the preview is showing, the tools row is hidden but keeps its width, so switching back does not move the text under the cursor.
A ::: video inside a preview arrives as HTML, so its chapter chips are re-wired to the frame with SPVideo.init() after each render.
Counter and drafts
The counter reads n / MAX, gets is-near above 90 % and is-over at the limit, and the textarea carries a matching maxLength. The number the counter shows and the number the server keeps are the same number on every surface — test/limits.test.js fails if that ever stops being true.
Drafts are written to localStorage on every input and restored only into an empty box, so a draft can never overwrite something you are already typing. Clearing the box removes the draft.
Keyboard
| Keys | Result |
|---|---|
Ctrl/Cmd + Enter |
Works. Submits, from onSubmit. |
Ctrl/Cmd + B |
Broken. |
Ctrl/Cmd + I |
Broken. |
Ctrl/Cmd + K |
Broken. |
The handler at public/js/markdown-editor.js:510 reads an undefined BUTTONS. The module declares FORMAT_BUTTONS, BLOCK_BUTTONS and ENTRY_BUTTONS and no combined list, so the line throws a ReferenceError after preventDefault() has already run. Nothing happens and the browser default is swallowed too, so the key appears dead. The one-word fix is FORMAT_BUTTONS. The toolbar buttons themselves work; only the shortcut is dead, and the tooltips still advertise the shortcut. This is written up in 23 Known gaps.
The cheat-sheet link
The Markdown link in the footer of every editor opens a panel with the core syntax, the block syntax, the video options (at=1:20, 0:46-1:25, chapter lines) and — only where blocks is on — the five changelog types.
Related
- 11 Guides system — the guides engine itself: the index, the URLs, the three-column reader, the notes map.
- 13 Markdown blocks — every
:::block, its options and its failure modes. - 08 Walkthrough recorder — where a
.rec.jsoncomes from. - 23 Known gaps — what is broken, stated plainly.