Thumbnail for {{ page.title }}

Highlighted code in slides

This is a dual feature! It's available in video form and in text form.


I have obsessed about this long enough, I think it’s only fair I (and you!) get some content out of it.

When I started writing this article, I was working on my P99 CONF slides. Those slides happen to include some bits of code. And because I’m a perfectionist, I would like this code to be syntax highlighted, like this:

let addr: SocketAddr = config.address.parse()?; let ln = TcpListener::bind(&addr).await?; info!("🦊 {}", config.base_url);

Not like this:

let addr: SocketAddr = config.address.parse()?; let ln = TcpListener::bind(&addr).await?; info!("🦊 {}", config.base_url);
Cool bear

A perfectionist? What, you set up highlight.js and called it a day?

Amos

Oh no. No no no.

Amos

I have my own blog engine with my own Markdown processing pipeline that calls out to my tree-sitter, using my own builds of a collection of grammars I’ve collected over the years like a poet collects rhymes, and then I generate compact HTML markup so that your browser doesn’t explode at the mere sight of one of my articles.

Cool bear

Cool bear

Well.

Cool bear

We all have hobbies.

Unfortunately, most of the time, I see either:

  • Code as text (no colors)
  • Screenshots of their code editor (big files, looks crap on high-density displays)

A few weeks ago, I started looking for better options, because surely someone else was bothered by this — there are dozens of us! Dozens!

Not invented here

And I found… some plug-ins for Google Slides (most of them only work on Google Docs, a different product) that take SEVERAL SECONDS to highlight one block of code, since the plug-in code appears to run on-demand on a shared coffee maker in Mountain View.

The menu when using the Code Syntax plug-in: it goes Extensions, Code Syntax, Colorize Selection as, and I had to zoom out to have it show rust

Oh and it looks absolutely dreadful because, to be fair, colors are hard, but to be real, most developers have absolutely no sense of style.

The default code syntax colors, with a heinous purple for keywords (also bold), a.. greenish blue for everything else? a green that I guess might look decent somewhere else for macros, I don’t know. it’s a mess. It looks like codemirror or something.

But then I noticed something.

When you copy text from a web page… and you paste it into Google Slides… it retains some of its formatting.

A screenshot of the Rust book, showing some syntax-highlighted code, then an arrow, then the slide, highlighted.

In fact, I’ve known that forever. Everyone knows that. It’s a really annoying feature: you have to do “clear formatting” after pasting, or you have to remember whichever keyboard combination pastes without formatting.

Cool bear Cool Bear's hot tip

On macOS, this operation is named “Paste and match style” and it has the heinous shortcut “Option-Shift-Command-V”.

A macOS menu showing the “Paste and Match Style” option

Bright side? Free finger yoga!

We have to go out of our way to make sure that pasting doesn’t retain style.

The problem is… it retains a bit too much. It retains font-size for example, and it retains background color, but only sometimes? Which is something you almost definitely never want.

But if you were to feed it carefully-formatted HTML… HTML generated only for the purpose of being pasted into Google Slides, then maybe… it could work?

That idea is hardly new: you’ll find slides code highlighters around the web.

One of the nicer code highlighters available online. It has a code editor on the left, and the colorized result is on the right. There’s a selection of themes and fonts to choose from. It looks rather nice, and even gives you instructions on which browsers to use (Safari for Keynote, Chrome for Google Slides), and which background color to use.

But I like the code highlighting on my website. I chose my colors (and fonts!) with love.

I revel in the knowledge that, even though it’s only doing “syntactic” highlighting and not “semantic” highligting (like something like rust-analyzer can), there’s still a full-on parser running behind the scenes, not just a set of regular expressions.

And most importantly: I now tend to work on both articles and videos at the same time: first writing the article, and then making slides I can work into the video at editing time.

This very article, opened as markdown sources in Zed, the code editor I’ve been using for a few months.

So, all the code I want on the slides is already on my website, taunting me, just BEGGING to be copied into my clipboard so that I can paste them into slides.

Adding a button

So I added a button.

You can’t see it, but I can — I’m only showing it to logged-in admins.

The copy button, as shown to only myself, in the top-right corner of any code block.

Not just in text form, like a GitHub README would do, but also as HTML markup.

