Send composer markdown to Mattermost as typed #271

Merged
robbertbos merged 1 commit from fix-composer-markdown-round-trip into main 2026-08-09 11:39:00 +00:00
Owner

Two tiptap-markdown serializer artefacts were reaching Mattermost verbatim. Both were pinned in markdown-mm-compat.ts as accepted deviations; on inspection one of them is a functional defect, not a cosmetic one.

~town-square was sent as \~town-square. tiptap-markdown escapes every ~ because GFM uses ~~ for strikethrough.

Mattermost's autolinkChannelMentions runs on the rendered text with /\B(~([a-z0-9.\-_]*))/gi, so the escape stopped the channel resolving. The old fixture comment claimed MM "renders \~town-square the same as ~town-square" — that is true for the visible text (MM's marked fork does have ~ in its escape class) but says nothing about the autolink, which is the point of typing ~channel.

unescapeLoneTilde drops the backslash from a lone tilde. A doubled ~~ keeps its escapes — that one really is markdown — and a lookbehind protects an escaped backslash the user typed in front of a tilde.

Task lists were sent as loose lists

The serializer puts a blank line between task-list items, which makes it a loose list; Mattermost then renders each item in its own paragraph with extra spacing.

tightenTaskLists removes a blank line only when a task item sits on both sides — a blank line between paragraphs, or between a task item and anything else, is untouched.

Left alone: the hard-break deviation

The third documented deviation (a two-space hard break serialising to a plain \n) is unchanged. I could not establish that it is a defect: MM's marked fork defaults to breaks: false and the webapp does not visibly set it, which would suggest a lost line break, but MM plainly does render single newlines as breaks in practice, so something else in that path accounts for it. Changing deliberate, tested behaviour on an unverified premise is not worth it; settling it needs a test against a real Mattermost.

Tests

Unit tests for both helpers, including the guards that make them safe: three-item runs, nested items, task-item-next-to-paragraph, ordinary bullet lists, \~\~, and a user-typed \~. Each guard was verified to fail with the guard removed.

The two fixtures now pin the corrected output. Full suite: 96 files, 714 tests green; typecheck and pre-commit clean.

Two tiptap-markdown serializer artefacts were reaching Mattermost verbatim. Both were pinned in `markdown-mm-compat.ts` as accepted deviations; on inspection one of them is a functional defect, not a cosmetic one. ## Channel links were escaped into plain text `~town-square` was sent as `\~town-square`. tiptap-markdown escapes every `~` because GFM uses `~~` for strikethrough. Mattermost's `autolinkChannelMentions` runs on the rendered text with `/\B(~([a-z0-9.\-_]*))/gi`, so the escape stopped the channel resolving. The old fixture comment claimed MM "renders `\~town-square` the same as `~town-square`" — that is true for the *visible* text (MM's marked fork does have `~` in its escape class) but says nothing about the autolink, which is the point of typing `~channel`. `unescapeLoneTilde` drops the backslash from a lone tilde. A doubled `~~` keeps its escapes — that one really is markdown — and a lookbehind protects an escaped backslash the user typed in front of a tilde. ## Task lists were sent as loose lists The serializer puts a blank line between task-list items, which makes it a loose list; Mattermost then renders each item in its own paragraph with extra spacing. `tightenTaskLists` removes a blank line only when a task item sits on both sides — a blank line between paragraphs, or between a task item and anything else, is untouched. ## Left alone: the hard-break deviation The third documented deviation (a two-space hard break serialising to a plain `\n`) is unchanged. I could not establish that it is a defect: MM's marked fork defaults to `breaks: false` and the webapp does not visibly set it, which would suggest a lost line break, but MM plainly does render single newlines as breaks in practice, so something else in that path accounts for it. Changing deliberate, tested behaviour on an unverified premise is not worth it; settling it needs a test against a real Mattermost. ## Tests Unit tests for both helpers, including the guards that make them safe: three-item runs, nested items, task-item-next-to-paragraph, ordinary bullet lists, `\~\~`, and a user-typed `\~`. Each guard was verified to fail with the guard removed. The two fixtures now pin the corrected output. Full suite: 96 files, 714 tests green; typecheck and pre-commit clean.
Send composer markdown to Mattermost as typed
Some checks failed
CI / pre-commit (pull_request) Successful in 1m45s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 57s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
security-scan / JS SCA (npm audit) (pull_request) Successful in 39s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 27s
security-scan / SBOM (trivy) (pull_request) Successful in 18s
CI / frontend-test (pull_request) Successful in 5m54s
test-build / build (backend) (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / backend-test (pull_request) Has been cancelled
test-build / build (frontend) (pull_request) Has been cancelled
test-build / build (pull_request) Has been cancelled
d2f48f7be6
Two serializer artefacts reached Mattermost verbatim.

tiptap-markdown escapes every `~` because GFM uses `~~` for strikethrough, so
`~town-square` was sent as `\~town-square`. Mattermost's channel autolinking
runs on the rendered text, and the escape stopped it resolving the channel. A
lone tilde is not markdown, so unescapeLoneTilde drops the backslash; a doubled
`~~` keeps its escapes, and an escaped backslash in front of a tilde is left
alone (the lookbehind guards the user's own `\~`).

The same serializer puts a blank line between task-list items, making it a loose
list that Mattermost renders with every item in its own paragraph.
tightenTaskLists removes a blank line only when a task item sits on both sides.

The two fixtures that pinned these as accepted deviations now pin the corrected
output.
robbertbos force-pushed fix-composer-markdown-round-trip from d2f48f7be6
Some checks failed
CI / pre-commit (pull_request) Successful in 1m45s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 57s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
security-scan / JS SCA (npm audit) (pull_request) Successful in 39s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 27s
security-scan / SBOM (trivy) (pull_request) Successful in 18s
CI / frontend-test (pull_request) Successful in 5m54s
test-build / build (backend) (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / backend-test (pull_request) Has been cancelled
test-build / build (frontend) (pull_request) Has been cancelled
test-build / build (pull_request) Has been cancelled
to c79da70912
All checks were successful
CI / pre-commit (pull_request) Successful in 1m55s
CI / release-scripts (pull_request) Successful in 13s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 58s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
security-scan / JS SCA (npm audit) (pull_request) Successful in 40s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 26s
security-scan / SBOM (trivy) (pull_request) Successful in 17s
CI / frontend-test (pull_request) Successful in 6m10s
test-build / build (backend) (pull_request) Successful in 2m34s
test-build / build (frontend) (pull_request) Successful in 2m31s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 13m12s
CI / e2e (pull_request) Successful in 5m56s
CI / release-scripts (push) Successful in 6s
security-scan / SBOM (trivy) (push) Successful in 11s
security-scan / Filesystem scan (trivy fs) (push) Successful in 15s
security-scan / JS SCA (npm audit) (push) Successful in 17s
security-scan / Python SAST (bandit) (push) Successful in 21s
security-scan / Python SCA (pip-audit) (push) Successful in 38s
publish-main / build (frontend) (push) Successful in 1m1s
publish-main / build (backend) (push) Successful in 1m6s
publish-main / build (push) Successful in 0s
CI / frontend-test (push) Successful in 1m13s
CI / pre-commit (push) Successful in 1m19s
CI / backend-test (push) Successful in 2m8s
CI / e2e (push) Successful in 3m13s
2026-08-08 21:43:28 +00:00
Compare
robbertbos deleted branch fix-composer-markdown-round-trip 2026-08-09 11:39:00 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle!271
No description provided.