Skip to main content
Inkkit edits RTF (Rich Text Format) documents in formatted mode, beside Markdown and TXT. Your app converts the file, and Inkkit edits the converted document. Offer RTF by listing 'rtf' in the formats option; it is never offered by default.

How a host opens and saves RTF

Inkkit never reads or writes RTF itself. Apple’s text system reads RTF reliably and a browser can’t, so your app converts the file with the InkKitRTF Swift package and gives Inkkit a RichTextDocument, a JSON form of the document’s blocks and runs:
  1. Your app reads the file with RTFConverter.document(fromRTF:) and passes the document’s JSON to loadDocument as { format: 'rtf', document }.
  2. The person edits it in formatted mode.
  3. Your app takes snapshot, whose document is the current document, and writes it with RTFConverter.rtfData(from:).
Host RTF documents shows the Swift side. Snapshots stay synchronous: no call to the native converter happens during a save.

What an RTF document keeps

Inkkit’s own style replaces what the converter drops. That is the structure promise: saving keeps the structure and marks, and the first save after an edit rewrites the whole file in Inkkit’s style, the style of TextEdit’s default document. Inkkit doesn’t write files: your app decides when to save, so write only after the person changes something, and an unedited file keeps its own bytes. A link’s underline is how the link is drawn, so a run with a link never also has underline: the converter and Inkkit both remove it, and the underline command is unavailable on link text. One link rule. A link keeps any address that isn’t empty and follows the rule for text with no control character, such as http://[, whether it was loaded, added or pasted; an address you add loses the spaces at its ends. The system’s RTF writer percent-encodes an address, so the converter also records each link’s exact address and restores it when it reads the file back. When the record no longer matches the links, for example after the file was edited in TextEdit, or when two links side by side would read as one, the converter keeps the system’s addresses, such as http://%5B. A link that a person adds or pastes must also pass Inkkit’s link safety: a relative address, or an http, https, mailto or tel address or a scheme in linkSchemes. A link loaded from a file keeps its address whatever its scheme, and Inkkit opens it only when link safety allows. Text. A run never holds a line or paragraph separator, U+FFFC, U+0000 or a lone surrogate. Loading refuses such a document, and an edit that would leave one in the text is refused and changes nothing: insertText returns false, and replaceAll counts no replacement.

Refused, never dropped

The converter refuses a file that holds content the document can’t represent, rather than losing it: pictures and attachments (RTFD), merged table cells, tables inside tables, lists inside tables, headers and footers, footnotes, comments and equations. It also refuses a table of more than 1,000 columns or 100,000 cells, since every row is as wide as the widest and a short file could otherwise declare millions of empty cells. It throws RTFConversionError.unsupportedContent with the kinds it found, and your app tells the person the file can’t be opened for editing. Old-style Word \pntext list markers read as plain paragraphs, keeping their text.

Why there are no headings

RTF has no heading that its readers report: a heading written to RTF comes back as a bold paragraph, whichever app wrote it. An RTF document in Inkkit therefore has no headings. headings() is empty, and the heading commands are unavailable. A heading converted or pasted from elsewhere becomes a bold paragraph.

What the editor refuses

An RTF document holds paragraphs, lists, tables, line breaks and the eight marks, and nothing else. Inkkit keeps everything else out in three ways:
  • Commands. commandState reports the commands for headings, code blocks, quotes, callouts, tasks, highlight, images, footnotes and reference definitions as unavailable, and format returns false for them. Table column alignment is unavailable, because an RTF cell has none.
  • Typing. Markdown that a person types stays text: # , > , - , three backticks, ---, ==, [ ], **bold** and the rest make no headings, quotes, lists, code, rules, highlights or tasks. A bare web address stays text too. Smart punctuation, when on, still applies.
  • Every edit. Any edit that would bring in something RTF can’t hold is refused, and the document, revision and history stay as they were.
There is no source mode: setEditingMode('source') returns false, and replaceSource throws invalid-argument.

Alignment

The format commands alignLeft, alignCenter, alignRight and alignJustify align every paragraph the selection touches, including paragraphs in list items. They are available only in RTF; a table cell has no alignment. Their shortcut names have no default keys, so bind them through keymap. commandState().caret.align reports the alignment at the selection, for a toolbar’s alignment buttons.

Paste

A paste into an RTF document keeps what RTF holds:
  • HTML keeps paragraphs and their alignment, lists, tables, line breaks and the eight marks, including from styled spans such as a word processor writes. Headings become bold paragraphs, code blocks become paragraphs in code, and quotes and other containers give up their paragraphs. Images, rules and anything that shows no text are left out. Whitespace between tags, such as indentation, makes no paragraph or list item, while an empty <p></p> is an empty paragraph; a break that ends a block is dropped, as a browser draws it. A table whose cells span rows becomes one paragraph for each row, with its cells separated by tabs, because spanned cells would shift; cells that span columns keep the table, padded with empty cells. Pasted HTML is read without running it, and a link whose address Inkkit wouldn’t open keeps its text without the link.
  • Plain text pastes a paragraph for each line. Every paragraph separator, including a final one, starts a new paragraph, so typing or pasting '\n' at the end of a paragraph leaves the caret in a new one; a line separator (U+2028) is a line break. The text takes the marks the next typed text would: those you set at the caret, or else those of the text around it.
  • Explicit Markdown in ClipboardInput.markdown pastes as its text, unparsed.
A paste into a table cell keeps to the cell, as lines of it. In a list item, which holds one paragraph, each pasted line or paragraph becomes an item beside it at its level: the first joins the item at the caret, and the text after the caret moves to the last. A pasted list nests under the item before it, a level deeper than nine joins the ninth, and a table becomes lines of tab-separated cells. A paste that the document still can’t hold is refused: paste rejects with preservation, a person’s paste reports it to events.error, and nothing changes.

Copy and print

Copying gives readable text and semantic HTML, with <u>, <sup> and <sub> for underline, superscript and subscript and text-align for alignment. clipboardSnapshot also returns markdown, the copied content written as Markdown, so text such as **literal** stays text when it is pasted into a Markdown document; alignment is left out. printableSnapshot prints the document with its marks and alignment.

Convert between Markdown and RTF

convertDocument converts the loaded Markdown document to RTF, or the loaded RTF document to Markdown, for a Save As to the other format. It changes nothing in the editor and returns the result with a list of what was lost, as stable FormatLossCode values, so your app can tell the person before saving. Load the result as a new generation.
  • Markdown to RTF turns headings into bold paragraphs and code blocks into paragraphs in code, and reports those and everything else RTF can’t hold, such as quotes, tasks, images and footnotes.
  • RTF to Markdown writes underline, superscript and subscript as <u>, <sup> and <sub>. Markdown can’t start or end a mark with a space, so spaces at a run’s edges move outside its marks; it reports mark-spaces where that changes what shows, such as an underline under a space. It also reports alignment, a paragraph of only a line break and empty paragraphs at the end, which Markdown doesn’t keep.
Inkkit doesn’t convert TXT to or from RTF. The Swift package’s RichTextDocument(plainText:) and plainText do that.

What stays as in Markdown

Find and replace, undo and redo, revisions, generation and stale-document checks, reloadDocument keeping the selection, textSnapshot and the readable-text ranges, read-only mode, composition guards and changed events work as they do in formatted Markdown. A snapshot’s dirty compares canonical documents, so undoing every edit makes it clean again.