That’s harder than it looks though. Because when you, the user, the human interacting with a browser, when you select text, and hit Ctrl+C or Cmd+C, you’re doing something that JavaScript code can’t really do.

Well, it can, but it has very little control over it.

Technically, when you press that key combination, a copy event is dispatched.

You can add a handler for that event, which lets you override what will go in the clipboard.

Cool bear Cool Bear's hot tip

That’s how some annoying websites have your clipboard filled with, “Oh, you can’t copy from this website. You need to pay us.” instead of the thing you copied.

You can also call preventDefault() to, well, prevent anything from being copied in the system clipboard. But you can’t read what’s in the clipboard, because presumably security and privacy and things like that.

You can also generate a synthetic copy event. However, it will not affect the system clipboard at all.

An excerpt from the Clipboard API standard (a W3C Editor’s Draft), which says event handlers may write to the clipboard if the scripting thread is allowed to show a popup (in response to a click etc.), and that it may allow trusted event types to modify the clipboard, but specifically not for synthetic cut and copy events.

You can use document.execCommand, Which actually does the same thing that hitting the key combination does. But it’s deprecated to hell, and you shouldn’t use it, even though all the browsers now support it.

And if you do use that, remember that you do get to override what gets written by intercepting the copy event. But you don’t get to read what was there in the first place.

At this point, you may as well do the right thing and use the Clipboard API.

It lets you read and write clipboard items asynchronously, and items can be made available in multiple mime types…

async function writeToClipboard() { const text = "Hello, world!"; const html = "<h1>Hello, world!</h1>"; await navigator.clipboard.write([ new ClipboardItem({ 'text/plain': new Blob([text], { type: 'text/plain' }), 'text/html': new Blob([html], { type: 'text/html' }) }) ]); console.log("Content copied to clipboard as both text and HTML."); }

…which matches the way clipboards work in real-life!

On macOS, Sindre Sorhus’s wonderful Pasteboard Viewer shows us what ends up in the “General pasteboard” when we run this code from Safari:

The plain text version of what’s in the pasteboard: just Hello, world!

The HTML version of what’s in the pasteboard: notably, the h1 tag has inline styles with a caret-color of rgb(0, 0, 0), same for color, the font-style is set to normal, font-variant-caps is set to normal, and other defaults. Finally it says Hello, world inside the h1 tag.

If we execute it from Firefox, the HTML version is a bit different:

Firefox’s take on what should be in HTML format, as seen by Pasteboard Viewer — this time it has the whole boilerplate of an HTML document: an html tag, a head tag, even a meta tag that sets the content-type to text/html with a charset of utf-8, then a body with the h1 tag in there. No inline styles this time.

Each browser will add its own little wrapper around our HTML payload, which is mildly upsetting, but not a huge deal in the grand scheme of things.

Chromium’s take on it, with a meta charset utf-8 outside the HTML tag, and then an empty head, and a body with just the h1.

However, we have a much bigger problem.

Computed styles

Cool bear

To be honest, the heading kind of gives it away….

Amos

Well Bear, people want TOCs, and TOCs they will have.

So when you press Ctrl+C or Cmd+C or do execCommand("copy"), whatever is currently selected on the page is copied to the clipboard as both plain text and as HTML.

Cool bear

And RTF! And others.

Safari writes RTF to the clipboard, isn’t that fun

But… it’s making interesting choices. Look at this payload for example (formatted for your viewing comfort):

<head> <meta charset="UTF-8" /> </head> <div class="bottom-nav-previous" style='margin: 0px; padding: 0px; border: 0px; box-sizing: border-box; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 18.9px; font-style: normal; font-variant-caps: normal; font-weight: 300; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration: none;' > Looking for<span class="Apple-converted-space"> </span ><a href="http://ftl.localhost:1111/" style="margin: 0px; padding: 0px; border: 0px; box-sizing: border-box; color: light-dark(rgb(232, 12, 12), rgb(255, 116, 116)); text-decoration: none;" >the homepage</a >? </div> <br class="Apple-interchange-newline" />
Amos

ftl.localhost:1111 is the domain I use when developing my website / writing articles, just pretend it says fasterthanli.me

