···11+/**
22+ * Serialise the PostEditor contenteditable DOM back to its flat-text
33+ * storage form (`@<rkey>` for entity mentions, `[label](url)` for
44+ * inline links, `\n` for line breaks). Exported so it can be unit
55+ * tested against fixtures built with happy-dom; the component holds the
66+ * editor element, this function holds the rules.
77+ */
88+99+function serialiseNode (node: Node): string {
1010+ if (node.nodeType === Node.TEXT_NODE) {
1111+ return node.textContent ?? ''
1212+ }
1313+ if (!(node instanceof HTMLElement)) return ''
1414+1515+ if (node.dataset.mentionRkey) {
1616+ return `@${node.dataset.mentionRkey}`
1717+ }
1818+ if (node.dataset.linkUri) {
1919+ return `[${node.textContent ?? ''}](${node.dataset.linkUri})`
2020+ }
2121+ if (node.tagName === 'BR') {
2222+ return '\n'
2323+ }
2424+2525+ let inner = ''
2626+ for (const child of node.childNodes) inner += serialiseNode(child)
2727+2828+ // Browsers wrap newlines in <div>/<p> on Enter, and may nest chips
2929+ // inside them. Recursing keeps chip semantics across line breaks.
3030+ if (node.tagName === 'DIV' || node.tagName === 'P') {
3131+ return (inner.startsWith('\n') ? '' : '\n') + inner
3232+ }
3333+ return inner
3434+}
3535+3636+export function serialiseEditor (root: HTMLElement): string {
3737+ let out = ''
3838+ for (const node of root.childNodes) out += serialiseNode(node)
3939+ return out.replace(/^\n+/, '')
4040+}