r/swift • • 2d ago

Rendering Markdown in Quick Look with TextKit instead of WebKit

MD QuickLook is a Quick Look extension that renders Markdown in Finder without WebKit. A Rust core produces a compact layout (text plus blocks, paragraphs and styled runs at UTF-16 offsets), and the Swift side turns it into an NSAttributedString and draws it with TextKit 1:

  • quotes, alerts and code blocks are NSTextBlocks with borders and backgrounds; tables are NSTextTables
  • images, formulas, Mermaid diagrams, badges and checkboxes are attachment cells
  • long documents build the first screen (~60 ms for a 900 KB file) and append the rest in the background
  • collapsible <details> sections work by cutting the folded text out of the storage and re-inserting it on click, since TextKit 1 can't hide paragraphs
  • math is typeset with SwiftMath, which I extended with KaTeX symbols, aliases and environments

A typical README is ready in ~30 ms. Source (MIT): https://github.com/armamut/md-quicklook Longer write-up: https://medium.com/@armamut/previewing-markdown-on-macos-in-30-ms-a-rust-core-and-textkit-no-web-engine-1adfa2734f48

Curious whether anyone has tried the same with TextKit 2.

9 Upvotes

11 comments sorted by

1

u/dimKant 2d ago

The details trick, cutting the folded text out of the storage and putting it back on click, is the kind of thing nobody writes down about TextKit 1. Nice write-up. On TextKit 2: folding gets easier there, because the content manager delegate can just skip elements during enumeration and you never touch the storage. But NSTextTable and NSTextBlock have no TextKit 2 version, and NSTextView quietly falls back to TextKit 1 as soon as it sees one. So with tables and bordered quotes you'd be writing your own NSTextLayoutFragment subclasses for all of it, which is a lot of work for a preview that already opens in 30 ms.

1

u/armamut 1d ago

Exactly – NSTextTable is the reason it's TextKit 1. I didn't want to write the table and block drawing by hand, so I stayed on TextKit 1 and lived with the storage surgery for <details>. Skipping elements in the content manager delegate would be much nicer for folding, though. If TextKit 2 ever gets table support, that's the first thing I'd move.

1

u/dimKant 3h ago

Same call I'd make. A 30 ms preview that handles tables is the product, folding is a nicety, and storage surgery that works is cheaper than a custom table layout that might.

1

u/Common_Green_1666 2d ago

Do you find that rendering this way is faster than using WebKit?

My understanding is that WebKit would be faster than textkit 2.

1

u/Ciosciaa 1d ago

That thought is stunning to me. Why do you think WebKit would be faster?

I’m not judging. I have a genuinely limited understanding of the discrepancies between those two engines. I just figured there was a ton more overhead with something like WebKit.

1

u/Common_Green_1666 1d ago

WebKit is optimized to render pages as fast as possible. Apple has strong incentives to make it fast so it can maintain Safari’s market share relative to Chrome, Firefox, etc.

I think your intuition that WebKit would have more overhead is generally correct, but let’s break down what that “overhead” means here.

WebKit does a lot of things besides just rendering HTML. When you use it, you need to use all of it. There’s no easy way to only use the text rendering part. This might mean That using WebKit could lead to higher memory usage than using TextKit. Apps built using Electron are a good example of this. They tend to use lots of memory compared to what an equivalent native app would use.

Here’s a blog post on the topic and a related discussion

Native all the way, until you need text

https://news.ycombinator.com/item?id=48168058

2

u/armamut 1d ago

I measured it, since the app has both renderers – you can switch to a WebKit one in settings. For a typical README, request → ready is ~30 ms native vs ~200 ms with WKWebView; for a 900 KB file the first screen is ~60 ms vs ~300 ms.

You're right that WebKit's layout engine itself is very fast. The cost in a Quick Look extension is everything around it: the web content process has to start, the page loads, and the scripts for highlighting, math and diagrams have to run before anything is on screen. TextKit just lays out an attributed string in-process, and since the Rust side has already done the parsing and highlighting, there's very little left to do. Memory is lower too, as you said.

To be fair, this is TextKit 1, not 2 – the tables need NSTextTable, which TextKit 2 doesn't support.

1

u/Financial_app 1d ago

Have you compared the two renderers with VoiceOver? A README with a table, an image with alt text, and a collapsed <details> section would make a useful test alongside the timing numbers. I'm particularly curious whether the attachment cells expose their content, and whether expanding a section leaves VoiceOver's position somewhere predictable.

0

u/[deleted] 1d ago

1

u/Common_Green_1666 1d ago

Just curious, did you profile where the time was being spent on the WebKit path?