First off, Apple-converted-space, ouch. But also, if we pretty-print the style of, say, the link:

margin: 0px; padding: 0px; border: 0px; box-sizing: border-box; color: light-dark(rgb(232, 12, 12), rgb(255, 116, 116)); text-decoration: none;

We notice a few things: first, it bakes in a “reset” inline whenever it can (specifically around dimensions and the box model).

But second, it does some style computation (it doesn’t rely on the fact that there is, somewhere, a CSS rule that applies to <a> tags, it actually copied the color directive inline)

But it doesn’t go far enough! It’s giving us both the dark color of the link and the light color of the link:

Light mode
Dark mode

…which the browser then chooses depending on which color scheme is active now, thanks to the light-dark CSS function.

Cool bear Cool Bear's hot tip

Most browsers have a way to override light/dark mode in their dev tools:

Safari:

A screenshot of the Safari Inspector, showing Appearance, Colour scheme, with System (Dark) — > it’s a dropdown that lets you override Light or Dark.

Firefox:

A screenshot of Firefox DevTools, showing two buttons that let you “toggle light color scheme > simulation for the page” (and similarly for dark)

Chromium:

Chrome DevTools with a dropdown showing “prefers-color-scheme” light and dark, also “Automatic > dark mode”

Really, it should be picking which color is active right now — I want it to look in the slides, how it looks to me right now.

So, we cannot rely on the HTML generated by our user agent (in this instance, that means “browser”) when a “copy” event is fired and we have part of the page selected.

Instead, we could find the relevant node and grab its outerHTML property.

If I select that same paragraph and run this code from dev tools:

$0.outerHTML

It evaluates to:

<div class="bottom-nav-previous"> Looking for <a href="/">the homepage</a>? </div>

Which… doesn’t have any inline styles at all.

Note that the innerHTML property, perhaps more well-known, evaluates to this:

Looking for <a href="/">the homepage</a>?

And the contextText property simply evaluates to this:

Looking for the homepage?

We don’t want class names, but we do want inline styles, at least some of them: not background colors, but text color, and perhaps font weight.

Luckily, browsers (except Firefox for some reason) provide a way to inspect the computed style of an element.

You know, the styles you see in the “Computed” tab of the dev tools, the ones that are active right now.

The Computed tab in Safari’s Web Inspector

Again, after right-clicking that paragraph at the bottom of my blog and choosing “Inspect”, the following:

$0.computedStyleMap().get("color").toString()

Evaluates to rgb(255, 255, 255).

And if we narrow it down to the link inside:

$0.querySelector("a").computedStyleMap().get("color").toString()

We get rgb(255, 116, 116) in dark mode, and rgb(232, 12, 12) in light mode. Victory!

Code listing

With a bit more elbow grease vanilla.js application, we can come up with the complete code to copy an element to the clipboard as HTML, keeping the color and font-weight of all its children:

let copyToClipboardInner = async (source) => { // true = do a deep clone // cf. https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode let target = source.cloneNode(true); let process = ({ source, target }) => { target.removeAttribute('class'); target.removeAttribute('style'); // I used `<i>` to save bytes on markup, but they will have default // styles wherever we paste them, so let's turn them into spans for this. if (target.tagName.toLowerCase() === 'i') { let span = document.createElement('span'); span.innerHTML = target.innerHTML; target.parentNode.replaceChild(span, target); target = span; } // properties are camelCase here, even though in CSS they're kebab-case let computedStyle = source.computedStyleMap(); target.style.color = normalizeColor(computedStyle.get('color').toString()); target.style.fontWeight = computedStyle.get('font-weight').toString(); for (let i = 0; i < source.children.length; i++) { process({ source: source.children[i], target: target.children[i] }); } } process({source, target}); let wrapper = document.createElement("div"); let params = new URLSearchParams(window.location.search); wrapper.style.fontSize = params.get('fontsize') || "16pt"; wrapper.style.fontFamily = params.get('fontfamily') || "Source Code Pro"; wrapper.appendChild(target); const clipboardItem = new ClipboardItem({ 'text/html': new Blob([(wrapper.outerHTML)], { type: 'text/html' }), 'text/plain': new Blob([(wrapper.innerText)], { type: 'text/plain' }) }); // note: even though `write` is async, this must all happen in response // to a user action. we couldn't first make a network request, for example. await navigator.clipboard.write([clipboardItem]); };

Disclaimer:

This code was prototyped with Claude 3.5 Sonnet, which, if you haven’t tried, you’re simply not aware what code assistants can do.

I have rewritten the code by hand for publication.

Now if you actually went and read the code above, you might be wondering what that normalizeColor function does.

Well, that brings us to today.

Color is hard

I added that copy button weeks ago, when working on another video (which I will finish one day, I swear).

But today, as I used that button to make my P99 CONF slides, it stopped working. Clicking it copied something to the clipboard, of course, but… pasting it into Google Slides showed all-black code.

(On a black background. I thought it was completely broken at first).

Upon investigation, it turns out that:

  • Nothing changed in Google Slides
  • Nothing changed in Safari

But something must have changed…

Well, I did redesign my website.

Let’s take a look at the new CSS:

/* from `bundle.scss` */ .code-block .code-block-inner i.hh1, .code-block .code-block-inner i.hh23 { color: light-dark( color(display-p3 0.5764705882 0.3725490196 0.2235294118), color(display-p3 0.7843137255 0.537254902 0.6078431373) ); }
Cool bear Cool Bear's hot tip

What’s with this weird font-weight of 120? “Normal” weight is traditionally 400, and “Bold” is usually 700.

Well, when using variable-width fonts, tradition doesn’t apply: you get the full range from 0 to 1000, and apparently the folks at Berkeley Graphics chose to make “Normal” 100 and “Bold” 150 when they made Berkeley Mono.

The colors are in Display P3, a variant of DCI-P3 that uses a D65 white point and the sRGB tone reproduction curve.

What does it mean? It means if you have an HDR display, then one one of these rows will look more intense than the other.

If you don’t have an HDR display, or a compatible browser, then they should look the same, as the Display P3 colors should be tone-mapped to the nearest sRGB color.

To simulate that effect, we could imagine a gamut even narrower than sRGB, and use it for the first line:

It’s not the same difference, but it’s the same kind of difference.

Cool bear Cool Bear's hot tip

When picking “wide gamut” colors like Display P3, Safari’s color picker shows you where sRGB stops:

Safari web inspector showing where sRGB stops in the color picker (and where Display P3 goes!)

You can read more about wide gamut color on css-tricks, a site which has recently gained a new lease on life.

So that’s why my “copy code as HTML” button broke!

As you can imagine, Google Slides does not actually inject the CSS you give it straight into the page: they parse it, extract whichever styles they support, and use that.

And it would appear they don’t support Display P3 colors! Hence, everything came out black.

Hence, we need to do a transformation ourselves.

Which is… easier said than done:

The process of transforming a color outside of a given gamut to a color that is as close as possible but is inside gamut is called gamut mapping and is the subject of entire books

Colors.js docs — Gamut mapping

See?

Sometimes there’s good reason to pull in third-party dependencies.

let normalizeColor = (propValue) => { // Convert all colors (including display-p3) to // sRGB using color.js with proper gamut mapping return new Color(propValue) .to("srgb") .toGamut({space: "srgb", method: "css"}) .toString(); }

Closing thoughts

Now I have pretty slides:

An example slide of my P99 conf talk.

(Also, you can go watch my talk if you want)

But between you and me? Google Slides is a pretty bad product.

In 2024, you still cannot insert an SVG in there. I don’t want to hear about your EPS-based workarounds because I love myself.

Also, did they really need to put in a keyboard shortcut for “rotate element”?

To whoever needs to hear it: Keynote does support display-p3 color and SVG. I’ve switched to it just for that!

This is (was? you're done reading I guess) a dual feature! It's also available in video form.


Comment on /r/fasterthanlime

(JavaScript is required to see this. Or maybe my stuff broke)

Here's another article just for you:

Rust generics vs Java generics

In my previous article, I said I needed to stop thinking of Rust generics as Java generics, because in Rust, generic types are erased.

Someone gently pointed out that they are also erased in Java, the difference was elsewhere. And so, let’s learn the difference together.

Java generics

I learned Java first (a long, long time ago), and their approach to generics made sense to me at the time.