<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.3">Jekyll</generator><link href="https://www.azabani.com/feed/tag/igalia.xml" rel="self" type="application/atom+xml" /><link href="https://www.azabani.com/" rel="alternate" type="text/html" /><updated>2025-12-24T03:25:33+00:00</updated><id>https://www.azabani.com/feed/tag/igalia.xml</id><title type="html">/diːˈlaːn(.)ˌʕaːzəbaːniː/</title><entry><title type="html">Web engine CI on a shoestring budget</title><link href="https://www.azabani.com/2025/12/18/shoestring-web-engine-ci.html" rel="alternate" type="text/html" title="Web engine CI on a shoestring budget" /><published>2025-12-18T10:00:00+00:00</published><updated>2025-12-18T10:00:00+00:00</updated><id>https://www.azabani.com/2025/12/18/shoestring-web-engine-ci</id><content type="html" xml:base="https://www.azabani.com/2025/12/18/shoestring-web-engine-ci.html">&lt;p&gt;Servo is a greenfield web browser engine that supports many platforms. Automated testing for the project requires building Servo for all of those platforms, plus several additional configurations, and running nearly two million tests including the entire Web Platform Tests. How do we do all of that in under half an hour, &lt;em&gt;without&lt;/em&gt; a hyperscaler budget for compute and an entire team to keep it all running smoothly, and securely enough to run untrusted code from contributors?

&lt;p&gt;We’ve answered these questions by building a CI runner orchestration system for GitHub Actions that we can run on our own servers, using ephemeral virtual machines for security and reproducibility. We also discuss how we implemented graceful fallback from self-hosted runners to GitHub-hosted runners, the lessons we learned in automating image rebuilds, and how we could port the system to other CI platforms like Forgejo Actions.

&lt;p&gt;This is a transcript post for a talk I gave internally at Igalia.

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0033.jpg&quot; alt=&quot;Web engine CI on a shoestring budget
delan azabani (she/her)
azabani.com
November 2025&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;Let&apos;s talk about how Servo can have fast CI for not a lot of money.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0034.jpg&quot; alt=&quot;Servo&apos;s situation
- Servo currently uses GitHub Actions (GHA) quite heavily
    - Many platforms: Linux, Windows, macOS, Android, and OpenHarmony
    - Many tests: Web Platform Tests (50K+ tests, 1.8M+ subtests), WebGPU CTS, devtools, unit tests…
    - Many configurations: MSRV, libservo, linting, release, benchmarking…
- GHA is a frustrating CI service with baffling limitations&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;Servo is a greenfield web browser engine. And being a web browser engine, it has some pretty demanding requirements for its CI setup.

&lt;p&gt;We have to build and run Servo for many platforms, including three desktop platforms and two mobile platforms.

&lt;p&gt;We have to run many, many tests, the main bulk of which is the entire Web Platform Tests suite, which is almost 2 million subtests. We also have several smaller test suites as well, like the WebGPU tests and the DevTools tests and so on.

&lt;p&gt;We have to build Servo in many different configurations for special needs. So we might build Servo with the oldest version of Rust that we still support, just to make sure that still works. We might build Servo as a library in the same way that it would be consumed by embedders. We have to lint the codebase. We have to build with optimizations for nightly and monthly releases. We have to build with other optimizations for benchmarking work, and so on.

&lt;p&gt;And as you&apos;ll see throughout this talk, we do this on GitHub and GitHub Actions, but GitHub Actions is a &lt;em&gt;very&lt;/em&gt; frustrating CI service, and it has many baffling limitations. And as time goes on, I think Servo being on GitHub and GitHub Actions will be more for the network effects we had early on than for any particular merits of these platforms.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0035.jpg&quot; alt=&quot;GitHub-hosted runners
- GitHub provides their own runners
- Essential for glue logic and small workloads
- Painful for building and testing a browser engine
    - The gratis runners are tiny and slow
    - The paid runners are very expensive
    - Need more tools or deps? Install them every time
    - Caching is a joke, not suitable for incremental builds&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;On GitHub Actions, GitHub provides their own first-party runners.

&lt;p&gt;And these runners are very useful for small workloads, as well as the logic that coordinates workloads. So this would include things like taking a tryjob request for &quot;linux&quot;, and turning that into a run that just builds Servo for Linux. Or you might get a try job request for &quot;full&quot;, and we&apos;ll turn that into a run that builds Servo for all of the platforms and runs all of the tests.

&lt;p&gt;But for a project of our scale, these runners really fall apart once you get into any workload beyond that.

&lt;p&gt;They have runners that are free of charge. They are very tiny, resource constrained, and it seems GitHub tries to cram as many of these runners onto each server as they possibly can.

&lt;p&gt;They have runners that you can pay for, but they&apos;re very very expensive. And I believe this is because you not only pay a premium for like hyperscaler cloud compute rates, but you also pay a premium on top of that for the convenience of having these runners where you can essentially just flip a switch and get faster builds. So they really make you pay for this.

&lt;p&gt;Not only that, but using GitHub hosted runners you can&apos;t really customize the image that runs on the runners besides being able to pull in containers, which is also kind of slow and not useful all the time. If there are tools and dependencies that you need that aren&apos;t in those images, you need to install them every single time you run a job or a workflow, which is such a huge waste of time, energy, and money, no matter whose money it is.

&lt;p&gt;There are also some caching features on GitHub Actions, but they&apos;re honestly kind of a joke. The caching performs really poorly, and there&apos;s not a lot you can cache with them. So in general, they&apos;re not really suitable for doing things like incremental builds.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0036.jpg&quot; alt=&quot;Alternatives considered
- SaaS providers: expensive, often no Win and/or macOS
- RunsOn: less expensive, AWS-only, no macOS support
- &amp;quot;Proxy&amp;quot; jobs: would compete for concurrent job limit
    - Tricky to do without losing access to GHA ecosystem
- Self-host a whole CI service
    - Built-in container orchestration, but virtual machines?
    - More ops burden: CI service now on the critical path&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;So we have all these slow builds, and they&apos;re getting slower, and we want to make them faster. So we considered several alternatives.

&lt;p&gt;The first that comes to mind are these third-party runner providers. These are things like Namespace, Warpbuild, Buildjet. There&apos;s so many of them. The caveat with these is that they&apos;re almost always... almost as expensive per hour as GitHub&apos;s first-party runners. And I think this is because they try to focus on providing features like better caching, that allow you to accumulate less hours on their runners. And in doing so, they don&apos;t really have any incentive to also compete on the hourly rate.

&lt;p&gt;There is one exception: there&apos;s a system called RunsOn. It&apos;s sort of an off-the-shelf thing that you can grab this software and operate it yourself, but you do have to use AWS. So it&apos;s very tightly coupled to AWS. And both of these alternatives, they often lack support for certain platforms on their runners. Many of them are missing Windows or missing macOS or missing both. And RunsOn is missing macOS support, and probably won&apos;t get macOS support for the foreseeable future.

&lt;p&gt;We considered offloading some of the work that these free GitHub hosted runners do onto our own servers with, let&apos;s call them like, &quot;proxy jobs&quot;. The idea would be that you&apos;d still use free GitHub hosted runners, but you&apos;d do the work remotely on another server. The trouble with this is that then you&apos;re still using these free GitHub hosted runners, which take up your quota of your maximum concurrent free runners.

&lt;p&gt;And it&apos;s also tricky to do this without losing access to the broader ecosystem of prebuilt Actions. So these Actions are like steps that you can pull in that will let you do useful things like managing artifacts and installing dependencies and so on. But it&apos;s one thing to say, okay, my workload is this shell script, and I&apos;m going to run it remotely now. That&apos;s easy enough to do, but it&apos;s a lot harder to say, okay, well, I&apos;ve got this workflow that has a bunch of scripts and a bunch of external Actions made by other people, and I&apos;m going to run all of this remotely. I&apos;m going to make sure that all of these Actions are also compatible with being run remotely. That&apos;s a lot trickier. That said, you should probably avoid relying too heavily on this ecosystem anyway. It&apos;s a great way to get locked into the platform, and working with YAML honestly really sucks.

&lt;p&gt;So we could set up an entire CI service. There are some CI services like Jenkins, Bamboo... honestly, most CI services nowadays have support for built-in container orchestration. So they can spin up containers for each runner, for each job, autonomously. And while they can do containers, none of them have really solved the problem of virtual machine orchestration out of the box. And this is a problem for us, because we want to use virtual machines for that security and peace of mind, which I&apos;ll explain in more detail a little bit later. And seeing as we lack the dedicated personnel to operate an entire CI service and have that be on the critical path — we didn&apos;t want to have someone on call for outages — this was probably not the best option for us at the moment.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0037.jpg&quot; alt=&quot;Self-hosted runners
- Self-hosted runners are better!
- Give the runners as much RAM and CPU as we want
- Custom build environments tailored to the project
    - Bake in whatever tools we want
    - Bake in a prebuilt Servo for incremental builds&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;What we decided to do was set up some self-hosted runners.

&lt;p&gt;These solve most of our problems, primarily because we can throw as much hardware and resources at these runners as we want. We can define the contention ratios.

&lt;p&gt;And better yet, we can also customize the images that the runners use. By being able to customize the runner images, we can move a lot of steps and a lot of work that used to be done on every single workflow run, and do it only once, or once per build. We only rebuild the runner images, say, once a week. So that significantly saves... it cuts down on the amount of work that we have to do.

&lt;p&gt;This is not just installing tools and dependencies, but it&apos;s also enabling incremental builds quite powerfully, which we&apos;ll see a little bit later on.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0038.jpg&quot; alt=&quot;How much faster?
- mach try full workflow: 61m30s → 25m47s (−58%)
- linux-unit-tests job: 34m29s → 3m15s (−90%)
- windows-unit-tests job: 59m14s → 8m4s (−86%)
- lint job: 11m54s → 2m25s (−79%)
- wpt jobs: 25m35s → 20m50s (−18%)
    - But we also went from 20 runners → 3 runners&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;How much faster is this system with the self-hosted runners? Well, it turns out, quite a lot faster.

&lt;p&gt;We&apos;ve got this typical workflow that you use when you&apos;re testing your commits, when you&apos;re making a pull request. And if you kick off a tryjob like this, several of the jobs that we&apos;ve now offloaded onto self-hosted runners are now taking 70, 80, even 90% less time than they did on GitHub hosted runners, which is excellent.

&lt;p&gt;And even for the web platform tests, we found more modest time savings with the capacity that we&apos;ve allocated to them. But as a result, even though the savings are a bit more modest, what&apos;s worth highlighting here is that we were able to go from twenty runners — we had to parallelise this test suite across twenty runners before — and now we only need three.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0039.jpg&quot; alt=&quot;What makes our system unique
- Augments the built-in CI service of the forge
- Almost transparent user experience
- Linux, Windows, and macOS runners
- Graceful fallback to GitHub-hosted runners
- Secure enough for a large public project
- Completely self-hosted, so it&apos;s dirt cheap^

^ operating expenses, not necessarily labour&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;Some things that make our system unique and that we&apos;re pretty proud of — and these things apply to all versions of our system, which we&apos;ve been developing over the last 12 to 18 months.

&lt;p&gt;The first is that we build on top of the native CI service of the Git forge. So in this case, it&apos;s GitHub and GitHub Actions. It could be Forgejo and Forgejo Actions in the future, and we&apos;re working on that already.

&lt;p&gt;We also want to give users more or less a transparent user experience, the idea being that users should not notice any changes in their day-to-day work, besides the fact that their builds have gotten faster. And I think for the most part, we have achieved that.

&lt;p&gt;Our system supports all of the platforms that GitHub Actions supports for their runners, including Linux, Windows, and macOS, and we could even support some of the other platforms that Forgejo Actions supports in the future, including BSD.

&lt;p&gt;We have the ability to set up a job so that it can try to use self-hosted runners if they&apos;re available, but fall back to GitHub hosted runners if there&apos;s none available for whatever reason, like maybe they&apos;re all busy for the foreseeable future, or the servers are down or something like that. We have the ability to fall back to GitHub hosted runners, and this is something that was quite complicated to build, actually. And we have a whole section of this talk explaining how that works, because this is something that&apos;s not actually possible with the feature set that GitHub provides in their CI system.

&lt;p&gt;It&apos;s secure enough for a large public project like Servo, where we don&apos;t necessarily know all of our contributors all that well personally. And this is in large part because we use virtual machines, instead of just containers, for each run and for each job. My understanding is that it is possible to build a system like this securely with containers, but in practice it&apos;s a lot harder to get that right as a security boundary than if you had the benefit of a hypervisor.

&lt;p&gt;And of course it is all completely self-hosted, which makes it about as cheap as it gets, because your operating expenses are really just the costs of bare metal compute.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0040.jpg&quot; alt=&quot;Completely self-hosted
so it&apos;s dirt cheap^
- We spend 312 EUR/month on general-purpose runners
- On comparable GitHub runners: 1421–2500 EUR/month
- On comparable third-party runners: 503–1077 EUR/month

^ operating expenses, not necessarily labour&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;Now, how cheap is that? Well, in our deployment in Servo, we spend about 300 EUR per month on servers that do general-purpose runners, and these handle most of our workload.

&lt;p&gt;If we were to compare that to if we were running on GitHub-hosted runners, there&apos;d be almost an order of magnitude increase in costs, somewhere like 1400 EUR to over 2000 EUR per month if we were doing the same work on GitHub-hosted runners.

&lt;p&gt;There are also significant increases if we went with third-party runner providers as well, although not quite as much.

&lt;p&gt;But in truth, this is actually kind of an unfair comparison, because it assumes that we would need the same amount of hours between if we were running on self-hosted runners or if we were running on GitHub hosted runners. And something that you&apos;ll see throughout this talk is that this is very much not the case. We spend so many fewer hours running our jobs, because we have to do so much less work on them. So in reality, the gap between our expenses with these two approaches would actually be a lot wider.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0041.jpg&quot; alt=&quot;Three ways to use runners
- Mandatory self-hosted: (with natural queueing)
    - runs-on: self-hosted-image:servo-ubuntu2204
- Graceful fallback to GitHub-hosted:
    - Decision job: POST https://monitor/select-runner
    - runs-on: ${{ needs.decision.outputs.label }}
- Graceful fallback plus queueing:
    - Decision job: POST https://queue/select-runner
    - runs-on: ${{ needs.decision.outputs.label }}&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;There are three ways to use our CI runner system.

&lt;p&gt;The first one is the way that GitHub designed self-hosted runners to be used. The way they intend you to use self-hosted runners is that when you define a job, you use this &lt;code&gt;runs-on&lt;/code&gt; setting to declare what kind of runners your job should run on. And you can use labels for GitHub hosted runners, or you can define arbitrary labels for your own runners and select those instead. But you have to choose one or the other in advance. And if you do that, you can&apos;t have any kind of fallback, which was a bit of a drawback for us, especially early on. One benefit of this, though, is that you get the natural ability to queue up jobs. Because if, at the time of queuing up a new job, if there&apos;s no self-hosted runners available yet, the job will stay in a queued state until a self-hosted runner becomes available. And it&apos;s nice that you essentially get that for free. &lt;em&gt;But&lt;/em&gt; you have no ability to have this kind of graceful fallback.

&lt;p&gt;So we built some features to allow you to do graceful fallback. And how this works is that each of the servers that operates these runners has a web API as well. And you can hit that web API to check if there are runners available and reserve them. Reserving them is something you have to do if you&apos;re doing graceful fallback. But I&apos;ll explain that in more detail a bit later on. And in doing so, because you have a job that can check if there are runners available, you can now parameterize the &lt;code&gt;runs-on&lt;/code&gt; setting and decide &quot;I want to use a self-hosted runner, or a GitHub hosted runner&quot;. It&apos;s unclear if this is going to be possible on Forgejo Actions yet, so we may have to add that feature, but it&apos;s certainly possible on GitHub Actions.

&lt;p&gt;Now, the downside of this is that you do lose the ability, that natural ability, to queue up jobs, and I&apos;ll explain why that is a bit later. But in short, we have a queue API that mitigates this problem, because you can hit the queue API, and it can have a full view of the runner capacity, and either tell you to wait, or forward your request once capacity becomes available.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0042.jpg&quot; alt=&quot;Faster checkouts
- No repo checkout unless you run actions/checkout
    - But servo/servo has 130K+ files and 6K+ directories
    - This is especially slow on Windows and macOS
- Bake a repo checkout into every runner image
- Replace actions/checkout with our own action:
    git fetch --depth=1 origin $commit
    git switch --detach
    git reset --hard FETCH_HEAD&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;Some things that you can do with our system, that you can&apos;t do with GitHub hosted runners. One of them is check out the repo significantly faster.

&lt;p&gt;Something about GitHub Actions and how it works is that, if you run a job, you run a workflow in a repo, you don&apos;t actually get a checkout of the repo. You don&apos;t get a clone of the repo out of the box, unless you explicitly add a step that does a checkout. And this is fine for the most part, it works well enough for most users and most repos.

&lt;p&gt;But the Servo repo has over 130,000 files across 6,000 directories, and that&apos;s just the &lt;em&gt;tracked&lt;/em&gt; files and directories. And as a result, even if we use things like shallow clones, there&apos;s just kind of no getting around the fact that cloning this repo and checking it out is just &lt;em&gt;slow&lt;/em&gt;. It&apos;s unavoidably slow.

&lt;p&gt;And it&apos;s &lt;em&gt;especially&lt;/em&gt; slow on Windows and macOS, where the filesystem performance is honestly often pretty poor compared to Linux. So we want to make our checkouts faster.

&lt;p&gt;Well, what we can do is, we can actually move the process of cloning and checking out the repo from the build process, and move that into the image build process. So we only do it once, when we&apos;re building the runner images.

&lt;p&gt;Then what we can do is go into the jobs that run on self-hosted runners, and switch out the stock checkout action with our own action. And our own action will just use the existing clone of the repo, it will fetch the commit that we actually need to build on, then switch to it, and check it out.

&lt;p&gt;And as a result, we can check out the Servo repo pretty reliably in a couple of seconds, instead of having to check out the entire repo from scratch.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0043.jpg&quot; alt=&quot;Incremental builds
- Cargo supports incremental builds
- We&apos;re now baking a repo checkout into every image
- Why not build Servo and bake that in too?
- Not perfect — some compilation units get false rebuilds
- Probably don&apos;t use this for release artifacts&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;Something that flows on from that, though, is that if we are baking a copy of the Servo repo into our runner images, well, what if we just bake a copy of the built artifacts as well? Like, why don&apos;t we just build Servo, and bake that into the image?

&lt;p&gt;And this will allow us to do incremental builds, because Servo is a Rust project, and we use Cargo, and Cargo supports incremental builds. As a result, by doing this, when you run a job on our CI system, most of the time we only have to rebuild a handful of crates that have changed, and not have to rebuild all of Servo from scratch.

&lt;p&gt;Now, this is not perfect. Sometimes we&apos;ll have some compilation units that get falsely rebuilt, but this works well enough, for the most part, that it&apos;s actually a significant time savings.

&lt;p&gt;I also probably wouldn&apos;t trust this for building artifacts that you actually want to release in a finished product, just because of the kinds of bugs that we&apos;ve seen in Cargo&apos;s incremental build support.

&lt;p&gt;But for everything else, just your typical builds where you do like commit checks and such, this is very very helpful.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0044.jpg&quot; alt=&quot;Servo&apos;s deployment
- Five servers on Hetzner^
    - 3x AX102 (Zen 4 16c/32t, 128G RAM) = 312 EUR/month
    - 2x AX42 (Zen 3 8c/16t, 64G RAM) = 92 EUR/month
- NixOS + libvirt + KVM + ZFS
- Custom orchestration

^ not including OpenHarmony runners&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;Servo has this system deployed, and it&apos;s had it deployed for at least the last year or so.

&lt;p&gt;Nowadays we have three servers which are modestly large, and we use these for the vast majority of our workload. We also have two smaller servers that we use for specific benchmarking tasks.

&lt;p&gt;The stack on these servers, if you could call it that, is largely things that I personally was very familiar with, because I built this. So we&apos;ve got NixOS for config management, the hypervisor is libvirt and Linux KVM, and the storage is backed by ZFS. The process of actually building the images, and managing the lifecycle of the virtual machines though, is done with some custom orchestration tooling that we&apos;ve written.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0045.jpg&quot; alt=&quot;How does it work?
- Monitor service orchestrates the runners
    - Rebuilds virtual machine images
    - Spawns virtual machines for runners
    - Registers runners with CI service API
    - Labels runners when asked to reserve them
      (optional, but required for graceful fallback)
- Queue service allows queueing with fallback (optional)&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;This tooling consists of two services. The monitor service, which runs on every server that operates self-hosted runners, and the queue service, which is single and global.

&lt;p&gt;So the monitor service does the vast majority of the work. It rebuilds virtual machine images, these templates. It clones the templates into virtual machines for each runner, for each job. It registers the runners with the CI service using its API, so that it can receive jobs. And it can also label the runners to tie them to specific jobs when asked to reserve them. This is optional, but it is a required step if we&apos;re doing graceful fallback.

&lt;p&gt;Now, with graceful fallback, you do lose the ability to naturally queue up jobs. So what we&apos;ve put on top of that is a single queue service that sits in front of the cluster, and it essentially acts as a reverse proxy. It&apos;s quite thin and simple, and there&apos;s a single one of them, not one per server, for the same reason as the general principle of, like, when you go to a supermarket, it&apos;s more efficient to have a single large queue, a single long queue of customers that gets dispatched to a bunch of shopkeepers. That&apos;s more efficient than having a sort of per-shopkeeper queue, especially when it comes to, like, moving jobs dynamically in response to the availability.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0046.jpg&quot; alt=&quot;Graceful fallback&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;So we&apos;ve got a whole section here about how graceful fallback works. I might cut this from shorter versions of the talk, but let&apos;s jump into it.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0047.jpg&quot; alt=&quot;Graceful fallback
- Every job has to choose a runner label in advance
    runs-on: ubuntu-latest     # GitHub-hosted
    runs-on: self-hosted-image:servo-ubuntu2204
- Once you choose the runner label, there&apos;s no turning back
- Borrowing a built-in label does not prioritise self-hosted runners over GitHub-hosted runners
- So there&apos;s no way to fall back… or is there?&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;On GitHub Actions, every job has this &lt;code&gt;runs-on&lt;/code&gt; setting, that defines what kind of runner it needs to get assigned to. It has to define this in advance, before the job runs.

&lt;p&gt;And annoyingly, once you choose, &quot;I want to run on a GitHub hosted runner&quot; or &quot;I want to run on a self-hosted runner&quot;, once you decide that, there&apos;s no turning back. Now, if you&apos;ve decided that my job needs to run on a self-hosted runner, it can &lt;em&gt;only&lt;/em&gt; run on a self-hosted runner. And now the outcome of your job, and the outcome of your workflow, now depends on that job actually &lt;em&gt;getting&lt;/em&gt; a self-hosted runner with the matching criteria and succeeding. There&apos;s no way for that to become optional anymore.

&lt;p&gt;And you might say, okay, well, maybe I can work around this by spinning up my self-hosted runners, but just giving them the same labels that GitHub assigns to their runners, maybe it&apos;ll do something smart? Like maybe... it will run my self-hosted runners if they&apos;re available and fall back. But no, the system has no sense of priority and you can&apos;t even define any sense of priority of like, I want to try this kind of runner and fall back to another kind of runner. This is simply not possible with the GitHub Action system.

&lt;p&gt;So it may seem like it&apos;s impossible to do graceful fallback, but we found a way.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0048.jpg&quot; alt=&quot;Decision jobs
- Prepend a job that chooses a runner label
    1.  let label = runner available?
           | yes =&gt; [self-hosted label]
           | no  =&gt; [GitHub-hosted label]
    2.  $ echo &amp;quot;label=${label}&amp;quot; | tee -a $GITHUB_OUTPUT
- Use the step output in runs-on
    runs-on: ${{ needs.decision.outputs.label }}
- But two decisions must not be made concurrently&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;What we can do is add a decision job, for each workload job that may need to run on self-hosted runners. And we prepend this job, and its job is to choose a runner label.

&lt;p&gt;So how it essentially works is: it checks if a runner is available, and based on that, either chooses a self-hosted label or a GitHub hosted label. And it chooses it and sets it in an output. And this output can get pulled in... in our workload job, the job that actually does our work, because now you can parameterize the &lt;code&gt;runs-on&lt;/code&gt; setting, so that it takes the value from this previous decision job.

&lt;p&gt;Unfortunately, it seems like this may not be possible in Forgejo Actions just yet, so we might have to develop support for that, but it&apos;s certainly possible in GitHub Actions today, and it has been for quite a while.

&lt;p&gt;The one caveat with this approach is that you need to be careful to only do this decision process one at a time. You should not do two instances of this process concurrently and interleave them.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0049.jpg&quot; alt=&quot;Decisions must be serialised
- Stack Overflow and GitHub answers are inherently racy:
    any idle runners? —(TOCTOU)→ commit to self-hosted
- We can reserve a runner by applying a unique label to it!
    - GH API: add custom label: reserved-for:&lt;UUIDv4&gt;
    -   runs-on: ${{ needs.decision.outputs.label }}
        runs-on: 6826776b-5c18-4ef5-8129-4644a698ae59
- Initially do this directly in the decision job (servo#33081)&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;The reason for this is something you&apos;ll see if you think about a lot of the answers that you get on Stack Overflow and the GitHub forums. If you look up, &quot;how do I solve this problem? How do I define a job where I can fall back from self-hosted runners to GitHub hosted runners?&quot;

&lt;p&gt;And most of the answers there, they&apos;ll have a problem where they check if there are any runners that are available, &lt;em&gt;and then&lt;/em&gt;, they will make the decision, committing to either a self-hosted runner or a GitHub hosted runner. The trouble is that in between, if another decision job comes in and tries to make the same kind of decision, they can end up &quot;earmarking&quot; the same runner for two jobs. But each runner is only meant to run one job, and it &lt;em&gt;can&lt;/em&gt; only run one job, so one of the jobs will get left without a runner.

&lt;p&gt;So we can start to fix this by actually reserving the runners when we&apos;re doing graceful fallback. And how we&apos;ve done it so far, is that we&apos;ve used the GitHub Actions API to label the runner when we want to reserve it, and we label it with a unique ID. Then the workload job can put that unique ID, that label, in its &lt;code&gt;runs-on&lt;/code&gt; setting. Instead of a general runner label, it can tie itself to this specific, uniquely identified runner label.

&lt;p&gt;And we did it this way initially, because it allowed us to do it inside the decision job, at first. I think in the future, we will have to move away from this, because on Forgejo Actions, the way runner labels work is quite different. They&apos;re not something that you can sort of update after the fact. In fact, they&apos;re actually kind of defined by the runner process. So this approach for reserving runners won&apos;t work on Forgejo Actions. We&apos;ll probably have to do that internally on the runner servers. But in the meantime, we use labeling. Yeah, so at first we did this inside the decision job.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0050.jpg&quot; alt=&quot;Decisions must be serialised
- Labelling runners requires a privileged GitHub API token
- Even with reservation, decisions must still be serialised:
    runner not yet reserved? —(TOCTOU)→ label the runner
    - But hey, at least we have job concurrency… right?
    - Wrong: runs will fail under even modest contention :(&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;There are some problems with this. One of them is that now the decision job has to have a GitHub token that has permissions to manage runners and update their labels. And this is something that we&apos;d like to avoid if possible, because we&apos;d like our jobs to have least privilege that they need to do their work.

&lt;p&gt;And something I didn&apos;t mention is that reserving runners in this way doesn&apos;t actually solve the problem on its own, because you&apos;ve now transformed the problem to being, okay, we&apos;re going to check if the runner is not yet reserved. We&apos;re going to check if there&apos;s an unreserved runner, &lt;em&gt;and then&lt;/em&gt;, we&apos;re going to label the runner. But in between, there&apos;s a moment where another process doing the same thing could make the same decision. And as a result, if we just did this, we could end up with a situation where one runner gets assigned two unique labels, but it can only fulfill one of them. So we have that same problem again.

&lt;p&gt;So you might say, okay, well, it looks like GitHub Actions has this neat job concurrency feature. I mean, they say you can use this to define a job in a way where only one of them will run at a time, and you can&apos;t run them concurrently, so let&apos;s try using that to solve this problem.

&lt;p&gt;What you&apos;ll find, though, is that if you try to solve the problem with job concurrency, as soon as there&apos;s even the slightest bit of contention, you&apos;ll just have your runs starting to fail spuriously, and you&apos;ll have to keep rerunning your jobs, and it&apos;ll be so much more annoying.

&lt;p&gt;And the reason for this is that, if you look more closely at the docs, job concurrency essentially has a queue limited to one job. So at a maximum, you can have one job, one instance of the job that&apos;s running, one instance of the job that&apos;s queued, and then if another instance of the job comes in while you have one running and one queued, then those extra jobs just get cancelled, and the build fails. So unfortunately, job concurrency does not solve this problem.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0051.jpg&quot; alt=&quot;Decisions must be serialised
- So move decisions out of the decision jobs (servo#33315)
- But what happens if reserved runners fail to materialise?
- You can limit in_progress time in GHA, but not queued time&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;So to solve these problems, what we did is we moved that decision and runner reservation process out of the decision jobs, and into the servers that operate the runners themselves. And we do this with an API that runs on the servers.

&lt;p&gt;One smaller problem you might notice though, is that there&apos;s still a possibility that you could reserve a runner, but then after you&apos;ve reserved the runner, it might fail to actually run. And this has become a lot less likely in our system, in our experience nowadays, now that we&apos;ve ironed out most of the bugs, but it can still happen from time to time, usually due to infrastructure or network connectivity failures.

&lt;p&gt;And we wanted to solve this by setting a time limit on how long a job can be &lt;code&gt;queued&lt;/code&gt; for, because if it can&apos;t actually get a runner in practice, it will get stuck in that &lt;code&gt;queued&lt;/code&gt; state indefinitely. But unfortunately, while we can set a time limit on how long a job can &lt;em&gt;run for&lt;/em&gt; once it&apos;s been assigned, we can&apos;t actually set a time limit on how long a job can be &lt;code&gt;queued&lt;/code&gt; for.

&lt;p&gt;So we have to rely on the default built-in limit for all jobs, where I think there&apos;s like a limit of a day or two? So like, 24 or 48 hours or so, for how long a job can be &lt;code&gt;queued&lt;/code&gt;? And this is just a really long time. So as a result, whenever this happens, you essentially have to cancel the job manually, or if you don&apos;t have permission to do that, you have to go &lt;em&gt;ask&lt;/em&gt; someone who &lt;em&gt;has&lt;/em&gt; permission, to go and cancel your job for you, which is really annoying.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0052.jpg&quot; alt=&quot;Timeout jobs
- Watchdog for your workload job, ensuring it gets a runner
    1.  Wait a short amount of time (e.g. 120 seconds)
    2.  Query the CI service API for the workload job
    3.  If the job is still queued, cancel the run
- Only run this when you actually use a self-hosted runner:
    if: ${{ fromJSON(needs.decision.outputs.is-self-hosted) }}&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;So we solved that using timeout jobs. A timeout job is a sibling to your workload job, and it acts like a watchdog, and it ensures that it actually got a runner when you expect to get a self-hosted runner.

&lt;p&gt;And how that works is, we wait a short amount of time, just like a minute or two, which should be long enough for the runner to actually start running the job, and then we query the API of the CI service to check if the workload job actually started running, or if it&apos;s still in that &lt;code&gt;queued&lt;/code&gt; state. If it&apos;s still queued after two minutes, we cancel the run.

&lt;p&gt;Unfortunately, we can&apos;t just cancel the job run. We do have to cancel the whole workflow run, which is quite annoying. But, you know, it&apos;s GitHub, nothing is surprising anymore.

&lt;p&gt;Thankfully, we only have to run this when we actually get a self-hosted runner, so we can make it conditional. But over time, in Servo&apos;s deployment, we have actually stopped using these, to free up some of those GitHub-hosted runner resources.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0053.jpg&quot; alt=&quot;Uniquely identifying jobs
- How do we know the run id of the workload job?
    - Jobs can be instantiated many times via workflow calls
- The only supported job relationship is needs
    - Workload job needs decision job
    - Timeout job needs decision job
    - Timeout job can&apos;t needs workload job
- needs relationships are not exposed in the API&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;One challenge that comes in making these timeout jobs is identifying the workload jobs uniquely, so we can look up whether it&apos;s still &lt;code&gt;queued&lt;/code&gt; or whether it&apos;s started running. There are unique IDs for each job run. These are just like an incrementing number, and you&apos;d think we&apos;d be able to use this number to look up the workload job uniquely and robustly.

&lt;p&gt;Unfortunately, you can&apos;t know the run ID of the job [correction 2025-12-24] &lt;del&gt;until it starts, and it may not ever start... or at least you may not know it&lt;/del&gt; until the workflow runs, and there can be many instances of the job in the workflow because of workflow calls. Workflow calls are a feature that essentially allows you to inline the contents of a workflow in another as many times as you like. And as a result, you can have multiple copies, multiple instances of a job that run independently within one workflow run. So we definitely need a way to uniquely look up our instance of the workload job.

&lt;p&gt;The trouble is that the only job relationship you can do in GitHub Actions is a &lt;code&gt;needs&lt;/code&gt; relationship, and that&apos;s inappropriate for our situation here, because we can say that the workload job &lt;code&gt;needs&lt;/code&gt; the decision job, we can say the timeout job &lt;code&gt;needs&lt;/code&gt; the decision job — and in fact we do both of these, we &quot;need&quot; to do both of these — but we can&apos;t say that the timeout job &lt;code&gt;needs&lt;/code&gt; the workload job, because of how &lt;code&gt;needs&lt;/code&gt; works.

&lt;p&gt;How &lt;code&gt;needs&lt;/code&gt; works is that if job A &lt;code&gt;needs&lt;/code&gt; job B, then job B has to actually get assigned a runner, and run, and complete its run — it has to finish — before job A can even start. And in this situation, we&apos;re making a timeout job to catch situations where the workload job never ends up running, so if we expressed a &lt;code&gt;needs&lt;/code&gt; relationship between them, then the timeout job would never run, in these cases at least.

&lt;p&gt;And even if we could express a &lt;code&gt;needs&lt;/code&gt; relationship between jobs, like maybe we could walk the job tree, and go from the timeout job, through the decision job via the &lt;code&gt;needs&lt;/code&gt; relationship, and then walk back down to the workload job using the same kind of &lt;code&gt;needs&lt;/code&gt; relationship... unfortunately, none of these &lt;code&gt;needs&lt;/code&gt; relationships are actually exposed in the API for a running workflow. So like, they&apos;re used for scheduling, but when you actually go and query the API, you can&apos;t tell what job &lt;code&gt;needs&lt;/code&gt; what other job. They&apos;re just three jobs, and they&apos;re unrelated to one another.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0054.jpg&quot; alt=&quot;Uniquely identifying jobs
- Tie them together by putting the &lt;UUIDv4&gt; in the name:
    name: Linux [${{ needs.decision.outputs.unique-id }}]
    name: Linux [6826776b-5c18-4ef5-8129-4644a698ae59]
- Query the CI service API for all jobs in the workflow run
- Check the status of the job whose name contains
    [${{ needs.decision.outputs.unique-id }}]
- Yes, really, we have to string-match the name :)))&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;So how we had to end up solving this is, we had to tie these jobs together by generating a unique ID, a UUID, and putting it in the friendly name, like the display name of the job, like this.

&lt;p&gt;And to query the CI service to find out if that job is still queued, we need to query it for the whole workflow run, and just look at all of the jobs, and then find the job whose name contains that unique ID.

&lt;p&gt;Then we can check the &lt;code&gt;status&lt;/code&gt;, and see if it&apos;s still &lt;code&gt;queued&lt;/code&gt;. This is really, really silly. Yes, we really do have to string match the name, which is bananas! But this is GitHub Actions, so this is what we have to do.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0055.jpg&quot; alt=&quot;Tokenless API
- Monitor API requires access to secrets in the workflow
    - All pull_request_target runs have access to secrets
        - …but you generally don&apos;t want to use it anyway
    - Most pull_request runs do not have access to secrets
- How do we prove the request is genuine and authorised, if we can&apos;t authenticate with a token?&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;One thing I didn&apos;t mention is that being able to reserve runners needs to be kind of a privileged operation, because we don&apos;t just want an arbitrary client on the internet to be able to erroneously or maliciously reserve runners. Even if they may not be able to do a whole lot with those runners, they can still deny service.

&lt;p&gt;So to use the monitor API to do graceful fallback and to request and reserve a runner, normally we would require knowledge of some kind of shared secret, like an API token, and that&apos;s what we&apos;ve done for most of the life of this system.

&lt;p&gt;The trouble with this is that there are many kinds of workflow runs that don&apos;t have access to the secrets defined in the repo. A big one is &lt;code&gt;pull_request&lt;/code&gt; runs. Most of the time, &lt;code&gt;pull_request&lt;/code&gt; runs don&apos;t have access to secrets defined in the repo. And there is another kind of run called a &lt;code&gt;pull_request_target&lt;/code&gt; run, and those do have access to secrets, but they also have some pretty gnarly security implications that mean that in general, you wanna avoid using these for pull requests anyway.

&lt;p&gt;So if you&apos;re stuck with &lt;code&gt;pull_request&lt;/code&gt; runs for your pull requests, does that mean that you can&apos;t use self-hosted runners? How do we allow &lt;code&gt;pull_request&lt;/code&gt; runs to request and reserve runners in a way that it can prove that its request is genuine and authorized?
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0056.jpg&quot; alt=&quot;Tokenless API
- Upload an artifact representing the request!
- Hit the monitor API
    - /select-runner ?unique_id &amp;qualified_repo &amp;run_id
    - (the profile_key is in the artifact)
- Important: delete the artifact, so it can&apos;t be reused (and set the minimum auto-delete, in case that fails)&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;What we do is we use artifacts. We upload a small artifact that encodes the details of the request and publish the artifact against the run. So in the artifact, we&apos;d say, &quot;I want two Ubuntu runners&quot; or &quot;one Windows runner&quot; or something like that.

&lt;p&gt;And then we would hit the monitor API, we hit a different endpoint that just says, go to this repo, go to this run ID, and then check the artifacts! You&apos;ll see my request there! And this does not require an API token, it&apos;s not a privileged operation. What&apos;s privileged is publishing the artifact, and that&apos;s unforgeable; the only entity who can publish the artifact is the workflow itself.

&lt;p&gt;All we have to do then, all we have to be careful to do, is to delete the artifact after we reserve the runner, so that it can&apos;t be replayed by a malicious client. And in the event that deleting the artifact fails, we can also set the minimum auto-delete period of, I think at the moment it&apos;s 24 hours, just in case that fails.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0057.jpg&quot; alt=&quot;Global queue
- Fallback happens immediately if no runners are available
- But if GitHub-hosted runs take 5x as long as self-hosted, we can wait up to 80% of that time and still win
- Run a queue service that allows jobs to wait for capacity
- Decision jobs hit the queue API instead of the monitor API
- Queue API says &amp;quot;wait&amp;quot; with HTTP 503 + &apos;Retry-After&apos;
- Queue API then proxies the reservation to a monitor API &quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;Graceful fallback normally means that you lose the ability to naturally queue jobs for self-hosted runners. And this happens because when you hit the API, requesting, you know, are there any available runners, please reserve one for me. If there aren&apos;t any available at the time of the request, we will immediately fall back to running on a GitHub hosted runner.

&lt;p&gt;But the thing is, is that our self-hosted runners are generally so much faster! A GitHub hosted run might take five times as long as the equivalent self-hosted run, and if that was the case, it would actually be beneficial to wait up to 80% of that time, and we&apos;d still probably save time.

&lt;p&gt;So to increase the utilization of our self-hosted runners, what we can do is run a small queue service that sits in front of the monitors, and it essentially acts as a reverse proxy. It will take the same kind of requests for reserving runners as before, but it will have a global view of the availability and the runner capacity at any given moment.

&lt;p&gt;And based on that, it will either respond to the client saying, go away and please try again later, or it will forward the request onto one of the monitors based on the available capacity.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0058.jpg&quot; alt=&quot;Runner images&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;We also have some lessons here about how to automate building virtual machine images, essentially. And these are lessons that we&apos;ve learned over the last year or two.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0059.jpg&quot; alt=&quot;Runner images
- GitHub uses Packer for their stock runner images
- Our monitor service manages image rebuilds
- Initially kicked off manually, now fully automated (#6)
    - Driven by Rust with reflink copies (#32)
- Mounting images to inject data is no longer viable (#30)
    - macOS has no usable FS with Linux write support
    - Get tools and deps from the monitor&apos;s web server (#32)&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;GitHub Actions uses Packer for their first-party runner images, so they use Packer to build those images. And our monitor service also automates building the images, but we don&apos;t use Packer.

&lt;p&gt;Initially, we had a handful of scripts that we just kicked off manually whenever we needed to update our images, but we&apos;ve now fully automated the process. And we&apos;ve done this using some modules in our monitor service, so there&apos;s some high-level Rust code that drives these image rebuilds, and it even uses reflink copies to take advantage of copy-on-write time savings and space savings with ZFS.

&lt;p&gt;Now, one of the complications we ran into when building images, over the past year or so, is that we used to pull a sort of clever trick where, we would do as much of the process of configuring the virtual machine image on the host, actually, as possible, rather than doing configuration inside the guest, and having to spin up the guest so that we can configure it. And we were able to do this by essentially mounting the root file system of the guest image, on the host, and then injecting files, like injecting tools and scripts and other things that are needed by the image and needed to configure the image.

&lt;p&gt;But we stopped being able to do this eventually because, well, essentially because of macOS. macOS has no file system that&apos;s usable for building Servo that can also be mounted on Linux with write support. Because like, just think about them, right? We&apos;ve got HFS+, which Linux can write to but only if there&apos;s no journaling and you can&apos;t install macOS on HFS+ without journaling. There&apos;s APFS, which Linux has no support for. There&apos;s exFAT, which has no support for symlinks, so a lot of tools like uv will break. There&apos;s NTFS, which we thought would be our savior, but when we tried to use it, we ran into all sorts of weird build failures, which we believe are due to some kind of filesystem syncing race condition or something like that, so NTFS was unusable as well.

&lt;p&gt;In the end, what we had to do is, if a guest image needed any tools or dependencies, it had to download them from a web server on the host.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0060.jpg&quot; alt=&quot;Runner images
- Consistent approach to automating operating systems
    - OS installation: whatever is easiest
    - Config bootstrap: native config management (if any)
        - Use it as little as possible
    - Image config: native scripting
    - Runner boot: native scripting&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;We eventually settled on a consistent approach to automating the process of installing the OSes and configuring them for each runner template.

&lt;p&gt;And the approach that we used was to install the OS using whatever method is easiest, and to bootstrap the config using any native config management system, if there is one included with the operating system.

&lt;p&gt;But once we&apos;ve kicked off that process, we then use the native config management as little as possible. And we do this because a lot of the config management tools that are built into these operating systems are quite quirky and annoying, and they are built for needs that we don&apos;t have, the primary need being that they can manage the configuration of a system over time, keeping it up to date with any changes. The thing about these runner images, though, is that each runner image only needs to get built once, it only needs to get configured once, and then after after that it&apos;ll be cloned for each runner, and then it will only run once, and this kind of sort of &quot;one shot&quot; use case is something that&apos;s kind of overkill with config management systems.

&lt;p&gt;We can just do it with the usual scripting and automation facilities of the operating system. How does that look like in practice?
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0061.jpg&quot; alt=&quot;Linux runners
- OS installation: prebuilt Ubuntu cloud images
- Config bootstrap: cloud-init config
    - Use it as little as possible (systemd journal to tty7, netplan config, curl and run next stage)
- Image config: bash script
- Runner boot: same bash script&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;Well for Linux, we install the OS using pre-built Ubuntu cloud images, that we just download from the mirrors.

&lt;p&gt;We bootstrap the config using cloud-init, which is such a painful... it&apos;s so painful to use cloud-init. We use it because it&apos;s included with the operating system, so that means it&apos;s the fastest possible way to get started.

&lt;p&gt;We use it as little as possible: we just configure the logs to go to a TTY, we configure the network, so we can connect to the network, and then once we&apos;re on the network, we just curl and run a bash script which does the rest.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0062.jpg&quot; alt=&quot;Windows runners
- OS installation: autounattend.xml (generator)
- Config bootstrap: same autounattend.xml
    - Use it as little as possible
    - Create elevated scheduled task to curl and run next stage
    - Install NetKVM driver, do some reg imports, reboot
- Image config: PowerShell script
- Runner boot: same PowerShell script&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;The same goes for Windows.

&lt;p&gt;We install the operating system using an automated answers file called autounattend.xml. There&apos;s a &lt;a href=&quot;https://schneegans.de/windows/unattend-generator/&quot;&gt;nice little generator here&lt;/a&gt; which you can use if you don&apos;t want to have to set up a whole Windows server to set up unattended installations. You generate that XML file, and you can also use that XML file to bootstrap the config.

&lt;p&gt;Again we use it as little as possible, because writing automations as XML elements is kind of a pain. So we essentially just set up a scheduled task to run the next stage of the config, we install the network driver, we import some registry settings, and we reboot. That&apos;s it. The rest of it is done with a PowerShell script.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0063.jpg&quot; alt=&quot;macOS runners
- OS installation: by hand :( but only once
- Config bootstrap: curl|sh by hand :( but only once
    - Use it as little as possible (zsh script)
    - Enable SSH, enable autologin, enable sudo NOPASSWD
    - Install a LaunchAgent to curl and run next stage
    - Disable broken session restore feature in Terminal.app
- Image config and runner boot: zsh script&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;The same goes for macOS.

&lt;p&gt;Now, unfortunately, installing the OS and bootstrapping the config does have to be done by hand. And this is because if you want to automate a macOS installation, your two options, more or less, are enterprise device management solutions, which cost a lot of money and mean that you have to have a macOS server around to control and orchestrate the servers. But if you don&apos;t want to use one of those enterprise solutions, what most open systems that are faced with this problem end up doing is to throw OpenCV at the problem. I&apos;ve seen several projects use OpenCV to OCR the setup wizard, which is... it&apos;s certainly a bold strategy. It&apos;s not really for me.

&lt;p&gt;What I decided to do instead is just install the OS by hand, and pipe &lt;code&gt;curl&lt;/code&gt; into &lt;code&gt;sh&lt;/code&gt; to kick off the config management process. And this is something that we only really have to do once, because we do it once, and then we take a snapshot of it, and then we never have to do it again, at least until the next version of macOS comes out.

&lt;p&gt;So this bootstrap script just does a handful of minimal things: it enables automatic login, it sets up a LaunchAgent to ensure that we can run our own code on each boot, and then it does a handful of other things which it honestly doesn&apos;t really have to do in this script. We could probably do these things in the &lt;code&gt;zsh&lt;/code&gt; script which we then &lt;code&gt;curl&lt;/code&gt; and run. And that&apos;s where the remainder of the work is done.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0064.jpg&quot; alt=&quot;Future directions
- Decoupling the system from Servo
- macOS arm64 runners (#64)
- Support for Forgejo Actions (#94)
- Support for other CI services?
- Dynamic runner counts / autoscaling
- Hot runners with memory ballooning
- microVM runners?&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;So looking forward, some things that we&apos;d like to do with this system.

&lt;p&gt;The first is to decouple it from Servo. So we built this CI system quite organically over the past 12 to 18 months, and we built it around Servo&apos;s needs specifically, but we think this system could be more broadly useful for other projects. We&apos;ll just have to abstract away some of the Servo specific bits, so that it can be more easily used on other projects, and that&apos;s something we&apos;re looking into now.

&lt;p&gt;Something else that we&apos;ll have to do sooner or later is add support for macOS runners on Apple Silicon, that is ARM64, and the reason we have to do this is that macOS 26, which is the most recent version of macOS that came out in September, that&apos;s just a couple months ago, that is the last version of macOS that will support x86 CPUs. And at the moment, our macOS runners run on x86 CPUs, on the host and in the guest.

&lt;p&gt;This is a little bit complicated because at the moment, our macOS runners actually run on Linux hosts, using a Linux-KVM-based sort of Hackintosh-y kind of solution. And there is no counterpart for this for arm64 hosts and guests, and I&apos;m not sure there ever will be one. So we&apos;re going to have to port the system so that it can run on macOS hosts, so we can use actual Mac hardware for this, which is easy enough to do, and that&apos;s in progress.

&lt;p&gt;But we&apos;re also going to have to port the system so it can run with other hypervisors. And this is because, although libvirt supports macOS hosts, the support for the macOS Hypervisor framework and Virtualization framework is not mature enough to actually run macOS guests in libvirt. And I&apos;m not sure how long that will take to develop, so in the meantime, we&apos;ve been looking at porting the system so that when you&apos;re on a Mac, you can run with UTM instead, and that&apos;s been working fairly well so far.

&lt;p&gt;We&apos;re also looking at porting the system so that it can run with Forgejo Actions and not just GitHub Actions. So Forgejo Actions is an open alternative to GitHub Actions that tries to be loosely compatible with GitHub Actions, and in our experience, from experimentation so far, we found that it mostly &lt;em&gt;is&lt;/em&gt; loosely compatible. We think we&apos;ll only have to make some fairly minor changes to our system to make it work on both CI systems.

&lt;p&gt;That said, this CI system could potentially be more broadly applicable to other CI services as well, because virtual machine orchestration is something that we haven&apos;t really seen any CI services have a great built-in solution for. So if this interests you and you want to use it on your project on some other CI service, then we&apos;d appreciate knowing about that, because that could be something we would explore next.

&lt;p&gt;The remaining ideas are things that I think we could look into to make our runners more efficient.

&lt;p&gt;The big one is autoscaling. So at the moment when you set up a server to operate some self-hosted runners, you essentially have to statically pre-configure how many runners of each kind of runner you want to be kept operating. And this has worked well enough for us for the most part, but it does mean that there&apos;s some kind of wasted resources sometimes, when the moment-to-moment needs of the jobs that are queued up aren&apos;t well fitted to the composition of your runner configuration. So if we had the ability to dynamically respond to demand, or some kind of autoscaling, I think we could improve our runner utilization rates a little bit, and sort of get more out of the same amount of runner capacity, the same amount of server capacity.

&lt;p&gt;There&apos;s a couple ideas here, also, about reducing boot times for the runners, which can be quite helpful if you have a big backlog of jobs queued up for these servers, and this is because time spent booting up each runner, each virtual machine, is time that cannot be spent doing real work.

&lt;p&gt;So two ways we can think of to reduce these boot times are, to have hot spares ready to go, the idea being that, if you can spin up more runners than you actually intend to run concurrently, and just have them sitting around, then you can kind of amortize the boot times, and sort of get the boot process process out of the way. And the way you do this is by spinning up a whole bunch of runners, say maybe you spin up like twenty runners, even though you only intend to run four of them concurrently.

&lt;p&gt;And what you do is you give these runners a token amount of RAM to start with. You give them like one gig of RAM instead of 16 or 32 gigs of RAM. And then when a job comes in, and you actually want to assign the runner out so that it can do the work, then you dynamically increase the RAM from one gig, or that token amount, to the actual amount, like 16 gigs or 32 gigs. And this should be fairly easy to do in practice. This is actually supported in libvirt using a feature known as memory ballooning. But there are some minor caveats, like you do lose the ability to do certain optimizations, like you can&apos;t do huge pages backing on the memory anymore. But for the most part, this should be fairly technically simple to implement.

&lt;p&gt;Something that could be more interesting in the longer term is microVMs, things like Firecracker, which as I understand it, these microVMs can sort of take the concept of paravirtualization to its logical extreme. And what it means is that on kernels that support being run as microVMs, you can boot them in like one or two seconds, instead of 20 or 30 or 60 seconds. And this could save a great deal of time, at least for jobs that run on Linux and BSD. I don&apos;t know if I said Linux and macOS, but I meant Linux and BSD.
&lt;/section&gt;

&lt;hr&gt;

&lt;section&gt;
&lt;div class=&quot;_slide&quot;&gt;
&lt;img src=&quot;/images/servo-ci/mpv-shot0065.jpg&quot; alt=&quot;github.com/servo/ci-runners
Slides: go.daz.cat/3tdhp&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;_spacer&quot;&gt;&lt;/div&gt;

&lt;p&gt;So yea, we now have a system that we use to speed up our builds in Servo&apos;s CI, and it works fairly well for us. And we think that it&apos;s potentially useful for other projects as well.

&lt;p&gt;So if you&apos;re interested to find out more, or you&apos;re interested to find out how you can use the system in your own projects, go to our GitHub repo at &lt;a href=&quot;https://github.com/servo/ci-runners&quot;&gt;servo/ci-runners&lt;/a&gt;, or you can go &lt;a href=&quot;https://go.daz.cat/3tdhp&quot;&gt;here for a link to the slides&lt;/a&gt;. Thanks!

&lt;style&gt;
article &gt; section:not(#specificity) {
	position: relative;
	display: flex;
	flex-flow: column nowrap;
	padding: 1em 0;
}
article &gt; section:not(#specificity) &gt; * {
	flex: 0 0 auto;
}
article &gt; section:not(#specificity) &gt; ._slide {
	max-height: 50vh;
	flex: 0 0 auto;
	position: sticky;
	top: 0;
	text-align: center;
}
article &gt; section:not(#specificity) &gt; ._slide &gt; img {
	max-width: 100%;
	max-height: 50vh;
}
article &gt; section:not(#specificity) &gt; ._spacer {
	flex: 1 0 1em;
}
&lt;/style&gt;</content><author><name></name></author><category term="home" /><category term="igalia" /><summary type="html">Servo is a greenfield web browser engine that supports many platforms. Automated testing for the project requires building Servo for all of those platforms, plus several additional configurations, and running nearly two million tests including the entire Web Platform Tests. How do we do all of that in under half an hour, without a hyperscaler budget for compute and an entire team to keep it all running smoothly, and securely enough to run untrusted code from contributors?</summary></entry><entry><title type="html">Generative AI in Servo</title><link href="https://www.azabani.com/2025/04/11/generative-ai-in-servo.html" rel="alternate" type="text/html" title="Generative AI in Servo" /><published>2025-04-11T12:00:00+00:00</published><updated>2025-04-11T12:00:00+00:00</updated><id>https://www.azabani.com/2025/04/11/generative-ai-in-servo</id><content type="html" xml:base="https://www.azabani.com/2025/04/11/generative-ai-in-servo.html">&lt;p&gt;&lt;a href=&quot;https://servo.org&quot;&gt;Servo&lt;/a&gt; has shown that we can build a browser with a modern, parallel layout engine in a fraction of the cost of the big incumbents, thanks to our &lt;a href=&quot;https://www.rust-lang.org&quot;&gt;powerful&lt;/a&gt; &lt;a href=&quot;https://rust-analyzer.github.io/book/&quot;&gt;tooling&lt;/a&gt;, our &lt;a href=&quot;https://servo.zulipchat.com/&quot;&gt;strong community&lt;/a&gt;, and our &lt;a href=&quot;https://book.servo.org/&quot;&gt;thorough documentation&lt;/a&gt;.
But we can, and should, build Servo without generative AI tools like GitHub Copilot.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;This post is my personal opinion, not necessarily representative of Servo or my colleagues at Igalia.
I hope it makes a difference.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;I’m the lead author of our &lt;a href=&quot;https://servo.org/blog/&quot;&gt;monthly updates&lt;/a&gt; and the &lt;a href=&quot;https://book.servo.org&quot;&gt;Servo book&lt;/a&gt;, a member of the &lt;a href=&quot;https://github.com/servo/project/blob/6dcfe4a26b034e0dccad2f4a31c1d797abcc8c82/governance/tsc/README.md&quot;&gt;Technical Steering Committee&lt;/a&gt;, and a coauthor of &lt;a href=&quot;https://book.servo.org/contributing.html#ai-contributions&quot;&gt;our current AI policy&lt;/a&gt; (&lt;a href=&quot;https://github.com/servo/book/blob/d4c87ea7646ce43b354aa5c37dea674e830d5edf/src/contributing.md#ai-contributions&quot;&gt;permalink&lt;/a&gt;).
That policy was inspired by &lt;a href=&quot;https://wiki.gentoo.org/wiki/Project:Council/AI_policy&quot;&gt;Gentoo’s AI policy&lt;/a&gt;, and has in turn inspired the AI policies of &lt;a href=&quot;https://discourse.gnome.org/t/loupe-no-longer-allows-generative-ai-contributions/27327&quot;&gt;Loupe&lt;/a&gt; and &lt;a href=&quot;https://mastodon.social/@whitequark/114303833444527216&quot;&gt;Amaranth&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Recently the TSC voted in favour of &lt;a href=&quot;https://github.com/servo/servo/discussions/36379&quot;&gt;two proposals&lt;/a&gt; that relax our ban on AI contributions.
This was a mistake, and it was also a mistake to wait until &lt;em&gt;after&lt;/em&gt; we had made our decision to seek community feedback (see &lt;a href=&quot;#on-governance&quot;&gt;§ On governance&lt;/a&gt;).
&lt;a href=&quot;#your-feedback&quot;&gt;§ Your feedback&lt;/a&gt; made it clear that those proposals are the wrong way forward for Servo.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;&lt;strong&gt;Correction (2025-04-12)&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;A previous version of this post highlighted a logic error in the &lt;a href=&quot;https://github.com/servo/servo/discussions/36379&quot;&gt;AI-assisted patch&lt;/a&gt; we used as a basis for those two proposals.
This error was made in a non-AI-assisted part of the patch.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;I call on the TSC to &lt;strong&gt;explicitly reaffirm that generative AI tools like Copilot are not welcome in Servo&lt;/strong&gt;, and make it clear that we &lt;strong&gt;intend to keep it that way indefinitely&lt;/strong&gt;, in both our policy and the community, so we can start rebuilding trust.
It’s not enough to say oops, sorry, we will not be moving forward with these proposals.&lt;/p&gt;

&lt;p&gt;Like any logic written by humans, this policy does have some unintended consequences.
Our intent was to ban AI tools that generate bullshit &lt;a href=&quot;https://link.springer.com/content/pdf/10.1007/s10676-024-09775-5.pdf&quot;&gt;[a]&lt;/a&gt; in inscrutable ways, including GitHub Copilot and ChatGPT.
But there are other tools that use &lt;a href=&quot;https://en.wikipedia.org/w/index.php?title=Transformer_(deep_learning_architecture)&amp;amp;oldid=1284020707&quot;&gt;similar&lt;/a&gt; &lt;a href=&quot;https://en.wikipedia.org/w/index.php?title=Deep_learning&amp;amp;oldid=1283714111&quot;&gt;underlying&lt;/a&gt; &lt;a href=&quot;https://en.wikipedia.org/w/index.php?title=Neural_network_(machine_learning)&amp;amp;oldid=1282432692&quot;&gt;technology&lt;/a&gt; in more useful and less problematic ways (see &lt;a href=&quot;#potential-exceptions&quot;&gt;§ Potential exceptions&lt;/a&gt;).
Reviewing these tools for use in Servo should be a &lt;strong&gt;community-driven process&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We should not punish contributors for honest mistakes, but we should &lt;strong&gt;make our policy easier to follow&lt;/strong&gt;.
Some ways to do this include documenting the tools that are known to be allowed and not allowed, documenting how to turn off features that are not allowed, and giving contributors a way to declare that they’ve read and followed the policy.&lt;/p&gt;

&lt;p&gt;The declaration would be a good place to provide a dated link to the policy, giving contributors the best chance to understand the policy and knowingly follow it (or violate it).
This is not perfect, and it won’t always be easy to enforce, but it should give contributors and maintainers a foundation of trust.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;potential-exceptions&quot;&gt;Potential exceptions&lt;/h2&gt;

&lt;p&gt;Proposals for exceptions should start in the community, and should focus on a specific tool used for a specific purpose.
If the proposal is for a specific &lt;em&gt;kind&lt;/em&gt; of tool, it must come with concrete examples of &lt;em&gt;which&lt;/em&gt; tools are to be allowed.
Much of the harm being caused by generative AI in the world around us comes from people using open-ended tools that are not fit for any purpose, or even treating them like they are &lt;a href=&quot;https://en.wikipedia.org/w/index.php?title=Artificial_general_intelligence&amp;amp;oldid=1284985284&quot;&gt;AGI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The goal of these discussions would be to understand:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the underlying challenges faced by contributors&lt;/li&gt;
  &lt;li&gt;how effective the tool is for the purpose&lt;/li&gt;
  &lt;li&gt;how well the tool and purpose mitigate the issues in the policy&lt;/li&gt;
  &lt;li&gt;whether there are any existing or alternative solutions&lt;/li&gt;
  &lt;li&gt;whether those solutions have problems that need to be addressed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the purpose may need to be constrained to mitigate the issues in the policy.
Let’s look at a couple of examples.&lt;/p&gt;

&lt;p&gt;For some tasks like &lt;strong&gt;speech recognition&lt;/strong&gt; &lt;a href=&quot;https://arxiv.org/pdf/2212.04356&quot;&gt;[b]&lt;/a&gt; and &lt;strong&gt;machine translation&lt;/strong&gt; &lt;a href=&quot;https://research.google/blog/recent-advances-in-google-translate/&quot;&gt;[c]&lt;/a&gt; &lt;a href=&quot;https://aclanthology.org/2020.amta-research.9.pdf&quot;&gt;[d]&lt;/a&gt;, tools with large language models and transformers are the state of the art (other than humans).
This means those tools may be probabilistic tools, and strictly speaking, they may be &lt;a href=&quot;https://en.wikipedia.org/w/index.php?title=Generative_artificial_intelligence&amp;amp;oldid=1284756817&quot;&gt;&lt;em&gt;generative AI&lt;/em&gt;&lt;/a&gt; tools, because the models they use are &lt;a href=&quot;https://en.wikipedia.org/w/index.php?title=Generative_model&amp;amp;oldid=1264858524&quot;&gt;&lt;em&gt;generative models&lt;/em&gt;&lt;/a&gt;.
Generative AI does not necessarily mean “AI that generates bullshit in inscrutable ways”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speech recognition&lt;/strong&gt; can be used in a variety of ways.
If plumbed into ChatGPT, it will have all of the same problems as ChatGPT.
If used for automatic captions, it can make videos and calls accessible to people that can’t hear well (myself included), but it can also infantilise us by &lt;a href=&quot;https://ericwbailey.website/published/swearing-and-automatic-captions/&quot;&gt;censoring profanities&lt;/a&gt; and make serious errors that &lt;a href=&quot;https://www.consumerreports.org/disability-rights/auto-captions-often-fall-short-on-zoom-facebook-and-others-a9742392879/&quot;&gt;cause real harm&lt;/a&gt;.
If deployed for that purpose by an online video platform, it can undermine the labour of human transcribers and lower the overall quality of captions.&lt;/p&gt;

&lt;p&gt;If used as an input method, it would be a clear win for accessibility.
My understanding of speech input tools is that they have a clear (if configurable) mapping from the things you say to the text they generate or &lt;a href=&quot;https://www.cursorless.org/docs/&quot;&gt;the edits they make&lt;/a&gt;, so they may be a good fit.&lt;/p&gt;

&lt;p&gt;In that case, &lt;em&gt;maintainer burden&lt;/em&gt; and &lt;em&gt;correctness and security&lt;/em&gt; would not be an issue, because the author is in complete control of what they write.
&lt;em&gt;Copyright issues&lt;/em&gt; seem less of a concern to me, since these tools operate on such a small scale (words and symbols) that they are unlikely to reproduce a copyrightable amount of text verbatim, but I am not a lawyer.
As for &lt;em&gt;ethical issues&lt;/em&gt;, these tools are generally trained once then run on the author’s device.
When used as an input method, they are not being used to undermine labour or justify layoffs.
I’m not sure about the process of training their models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machine translation&lt;/strong&gt; can also be used in a variety of ways.
If deployed by a language learning app, it can ruin the quality of your core product, but hey, then you can &lt;a href=&quot;https://www.washingtonpost.com/technology/2024/01/10/duolingo-ai-layoffs/&quot;&gt;lay off&lt;/a&gt; those pesky human translators.
If used to localise your product, your users will finally be able to &lt;a href=&quot;https://www.neowin.net/news/accept-essential-biscuits-windows-11-calls-zip-files-postcode-files-in-uk-english/&quot;&gt;compress to postcode file&lt;/a&gt;.
If used to localise your docs, it can make your docs worse than useless unless you &lt;a href=&quot;https://www.reddit.com/r/rust/comments/1jtw560/comment/mm6e34l/&quot;&gt;take very careful precautions&lt;/a&gt;.
What if we allowed contributors to use machine translation to communicate with each other, but not in code commits, documentation, or any other work products?&lt;/p&gt;

&lt;p&gt;Deployed carelessly, they will waste the reader’s time, and undermine the labour of actual human translators who would otherwise be happy to contribute to Servo.
If constrained to collaboration, it would still be far from perfect, but it may be worthwhile.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Maintainer burden&lt;/em&gt; should be mitigated, because this won’t change the amount or kind of text that needs to be reviewed.
&lt;em&gt;Correctness and security&lt;/em&gt; too, because this won’t change the text that can be committed to Servo.
I can’t comment on the &lt;em&gt;copyright issues&lt;/em&gt;, because I am not a lawyer.
The &lt;em&gt;ethical issues&lt;/em&gt; may be significantly reduced, because this use case wasn’t a market for human translators in the first place.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;your-feedback&quot;&gt;Your feedback&lt;/h2&gt;

&lt;p&gt;I appreciate the feedback you gave &lt;a href=&quot;https://floss.social/@servo/114296977894869359&quot;&gt;on the Fediverse&lt;/a&gt;, &lt;a href=&quot;https://bsky.app/profile/servo.org/post/3lma3ru5jok2y&quot;&gt;on Bluesky&lt;/a&gt;, and &lt;a href=&quot;https://www.reddit.com/r/rust/comments/1jtw560&quot;&gt;on Reddit&lt;/a&gt;.
I also appreciate the comments &lt;a href=&quot;https://github.com/servo/servo/discussions/36379&quot;&gt;on GitHub&lt;/a&gt; from several people who were more on the &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752620&quot;&gt;favouring&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752951&quot;&gt;side&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12754434&quot;&gt;of&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12770260&quot;&gt;the&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12775011&quot;&gt;proposal&lt;/a&gt;, even though we reached different conclusions in most cases.
One comment argued that it’s possible to use AI autocomplete safely by accepting the completions &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12756971&quot;&gt;one word at a time&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That said, the overall consensus in our community was overwhelmingly clear, including among many of those who were in favour of the proposals.
None of the benefits of generative AI tools are worth the cost in community goodwill &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12770260&quot;&gt;[e]&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Much of the dissent on GitHub was already covered by our existing policy, but there were quite a few arguments worth highlighting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speech-to-text input&lt;/strong&gt;
is ok &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12776344&quot;&gt;[f]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12756111&quot;&gt;[g]&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machine translation&lt;/strong&gt;
is generally not useful or effective for technical writing &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12751548&quot;&gt;[h]&lt;/a&gt; &lt;a href=&quot;https://www.reddit.com/r/rust/comments/1jtw560/comment/mlxty67&quot;&gt;[i]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752411&quot;&gt;[j]&lt;/a&gt;.
It can be, if some precautions are taken &lt;a href=&quot;https://www.reddit.com/r/rust/comments/1jtw560/comment/mm6e34l/&quot;&gt;[k]&lt;/a&gt;.
It may be less ethically encumbered than generative AI tools &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752707&quot;&gt;[l]&lt;/a&gt;.
Client-side machine translation is ok &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753281&quot;&gt;[m]&lt;/a&gt;.
Machine translation for collaboration is ok &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12759143&quot;&gt;[n]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12756111&quot;&gt;[o]&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The proposals.&lt;/strong&gt;
Proposal 1 is ill-defined &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12754818&quot;&gt;[p]&lt;/a&gt;.
Proposal 2 has an ill-defined distinction between autocompletes and “full” code generation &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753114&quot;&gt;[q]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752843&quot;&gt;[r]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753041&quot;&gt;[s]&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation&lt;/strong&gt;
is just as technical as code &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12767895&quot;&gt;[u]&lt;/a&gt;.
Wrong documentation is worse than no documentation &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12751560&quot;&gt;[v]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753041&quot;&gt;[w]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12769041&quot;&gt;[x]&lt;/a&gt;.
Good documentation requires human context &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12751548&quot;&gt;[y]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752411&quot;&gt;[z]&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt;
is not a good tool for answering questions &lt;a href=&quot;https://github.com/servo/book/pull/27&quot;&gt;[ab]&lt;/a&gt;.
It isn’t even that good of a programming tool &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752073&quot;&gt;[ac]&lt;/a&gt;.
Using it may be incompatible with the DCO &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752073&quot;&gt;[ad]&lt;/a&gt;.
Using it could make us depend on Microsoft to protect us against legal liability &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752938&quot;&gt;[ae]&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correctness.&lt;/strong&gt;
Generative AI code is wrong at an alarming rate &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12754116&quot;&gt;[af]&lt;/a&gt;.
Generative AI tools will lie to us with complete confidence &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753566&quot;&gt;[ag]&lt;/a&gt;.
Generative AI tools (and users of those tools) cannot explain their reasoning &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752823&quot;&gt;[ah]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12776304&quot;&gt;[ai]&lt;/a&gt;.
Humans as supervisors are ill-equipped to deal with the subtle errors that generative AI tools make &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752707&quot;&gt;[aj]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753375&quot;&gt;[ak]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12756059&quot;&gt;[al]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12766312&quot;&gt;[am]&lt;/a&gt;.
Even experts can easily be misled by these tools &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752411&quot;&gt;[an]&lt;/a&gt;.
Typing is not the hard part of programming &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752823&quot;&gt;[ao]&lt;/a&gt;, as even some of those in favour &lt;a href=&quot;https://medium.com/@polyglot_factotum/ai-is-not-a-better-ide-e395db9da063&quot;&gt;have said&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If I could offload that part of the work to copilot, I would be left with more energy for the challenging part.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Project health.&lt;/strong&gt;
Partially lifting the ban will create uncertainty that increases maintainer burden for all contributions &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753041&quot;&gt;[ap]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12776304&quot;&gt;[aq]&lt;/a&gt;.
Becoming dependent on tools with non-free models is risky &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752620&quot;&gt;[ar]&lt;/a&gt;.
Generative AI tools may not be fair use &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12755626&quot;&gt;[as]&lt;/a&gt; → &lt;a href=&quot;https://suchir.net/fair_use.html&quot;&gt;[at]&lt;/a&gt;.
Outside of Servo, people have spent so much time cleaning up after LLM-generated mess &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752843&quot;&gt;[au]&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Material.&lt;/strong&gt;
Servo contributor refuses to spend time cleaning up after LLM-generated mess &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752707&quot;&gt;[av]&lt;/a&gt;.
Others will stop donating &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752707&quot;&gt;[aw]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753442&quot;&gt;[ax]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753939&quot;&gt;[ay]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12754804&quot;&gt;[az]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12754889&quot;&gt;[ba]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12755244&quot;&gt;[bb]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12758397&quot;&gt;[bc]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12759143&quot;&gt;[bd]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12763689&quot;&gt;[be]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12755031&quot;&gt;[bf]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12756035&quot;&gt;[bg]&lt;/a&gt;, will stop contributing &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752707&quot;&gt;[bh]&lt;/a&gt;, will not start donating &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12754139&quot;&gt;[bi]&lt;/a&gt;, will not start contributing &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753271&quot;&gt;[bj]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12755031&quot;&gt;[bk]&lt;/a&gt;, or will not start promoting &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12753271&quot;&gt;[bl]&lt;/a&gt; the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broader context.&lt;/strong&gt;
Allowing AI contributions is a bad signal for the project’s relationship with the broader AI movement &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752415&quot;&gt;[bm]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752707&quot;&gt;[bn]&lt;/a&gt; &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12757753&quot;&gt;[bo]&lt;/a&gt;.
The modern AI movement is backed by overwhelming capital interests, and must be opposed equally strongly &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752707&quot;&gt;[bp]&lt;/a&gt;.
People often “need” GitHub or Firefox, but no one “needs” Servo, so we can and should be held to a higher standard &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752707&quot;&gt;[bq]&lt;/a&gt;.
Rejection of AI is only credible if the project rejects AI contributions &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752707&quot;&gt;[br]&lt;/a&gt;.
We can attract funding from AI-adjacent parties without getting into AI ourselves &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12752823&quot;&gt;[bs]&lt;/a&gt;, though that may be easier said than done &lt;a href=&quot;https://github.com/servo/servo/discussions/36379#discussioncomment-12775861&quot;&gt;[bt]&lt;/a&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;on-governance&quot;&gt;On governance&lt;/h2&gt;

&lt;p&gt;Several people have raised concerns about how Servo’s governance could have led to this decision, and some have even suspected foul play.
But like most discussions in the TSC, most of the discussion around AI contributions happened async on Zulip, and we didn’t save anything special for the synchronous monthly public calls.
As a result, whenever the discussion overflowed the sync meeting, we just continued it internally, so the public minutes were missing the vast majority of the discussion (and the decisions).
These decisions should probably have happened in public.&lt;/p&gt;

&lt;p&gt;Our decisions followed the TSC’s usual process, with a strong preference for resolving disagreements by consensus rather than by voting, but we didn’t have any consistent structure for moving from one to the other.
This may have made the decision process prone to being blocked and dominated by the most persistent participants.&lt;/p&gt;

&lt;p&gt;Contrast this with decision making within Igalia, where we also prefer consensus before voting, but the consensus process is always used to inform &lt;em&gt;proposals&lt;/em&gt; that are drafted by &lt;em&gt;more than one person&lt;/em&gt; and then &lt;em&gt;always voted on&lt;/em&gt;.
Most polls are “yes” or “no” by majority, and only a few polls for the most critical matters allow vetoing.
This ensures that proposals have meaningful support before being considered, and if only one person is strongly against something, they are heard but they generally can’t single-handedly block the decision with debate.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;The rules are actually more complex than just by majority.
There’s clear advice on what “yes”, “no”, and “abstain” actually mean, they take into account abstaining and undecided voters, there are set time limits and times to contact undecided voters, and they provide for a way to abort a poll if the wording of the proposal is ill-formed.&lt;/p&gt;

  &lt;p&gt;We had twenty years to figure out all those details, and one of the improvements above only landed a couple of months ago.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;We also didn’t have any consistent structure for community consultation, so it wasn’t clear how or when we should seek feedback.
A public RFC process may have helped with this, and would also help us collaborate on and document other decisions.&lt;/p&gt;

&lt;p&gt;More personally, I did not participate in the extensive discussion in January and February that helped move consensus in the TSC towards allowing the non-code and Copilot exceptions until &lt;a href=&quot;https://github.com/servo/project/blob/6dcfe4a26b034e0dccad2f4a31c1d797abcc8c82/governance/tsc/tsc-2025-02-24.md#ai-policy-review&quot;&gt;fairly late&lt;/a&gt;.
Some of that was because I was on leave, including for the vote on the initial Copilot “experiments”, but most of it was that I didn’t have the bandwidth.
Doing politics is hard, exhausting work, and there’s only so much of it you can do, even when you’re not wearing three other hats.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;a href=&quot;/images/generative-ai-in-servo.jpg&quot;&gt;&lt;img alt=&quot;a white and grey cat named Diffie, poking her head out through a sliding door&quot; title=&quot;a white and grey cat named Diffie, poking her head out through a sliding door&quot; src=&quot;/images/generative-ai-in-servo.jpg&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;</content><author><name></name></author><category term="home" /><category term="igalia" /><summary type="html">Servo has shown that we can build a browser with a modern, parallel layout engine in a fraction of the cost of the big incumbents, thanks to our powerful tooling, our strong community, and our thorough documentation. But we can, and should, build Servo without generative AI tools like GitHub Copilot.</summary></entry><entry><title type="html">Meet the CSS highlight pseudos</title><link href="https://www.azabani.com/2022/09/01/meet-the-css-highlight-pseudos.html" rel="alternate" type="text/html" title="Meet the CSS highlight pseudos" /><published>2022-09-01T15:00:00+00:00</published><updated>2022-09-01T15:00:00+00:00</updated><id>https://www.azabani.com/2022/09/01/meet-the-css-highlight-pseudos</id><content type="html" xml:base="https://www.azabani.com/2022/09/01/meet-the-css-highlight-pseudos.html">&lt;p&gt;A year and a half ago, I was asked to help upstream a Chromium patch allowing authors to recolor &lt;span class=&quot;_spelling&quot;&gt;spelling&lt;/span&gt; and &lt;span class=&quot;_grammar&quot;&gt;grammar&lt;/span&gt; errors in CSS.
At the time, I didn’t realise that this was part of a far more ambitious effort to reimagine spelling errors, grammar errors, text selections, and more as a coherent system that didn’t yet exist as such in any browser.
That system is known as the &lt;em&gt;highlight pseudos&lt;/em&gt;, and this post will focus on the design of said system and its consequences for authors.&lt;/p&gt;

&lt;p&gt;This is the third part of a series (&lt;a href=&quot;/2021/05/17/spelling-grammar.html&quot;&gt;part one&lt;/a&gt;, &lt;a href=&quot;/2021/12/16/spelling-grammar-2.html&quot;&gt;part two&lt;/a&gt;) about Igalia’s work towards making the CSS highlight pseudos a reality.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;&lt;strong&gt;Update (2024-04-29):&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;added &lt;a href=&quot;#applicable-properties&quot;&gt;details about applicable properties&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;removed &lt;a href=&quot;#accessing-global-constants&quot;&gt;§ &lt;em&gt;Accessing global constants&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;added &lt;a href=&quot;#custom-properties&quot;&gt;§ &lt;em&gt;Custom properties&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/aside&gt;

&lt;style&gt;
article { --cr-highlight: #3584E4; --cr-highlight-aC0h: #3584E4C0; }
article figure &gt; img { max-width: 100%; }
article figure &gt; figcaption { max-width: 30rem; margin-left: auto; margin-right: auto; }
article pre, article code { font-family: Inconsolata, monospace, monospace; }
article aside, article blockquote { font-size: 0.75em; max-width: 30rem; }
article aside { margin-left: 0; padding-left: 1rem; border-left: 3px double rebeccapurple; }
article blockquote { margin-left: 3rem; }
article blockquote:before { margin-left: -2rem; }

._spelling, ._grammar { text-decoration-thickness: /* iOS takes 0 literally */ 1px; text-decoration-skip-ink: none; }
._spelling { text-decoration: /* not a shorthand on iOS */ underline; text-decoration-style: wavy; text-decoration-color: red; }
._grammar { text-decoration: /* not a shorthand on iOS */ underline; text-decoration-style: wavy; text-decoration-color: green; }
._example { border: 2px dotted rebeccapurple; }
._example * + *, ._hpdemo * + * { margin-top: 0; }

._checker { position: relative; margin-left: auto; margin-right: auto; }
._checker:focus { outline: none; }
._checker::before { display: flex; align-items: center; justify-content: center; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; font-size: 7em; color: transparent; background: transparent; content: &quot;▶&quot;; }
._checker:not(:focus)::before { color: rebeccapurple; background: #66339940; }

._checker tbody th { text-align: left; }
._checker ._live::selection, ._checker ._live *::selection { color: currentColor; background: transparent; }
._checker:not(:focus) ._live &gt; div { visibility: hidden; }
._checker:not([data-phase=done]):not(#specificity) ._live &gt; div,
._checker:not([data-phase=done]):not(#specificity) ._live &gt; div * { color: transparent; }
._checker:not([data-phase=done]):not(#specificity) ._live &gt; div::selection,
._checker:not([data-phase=done]):not(#specificity) ._live &gt; div *::selection { color: transparent; }
._checker:not([data-phase=done]):not(#specificity) ._live &gt; div::highlight(checker),
._checker:not([data-phase=done]):not(#specificity) ._live &gt; div *::highlight(checker),
._checker:not([data-phase=done]):not(#specificity) ._live &gt; div::highlight(lower),
._checker:not([data-phase=done]):not(#specificity) ._live &gt; div *::highlight(lower) { color: transparent; }
._checker ._live &gt; div { width: 5em; }
._checker ._live &gt; div { position: relative; line-height: 1; }
._checker ._live &gt; div &gt; span { position: absolute; margin: 0; padding-top: calc((1.5em - 1em) / 2); width: 5em; }

/*
    ::highlight() [end-to-end test]
    = no, if the pseudo selector is broken and/or no active highlight
    = yes, if the pseudo selector works and highlight is active
*/
._checker ._custom
    :nth-child(2) { color: transparent; background: transparent; }
._checker ._custom
    :nth-child(1)::highlight(checker) { color: transparent; }
._checker ._custom
    :nth-child(2)::highlight(checker) { color: CanvasText; background: Canvas; }

/*
    ::highlight() [selector]
    = no, if the pseudo selector is unsupported
    = yes, if the pseudo selector is supported
    • highlight not active, only for selector list validity
*/
._checker ._chps
    :nth-child(2) { color: transparent; }
._checker ._chps
    :nth-child(1), :not(*)::highlight(checker) { color: transparent; }
._checker ._chps
    :nth-child(2), :not(*)::highlight(checker) { color: CanvasText; }

/*
    ::highlight() [API]
    = no, if the API is missing or broken
    = yes, if the API is present and working
*/
._checker ._cha {}

/*
    ::spelling-error [end-to-end test]
    = no, if the pseudo selector is broken and/or no active highlight
    = yes, if the pseudo selector works and highlight is active
*/
._checker [spellcheck]
    :nth-child(2) { color: transparent; background: transparent; }
._checker [spellcheck]
    :nth-child(1)::spelling-error { color: transparent; }
._checker [spellcheck]
    :nth-child(2)::spelling-error { color: CanvasText; background: Canvas; }
._checker [spellcheck]
    *::spelling-error { text-decoration: none; }

/*
    Highlight inheritance (::highlight)
    = no, if var() inherits from originating element
    = yes, if var() ignores originating element and uses fallback
*/
._checker ._hih
    { color: transparent; background: transparent; }
._checker ._hih::highlight(checker)
    { --t: transparent; --x: CanvasText; --y: Canvas; }
._checker ._hih :nth-child(1)::highlight(checker)
    { color: var(--t, CanvasText); background: var(--t, Canvas); }
._checker ._hih :nth-child(2)::highlight(checker)
    { color: var(--x, transparent); background: var(--y, transparent); }

/*
    Highlight inheritance (::selection)
    = no, if var() inherits from originating element
    = yes, if var() ignores originating element and uses fallback
*/
._checker ._his
    { color: transparent; background: transparent; }
._checker ._his::selection
    { --t: transparent; --x: CanvasText; --y: Canvas; }
._checker ._his :nth-child(1)::selection
    { color: var(--t, CanvasText); background: var(--t, Canvas); }
._checker ._his :nth-child(2)::selection
    { color: var(--x, transparent); background: var(--y, transparent); }

/*
    Highlight overlay painting
    = no, if currentColor takes color from originating element only
    = yes, if currentColor takes color from next active highlight
    • lower highlight “yes” is hidden by ‘-webkit-text-fill-color’
*/
._checker ._hop
    { color: transparent; background: transparent; }
._checker ._hop :nth-child(1) { color: CanvasText; }
._checker ._hop :nth-child(1)::highlight(lower) { color: transparent; }
._checker ._hop :nth-child(1)::highlight(checker) { color: currentColor; }
._checker ._hop :nth-child(2) { color: transparent; }
._checker ._hop :nth-child(2)::highlight(lower) { color: CanvasText; -webkit-text-fill-color: transparent; }
._checker ._hop :nth-child(2)::highlight(checker) { color: currentColor; -webkit-text-fill-color: currentColor; }

._table { font-size: 0.75em; }
._table td, ._table th { vertical-align: top; border: 1px solid black; }
._table td:not(._tight), ._table th:not(._tight) { padding: 0.5em; }
._tight picture, ._tight img { vertical-align: top; }
._compare * + *, ._tight * + *, ._gifs * + * { margin-top: 0; }
._compare { max-width: 100%; border: 1px solid rebeccapurple; }
._compare &gt; div { max-width: 100%; position: relative; touch-action: pinch-zoom; --cut: 50%; }
._compare &gt; div &gt; * { vertical-align: top; max-width: 100%; }
._compare &gt; div &gt; :nth-child(1) { position: absolute; clip: rect(auto, auto, auto, var(--cut)); }
._compare &gt; div &gt; :nth-child(2) { position: absolute; width: var(--cut); height: 100%; border-right: 1px solid rebeccapurple; }
._compare &gt; div &gt; :nth-child(2):before { content: var(--left-label); color: rebeccapurple; font-size: 0.75em; position: absolute; right: 0.5em; }
._compare &gt; div &gt; :nth-child(2):after { content: var(--right-label); color: rebeccapurple; font-size: 0.75em; position: absolute; left: calc(100% + 0.5em); }
._sum td:first-of-type { padding-right: 1em; }
._gifs { position: relative; display: flex; flex-flow: column nowrap; }
._gifs &gt; video { transition: opacity 0.125s linear; }
._gifs &gt; button { transition: 0.125s linear; transition-property: color, background-color; }
._gifs._paused &gt; video { opacity: 0.5; }
._gifs._paused &gt; button { color: rebeccapurple; background: #66339940; }
._gifs &gt; button { position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; font-size: 7em; color: transparent; background: transparent; content: &quot;▶&quot;; }
._gifs &gt; button:focus-visible { outline: 0.25rem solid #663399C0; outline-offset: -0.25rem; }

._commits { position: relative; }
._commits &gt; :first-child { position: absolute; right: -0.1em; height: 100%; border-right: 0.2em solid rgba(102,51,153,0.5); }
._commits &gt; :last-child { position: relative; padding-right: 0.5em; }
* + ._commit, ._commit * + * { margin-top: 0; }
._commit { line-height: 2; margin-right: -1.5em; text-align: right; }
._commit &gt; img { width: 2em; vertical-align: middle; }
._commit &gt; a { padding-right: 0.5em; text-decoration: none; color: rebeccapurple; }
._commit &gt; a &gt; code { font-size: 1em; }
._commit-none &gt; a { color: rgba(102,51,153,0.5); }
&lt;/style&gt;

&lt;h2 id=&quot;contents&quot;&gt;Contents&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#what-are-they&quot;&gt;What are they?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#can-i-use-them&quot;&gt;Can I use them?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#how-do-i-use-them&quot;&gt;How do I use them?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#how-do-they-work&quot;&gt;How do they work?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#gotchas&quot;&gt;Gotchas&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#removing-decorations-and-shadows&quot;&gt;Removing decorations and shadows&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#accessing-global-constants&quot;&gt;Accessing global constants&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#spec-issues&quot;&gt;Spec issues&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-are-they&quot;&gt;What are they?&lt;/h2&gt;

&lt;p&gt;CSS has four highlight pseudos and an open set of author-defined custom highlight pseudos.
They have their roots in ::selection, which was a rudimentary and non-standard, but widely supported, way of styling text and images selected by the user.&lt;/p&gt;

&lt;p&gt;The built-in highlights are ::selection for user-selected content, ::target-text for linking to text fragments, ::spelling-error for misspelled words, and ::grammar-error for text with grammar errors, while the custom highlights are known as ::highlight(&lt;em&gt;x&lt;/em&gt;) where &lt;em&gt;x&lt;/em&gt; is the author-defined highlight name.&lt;/p&gt;

&lt;h2 id=&quot;can-i-use-them&quot;&gt;Can I use them?&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/::selection&quot;&gt;::selection&lt;/a&gt; has long been supported by all of the major browsers, and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/::target-text&quot;&gt;::target-text&lt;/a&gt; shipped in Chromium 89.
But for most of that time, no browser had yet implemented the more robust highlight pseudo system in the &lt;a href=&quot;https://drafts.csswg.org/css-pseudo/&quot;&gt;CSS pseudo spec&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;::highlight() and the custom highlight API shipped in Chromium 105, thanks to the work by members&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; of the Microsoft Edge team.
They are also available in Safari 14.1 (including iOS 14.5) as an experimental feature (Highlight API).
You can enable that feature in the Develop menu, or for iOS, under Settings &amp;gt; Safari &amp;gt; Advanced.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;Safari’s support currently has a couple of quirks, as of TP 152.
Range is not supported for custom highlights yet, only StaticRange, and the Highlight constructor has a bug where it requires passing exactly one range, ignoring any additional arguments.
To create a Highlight with no ranges, first create one with a dummy range, then call the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clear&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete&lt;/code&gt; methods.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;Chromium 105 also implements the vast majority of the new highlight pseudo system.
This includes highlight overlay painting, which was enabled for all highlight pseudos, and highlight inheritance, which was enabled for ::highlight() only.&lt;/p&gt;

&lt;p&gt;Chromium 108 includes ::spelling-error and ::grammar-error as an experimental feature, together with the new ‘text-decoration-line’ values ‘spelling-error’ and ‘grammar-error’.
Chromium 111 enables highlight inheritance for ::selection and ::target-text as an experimental feature, in addition to ::highlight() and the spelling and grammar pseudos (which always use highlight inheritance).
You can enable these features at&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;chrome://flags/#enable-experimental-web-platform-features&lt;/p&gt;
&lt;/blockquote&gt;

&lt;aside&gt;
  &lt;p&gt;Chromium’s support also currently has some bugs, as of r1041796.
Notably, highlights don’t yet work under ::first-line and ::first-letter&lt;sup id=&quot;fnref:2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;, ‘text-shadow’ is &lt;a href=&quot;https://crbug.com/1350475&quot;&gt;not yet enabled&lt;/a&gt; for ::highlight(), computedStyleMap &lt;a href=&quot;https://crbug.com/1099874&quot;&gt;results are wrong&lt;/a&gt; for ‘currentColor’, and highlights that split ligatures (e.g. for complex scripts) only render accurately in ::selection&lt;sup id=&quot;fnref:2:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;Click the table below to see if your browser supports these features.&lt;/p&gt;

&lt;pre id=&quot;debug&quot; hidden=&quot;&quot; style=&quot;position: fixed; color: white; background: black; left: 0; top: 0; right: 0; margin: 0; white-space: pre-wrap;&quot;&gt;act: &lt;span id=&quot;debug_active&quot;&gt;&lt;/span&gt;
sel: &lt;span id=&quot;debug_selection&quot;&gt;&lt;/span&gt;
cha: &lt;span id=&quot;debug_cha&quot;&gt;&lt;/span&gt;
&lt;span id=&quot;debug_count&quot; hidden=&quot;&quot;&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;div class=&quot;flex column_bag&quot;&gt;
&lt;table class=&quot;_table _checker&quot; contenteditable=&quot;&quot; spellcheck=&quot;false&quot; data-phase=&quot;fresh&quot;&gt;
    &lt;thead&gt;&lt;tr&gt;
        &lt;th&gt;&lt;/th&gt;&lt;th&gt;yours&lt;/th&gt;&lt;th&gt;Chromium&lt;/th&gt;&lt;th&gt;Safari&lt;/th&gt;&lt;th&gt;Firefox&lt;/th&gt;
    &lt;/tr&gt;&lt;/thead&gt;
    &lt;!--
        Safari 14.0.3 (iOS 14.4.2): -selector (H)
        Safari 14.1.2 (macOS 11): +selector (ab)
        Safari 15.6? (iOS 15.6.1): +selector (ab)
        Safari 15.6.1 (macOS 11): +selector (ab)
        Safari TP 152 (macOS 12.5.1): +selector (ab)
            (16.0, WebKit 17615.1.2.3)
    --&gt;
    &lt;tr&gt;&lt;th&gt;Custom highlights&lt;/th&gt;
        &lt;td class=&quot;_live&quot;&gt;&lt;div class=&quot;_custom&quot;&gt;&lt;span&gt;no&lt;/span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/div&gt;&lt;/td&gt;
        &lt;td&gt;105&lt;/td&gt;&lt;td&gt;14.1*&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;
    &lt;/tr&gt;&lt;tr&gt;&lt;th&gt;• ::highlight()&lt;/th&gt;
        &lt;td class=&quot;_live&quot;&gt;&lt;div class=&quot;_chps&quot;&gt;&lt;span&gt;no&lt;/span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/div&gt;&lt;/td&gt;
        &lt;td&gt;105&lt;/td&gt;&lt;td&gt;14.1*&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;
    &lt;/tr&gt;&lt;tr&gt;&lt;th&gt;• CSSOM API&lt;/th&gt;
        &lt;td class=&quot;_live&quot;&gt;&lt;div class=&quot;_cha&quot;&gt;&lt;span&gt;no&lt;/span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/div&gt;&lt;/td&gt;
        &lt;td&gt;105&lt;/td&gt;&lt;td&gt;14.1* (ab)&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;
    &lt;/tr&gt;&lt;tr&gt;&lt;th&gt;::spelling-error&lt;/th&gt;
        &lt;td class=&quot;_live&quot;&gt;&lt;div spellcheck=&quot;true&quot; lang=&quot;en&quot;&gt;&lt;span&gt;no&lt;/span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/div&gt;&lt;/td&gt;
        &lt;td&gt;108*&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;
    &lt;/tr&gt;&lt;tr&gt;&lt;th&gt;Highlight overlay painting&lt;/th&gt;
        &lt;td class=&quot;_live&quot;&gt;&lt;div class=&quot;_hop&quot;&gt;&lt;span&gt;no&lt;/span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/div&gt;&lt;/td&gt;
        &lt;td&gt;105&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;
    &lt;/tr&gt;&lt;tr&gt;&lt;th&gt;Highlight inheritance (::selection)&lt;/th&gt;
        &lt;td class=&quot;_live&quot;&gt;&lt;div class=&quot;_his&quot;&gt;&lt;span&gt;no&lt;/span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/div&gt;&lt;/td&gt;
        &lt;td&gt;111*&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;
    &lt;/tr&gt;&lt;tr&gt;&lt;th&gt;Highlight inheritance (::highlight)&lt;/th&gt;
        &lt;td class=&quot;_live&quot;&gt;&lt;div class=&quot;_hih&quot;&gt;&lt;span&gt;no&lt;/span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/div&gt;&lt;/td&gt;
        &lt;td&gt;105&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;&lt;td&gt;?&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;
&lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
&lt;aside&gt;
        &lt;ul&gt;
          &lt;li&gt;* = experimental (can be enabled in UI)&lt;/li&gt;
          &lt;li&gt;S = ::highlight() unsupported in querySelector&lt;/li&gt;
          &lt;li&gt;C = CSS.highlights missing or setlike (&lt;a href=&quot;https://www.w3.org/TR/2020/WD-css-highlight-api-1-20201208/&quot;&gt;older API from 2020&lt;/a&gt;)&lt;/li&gt;
          &lt;li&gt;H = new Highlight() missing&lt;/li&gt;
          &lt;li&gt;a = StaticRange only (no support for Range)&lt;/li&gt;
          &lt;li&gt;b = new Highlight() requires exactly one range argument&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/aside&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;

&lt;script&gt;
    let checkerTimer = null;

    // selectionchange events can get stuck in infinite loops if they get
    // normalised in a way that fixCheckerSelectionIfNeeded doesn’t expect
    const selectionchangeTimes = [...Array(10)].map(_ =&gt; null);

    const checker = document.querySelector(&quot;._checker&quot;);
    const counts = new Map;

    function debug_active() {
        if (document.querySelector(&quot;#debug&quot;).hidden)
            return;
        const debug = document.querySelector(&quot;#debug_active&quot;);
        debug.textContent = document.activeElement;
    }

    function debug_selection() {
        if (document.querySelector(&quot;#debug&quot;).hidden)
            return;
        const debug = document.querySelector(&quot;#debug_selection&quot;);
        const sel = getSelection();
        debug.textContent =
            `${sel.anchorOffset} ${format(sel.anchorNode)}`
            + `\n     `
            + `${sel.focusOffset} ${format(sel.focusNode)}`;
        function format(node) {
            if (node == null || node.nodeValue == null)
                return &quot;&quot;;
            if (node.nodeValue.length &lt; 30)
                return `${node.nodeName} &quot;${node.nodeValue}&quot;`;
            return ` &quot;${node.nodeName} ${node.nodeValue.slice(0,27)}&quot;...`;
        }
    }

    function debug_count(eventType) {
        if (document.querySelector(&quot;#debug&quot;).hidden)
            return;
        console.log(eventType);
        counts.set(eventType, (counts.get(eventType) ?? 0) + 1);
        const debug = document.querySelector(&quot;#debug_count&quot;);
        debug.textContent = &quot;&quot;;
        for (const [i, n] of counts)
            debug.textContent += (debug.textContent ? &quot;    &quot; : &quot;cou:&quot;)
                + ` • ${n} x ${i}\n`;
    }

    function debug_cha(message) {
        if (document.querySelector(&quot;#debug&quot;).hidden)
            return;
        const debug = document.querySelector(&quot;#debug_cha&quot;);
        debug.textContent = message;
    }

    checker.addEventListener(&quot;focus&quot;, ({target}) =&gt; {
        debug_count(&quot;focus&quot;);
        if (target.dataset.phase == &quot;fresh&quot;) {
            target.dataset.phase = &quot;spell&quot;;
            checkerTimer = setTimeout(finish, 250);
            const range = new Range;
            range.selectNodeContents(target.querySelector(&quot;[spellcheck]&quot;));
            getSelection().removeAllRanges();
            getSelection().addRange(range);
            if (this.internals)
                internals.setMarker(document, range, &quot;spelling&quot;);
        } else if (target.dataset.phase == &quot;spell&quot;) {
            clearTimeout(checkerTimer);
            checkerTimer = setTimeout(finish, 250);
        } else {
            finish();
        }

        function finish() {
            target.dataset.phase = &quot;done&quot;;
            checkerTimer = null;

            const selector = (() =&gt; {
                try {
                    return !document.querySelector(&quot;:not(*)::highlight(checker)&quot;);
                } catch (e) {}
                return false;
            })();
            const collection = !!(this.CSS &amp;&amp; CSS.highlights &amp;&amp; CSS.highlights.set);
            const ctor = !!this.Highlight;
            const staticRangesOnly = ctor ? (() =&gt; {
                try {
                    const range = new Range;
                    range.selectNodeContents(document.body);
                    return !new Highlight(range);
                } catch (e) {}
                try {
                    const range = new StaticRange({
                        startOffset: 0, endOffset: 0,
                        startContainer: document.body,
                        endContainer: document.body,
                    });
                    return !!new Highlight(range);
                } catch (e) {}
                return null;
            })() : null;
            const ctorTakesExactlyOneRange = ctor ? (() =&gt; {
                try {
                    const foo = new StaticRange({
                        startOffset: 0, endOffset: 0,
                        startContainer: document.body,
                        endContainer: document.body,
                    });
                    const bar = new StaticRange({
                        startOffset: 1, endOffset: 1,
                        startContainer: document.body,
                        endContainer: document.body,
                    });
                    switch (new Highlight(foo, bar).size) {
                        case 1: return true;
                        case 2: return false;
                    }
                } catch (e) {}
                return null;
            })() : null;

            checker.querySelector(&quot;._cha&quot;).textContent = (() =&gt; {
                if (selector &amp;&amp; collection &amp;&amp; ctor &amp;&amp; !staticRangesOnly &amp;&amp; !ctorTakesExactlyOneRange)
                    return &quot;yes&quot;;
                if (!selector || !collection || !ctor) {
                    let result = &quot;no (&quot;;
                    result += !selector ? &quot;S&quot; : &quot;&quot;;
                    result += !collection ? &quot;C&quot; : &quot;&quot;;
                    result += !ctor ? &quot;H&quot; : &quot;&quot;;
                    return result + &quot;)&quot;;
                }
                if (staticRangesOnly || ctorTakesExactlyOneRange) {
                    let result = &quot;buggy (&quot;;
                    result += staticRangesOnly ? &quot;a&quot; : &quot;&quot;;
                    result += ctorTakesExactlyOneRange ? &quot;b&quot; : &quot;&quot;;
                    return result + &quot;)&quot;;
                }
            })();

            try {
                if (this.CSS &amp;&amp; CSS.highlights) {
                    const hop = new StaticRange({
                        startOffset: 0, endOffset: 2,
                        startContainer: checker.querySelector(&quot;._hop&quot;),
                        endContainer: checker.querySelector(&quot;._hop&quot;),
                    });
                    const custom = new StaticRange({
                        startOffset: 0, endOffset: 2,
                        startContainer: checker.querySelector(&quot;._custom&quot;),
                        endContainer: checker.querySelector(&quot;._custom&quot;),
                    });
                    const hih = new StaticRange({
                        startOffset: 0, endOffset: 2,
                        startContainer: checker.querySelector(&quot;._hih&quot;),
                        endContainer: checker.querySelector(&quot;._hih&quot;),
                    });

                    CSS.highlights.set(&quot;lower&quot;, new Highlight(hop));

                    // work around Safari bug where ctor takes exactly one range
                    // (beware that having hop highlighted by lower but not by
                    // checker causes false “yes”, because Safari does not seem
                    // to support ‘-webkit-text-fill-color’ in highlights)
                    const h = new Highlight(hop, custom, hih);
                    CSS.highlights.set(&quot;checker&quot;, h);
                    if (CSS.highlights.get(&quot;checker&quot;).size == 1) {
                        h.add(custom);
                        h.add(hih);
                    }
                }
            } catch (e) {
                debug_cha(&quot;ex: &quot; + e + &quot;\n&quot; + e.stack + &quot;\n&quot; + this.Highlight);
            }
            fixCheckerSelectionIfNeeded();
        }
    });

    checker.addEventListener(&quot;blur&quot;, ({target}) =&gt; {
        if (target.dataset.phase == &quot;done&quot;)
            getSelection().removeAllRanges();
    });

    checker.addEventListener(&quot;click&quot;, ({target}) =&gt; {
        if (target.dataset.phase != &quot;done&quot;)
            return;
        debug_count(&quot;click&quot;);
        fixCheckerSelectionIfNeeded();
    });

    checker.addEventListener(&quot;beforeinput&quot;, event =&gt; {
        event.preventDefault();
    });

    document.addEventListener(&quot;selectionchange&quot;, event =&gt; {
        const now = performance.now();
        const front = selectionchangeTimes.shift();
        selectionchangeTimes.push(now);
        if (now - front &lt; 1000)
            return;
        debug_count(&quot;selectionchange&quot;);
        if (checker.dataset.phase != &quot;done&quot;)
            return;
        debug_active();
        if (document.activeElement != checker)
            return;
        fixCheckerSelectionIfNeeded();
    });

    function fixCheckerSelectionIfNeeded() {
        debug_count(&quot;fix&quot;);
        const row = checker.querySelector(&quot;._his&quot;);
        const sel = getSelection();
        let anchorOk = false, focusOk = false;
        for (let node = row; node != null; node = node.firstChild)
            if (sel.anchorNode == node &amp;&amp; sel.anchorOffset == 0)
                anchorOk = true;
        for (let node = row; node != null; node = node.lastChild)
            if (sel.focusNode == node &amp;&amp; sel.focusOffset == (
                    node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length))
                focusOk = true;
        if (anchorOk &amp;&amp; focusOk)
            return;
        debug_selection();
        const his = new Range;
        his.selectNodeContents(row);
        getSelection().removeAllRanges();
        getSelection().addRange(his);
    }
&lt;/script&gt;

&lt;h2 id=&quot;how-do-i-use-them&quot;&gt;How do I use them?&lt;/h2&gt;

&lt;p&gt;While you can write rules for highlight pseudos that target all elements, as was commonly done for pre-standard ::selection, selecting specific elements can be more powerful, allowing descendants to cleanly override highlight styles.&lt;/p&gt;

&lt;figure&gt;
  &lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex column_bag&quot;&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 2em; color: white;&quot;&gt;
    &lt;span style=&quot;color: white; background: black;&quot;&gt;the fox jumps over the dog&lt;/span&gt;
    &lt;div&gt;
        &lt;span style=&quot;color: white; background: darkred;&quot;&gt;(the &lt;/span&gt;&lt;sup style=&quot;color: white; background: darkred;&quot;&gt;quick&lt;/sup&gt;&lt;span style=&quot;color: white; background: darkred;&quot;&gt; fox, mind you)&lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;:root::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;white&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;black&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;aside&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;darkred&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;the fox jumps over the dog
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;aside&amp;gt;&lt;/span&gt;
        (the &lt;span class=&quot;nt&quot;&gt;&amp;lt;sup&amp;gt;&lt;/span&gt;quick&lt;span class=&quot;nt&quot;&gt;&amp;lt;/sup&amp;gt;&lt;/span&gt; fox, mind you)
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;Previously the same code would yield…&lt;/p&gt;

&lt;figure&gt;
  &lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 2em; color: white;&quot;&gt;
    &lt;span style=&quot;color: white; background: var(--cr-highlight);&quot;&gt;the fox jumps over the dog&lt;/span&gt;
    &lt;div&gt;
        &lt;span style=&quot;color: white; background: darkred;&quot;&gt;(the &lt;/span&gt;&lt;sup style=&quot;color: white; background: var(--cr-highlight);&quot;&gt;quick&lt;/sup&gt;&lt;span style=&quot;color: white; background: darkred;&quot;&gt; fox, mind you)&lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;figcaption&gt;
    &lt;p&gt;(in older browsers)&lt;/p&gt;

    &lt;p&gt;Notice how &lt;em&gt;none&lt;/em&gt; of the text is white on black, because there are always other elements (body, p, aside, sup) between the root and the text.&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;…unless you also selected the descendants of :root and aside:&lt;/p&gt;

&lt;figure&gt;
  &lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;:root::selection&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;/* (or just ::selection) */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;white&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;black&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;aside&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;aside&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;green&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;Note that a bare ::selection rule still means *::selection, and like any universal rule, it can interfere with inheritance when mixed with non-universal highlight rules.&lt;/p&gt;

&lt;figure&gt;
  &lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex column_bag&quot;&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 2em; color: white;&quot;&gt;
    &lt;span style=&quot;color: white; background: black;&quot;&gt;the fox jumps over the dog&lt;/span&gt;
    &lt;div&gt;
        &lt;span style=&quot;color: white; background: darkred;&quot;&gt;(the &lt;/span&gt;&lt;sup style=&quot;color: white; background: black;&quot;&gt;quick&lt;/sup&gt;&lt;span style=&quot;color: white; background: darkred;&quot;&gt; fox, mind you)&lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;white&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;black&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;aside&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;darkred&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;the fox jumps over the dog
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;aside&amp;gt;&lt;/span&gt;
        (the &lt;span class=&quot;nt&quot;&gt;&amp;lt;sup&amp;gt;&lt;/span&gt;quick&lt;span class=&quot;nt&quot;&gt;&amp;lt;/sup&amp;gt;&lt;/span&gt; fox, mind you)
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;figcaption&gt;
    &lt;p&gt;sup::selection &lt;em&gt;would have&lt;/em&gt; inherited ‘darkred’ from aside::selection, but the universal ::selection rule matches it directly, so it becomes black.&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;::selection is primarily controlled by user input, though pages can both read and write the active ranges via the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Selection&quot;&gt;Selection&lt;/a&gt; API with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection&quot;&gt;getSelection()&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;::target-text is activated by navigating to a URL ending in a &lt;a href=&quot;https://wicg.github.io/scroll-to-text-fragment/#the-fragment-directive&quot;&gt;fragment directive&lt;/a&gt;, which has its own syntax embedded in the #fragment. For example:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#foo:~:text=bar&lt;/code&gt; targets #foo and highlights the first occurrence of “bar”&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#:~:text=the,dog&lt;/code&gt; highlights the first range of text from “the” to “dog”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;::spelling-error and ::grammar-error are controlled by the user’s spell checker, which is only used where the user can input text, such as with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;textarea&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;contenteditable&lt;/code&gt;,
subject to the &lt;a href=&quot;https://html.spec.whatwg.org/multipage/interaction.html#attr-spellcheck&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spellcheck&lt;/code&gt;&lt;/a&gt; attribute (which also affects grammar checking).
For privacy reasons, pages can’t read the active ranges of these highlights, despite being visible to the user.&lt;/p&gt;

&lt;p&gt;::highlight() is controlled via the &lt;a href=&quot;https://drafts.csswg.org/css-highlight-api-1/#highlight&quot;&gt;Highlight&lt;/a&gt; API with &lt;a href=&quot;https://drafts.csswg.org/css-highlight-api-1/#intro-ex&quot;&gt;CSS.highlights&lt;/a&gt;.
CSS.highlights is a &lt;em&gt;maplike&lt;/em&gt; object, which means the interface is the same as a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map&quot;&gt;Map&lt;/a&gt; of strings (highlight names) to Highlight objects.
Highlight objects, in turn, are &lt;em&gt;setlike&lt;/em&gt; objects, which you can use like a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set&quot;&gt;Set&lt;/a&gt; of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Range&quot;&gt;Range&lt;/a&gt; or &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/StaticRange&quot;&gt;StaticRange&lt;/a&gt; objects.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex column_bag&quot;&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 3em;&quot;&gt;
    &lt;span style=&quot;background: yellow;&quot;&gt;Hello&lt;/span&gt;, world!
&lt;/div&gt;
      &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;::highlight&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;yellow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;foo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;CSS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;highlights&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// maplike&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;range&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setStart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;firstChild&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setEnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;firstChild&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// setlike&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;Hello, world!&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;You can use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle&quot;&gt;getComputedStyle()&lt;/a&gt; to query resolved highlight styles under a particular element.
Regardless of which parts (if any) are highlighted, the styles returned are as if the given highlight is active and all other highlights are inactive.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#00FF00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;::highlight&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#FF00FF&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;getSelection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;removeAllRanges&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;getSelection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectAllChildren&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;style&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getComputedStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;::highlight(foo)&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;backgroundColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;Hello, world!&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
    &lt;p&gt;This code always prints “rgb(255, 0, 255)”, even though only ::selection is active.&lt;/p&gt;
  &lt;/figcaption&gt;&lt;/figure&gt;

&lt;h2 id=&quot;how-do-they-work&quot;&gt;How do they work?&lt;/h2&gt;

&lt;p&gt;Highlight pseudos are defined as pseudo-elements, but they actually have very little in common with other pseudo-elements like ::before and ::first-line.&lt;/p&gt;

&lt;p&gt;Unlike other pseudos, they generate &lt;em&gt;highlight overlays&lt;/em&gt;, not boxes, and these overlays are like layers over the original content.
Where text is highlighted, a highlight overlay can add backgrounds and text shadows, while the text proper and any other decorations are “lifted” to the very top.&lt;/p&gt;

&lt;style&gt;@import url(/images/hpdemo.css);&lt;/style&gt;

&lt;script src=&quot;/images/hpdemo.js&quot;&gt;&lt;/script&gt;

&lt;figure&gt;&lt;div class=&quot;_demo _hpdemo&quot; data-_demo=&quot;_hpdemo&quot; style=&quot;--w: var(--inner-width); user-select: none; cursor: pointer;&quot;&gt;
    &lt;script type=&quot;text/x-choreography&quot;&gt;
        q   q   q   q   q   q
        0   1   2   2   3   3
    &lt;/script&gt;
    &lt;div&gt;&lt;main style=&quot;--n: 7;&quot;&gt;
        &lt;div class=&quot;q&quot; style=&quot;outline: 3px dotted #00000070; background: #70700038;&quot;&gt;
            &lt;span&gt;quikc brown&lt;span style=&quot;color: initial;&quot;&gt; fox&lt;/span&gt;&lt;/span&gt;
            &lt;label&gt;originating element&lt;/label&gt;
        &lt;/div&gt;
        &lt;div class=&quot;q&quot; style=&quot;outline: 3px dotted #00000070; background: #A8000038;&quot;&gt;
            &lt;span&gt;&lt;span style=&quot;color: initial; text-decoration: underline; text-decoration-style: wavy; text-decoration-color: red;&quot;&gt;qui&lt;/span&gt;kc brown fox&lt;/span&gt;
            &lt;label&gt;::spelling-error&lt;/label&gt;
        &lt;/div&gt;
        &lt;div class=&quot;q&quot; style=&quot;outline: 3px dotted #00000070; background: #66339938;&quot;&gt;
            &lt;span&gt;quikc &lt;span style=&quot;background: #D070D0C0;&quot;&gt;br&lt;span&gt;own&lt;/span&gt;&lt;/span&gt; fox&lt;/span&gt;
            &lt;label&gt;::target-text&lt;/label&gt;
        &lt;/div&gt;
        &lt;div class=&quot;q&quot;&gt;
            &lt;span&gt;quikc &lt;span&gt;br&lt;span style=&quot;color: initial;&quot;&gt;own&lt;/span&gt;&lt;/span&gt; fox&lt;/span&gt;
        &lt;/div&gt;
        &lt;div class=&quot;q&quot; style=&quot;outline: 3px dotted #00000070; background: #3838C038;&quot;&gt;
            &lt;span&gt;qui&lt;span style=&quot;background: #3838C0C0;&quot;&gt;&lt;span&gt;kc&lt;/span&gt; br&lt;/span&gt;own fox&lt;/span&gt;
            &lt;label&gt;::selection&lt;/label&gt;
        &lt;/div&gt;
        &lt;div class=&quot;q&quot;&gt;
            &lt;span&gt;qui&lt;span style=&quot;color: initial;&quot;&gt;&lt;span style=&quot;text-decoration: underline; text-decoration-style: wavy; text-decoration-color: red;&quot;&gt;kc&lt;/span&gt; br&lt;/span&gt;own fox&lt;/span&gt;
        &lt;/div&gt;
    &lt;/main&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;script&gt;
    const hpdemo = {
        update() {
            const t = this.tFunction();
            if (t == this.t) return;
            this.t = t;
            this.state = _hpdemo(this.state, this.root, this.t);
        },
        tFunction() {
            if (hpdemo.clicked) return 1 - hpdemo.t;
            const rect = this.root.getBoundingClientRect();
            const y = rect.top + (rect.bottom - rect.top) / 2;
            return Number(y &lt; innerHeight / 2);
        },
        state: {},
        root: document.querySelector(&quot;._hpdemo&quot;),
        t: null,
        clicked: false,
    };
    hpdemo.update();
    addEventListener(&quot;scroll&quot;, () =&gt; {
        if (hpdemo.clicked) return;
        hpdemo.update();
    });
    hpdemo.root.addEventListener(&quot;click&quot;, () =&gt; {
        hpdemo.clicked = true;
        hpdemo.update();
    });
&lt;/script&gt;

&lt;p&gt;You can think of highlight pseudos as &lt;em&gt;innermost&lt;/em&gt; pseudo-elements that always exist at the bottom of any tree of elements and other pseudos, but unlike other pseudos, they don’t inherit their styles from that element tree.&lt;/p&gt;

&lt;p&gt;Instead each highlight pseudo forms its own inheritance tree, parallel to the element tree.
This means body::selection inherits from html::selection, not from ‘body’ itself.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;At this point, you can probably see that the highlight pseudos are quite different from the rest of CSS, but there are also several special cases and rules needed to make them a coherent system.&lt;/p&gt;

&lt;p&gt;For the typical appearance of &lt;span class=&quot;_spelling&quot;&gt;spelling&lt;/span&gt; and &lt;span class=&quot;_grammar&quot;&gt;grammar&lt;/span&gt; errors, highlight pseudos need to be able to add their own decorations, and they need to be able to leave the underlying foreground color unchanged.
Highlight inheritance happens separately from the element tree, so we need some way to refer to the underlying foreground color.&lt;/p&gt;

&lt;p&gt;That escape hatch is to set ‘color’ itself to ‘currentColor’, which is the default if nothing in the highlight tree sets ‘color’.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex column_bag&quot;&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 3em;&quot;&gt;
    quick → &lt;span class=&quot;_spelling&quot;&gt;quikc&lt;/span&gt;
    &lt;br /&gt;
    &lt;span style=&quot;color: rebeccapurple;&quot;&gt;quick → &lt;span class=&quot;_spelling&quot;&gt;quikc&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;:root::spelling-error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;/* color: currentColor; */&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;red&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wavy&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;underline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;&lt;/figure&gt;

&lt;aside&gt;
  &lt;p&gt;This is a bit of a special case within a special case.&lt;/p&gt;

  &lt;p&gt;You see, ‘currentColor’ is usually defined as “the computed value of ‘color’”, but the way I like to think of it is “don’t change the foreground color”, and most color-valued properties like ‘text-decoration-color’ default to this value.&lt;/p&gt;

  &lt;p&gt;For ‘color’ itself that wouldn’t make sense, so we instead define ‘color:currentColor’ as equivalent to ‘color:inherit’, which still fits that mental model.
But for highlights, that definition would no longer fit, so we redefine it as being the ‘color’ of the next active highlight below.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;To make highlight inheritance actually useful for &lt;span class=&quot;_spelling&quot;&gt;‘text-decoration’&lt;/span&gt; and &lt;span style=&quot;background: yellow;&quot;&gt;‘background-color’&lt;/span&gt;, &lt;em&gt;all properties are inherited&lt;/em&gt; in highlight styles, even those that are not usually inherited.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex row_bag&quot;&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 3em;&quot;&gt;
    &lt;sup style=&quot;background-color: yellow;&quot;&gt;quick&lt;/sup&gt;&lt;span style=&quot;background-color: yellow;&quot;&gt; fox&lt;/span&gt;
&lt;/div&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;aside&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;yellow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;aside&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;sup&amp;gt;&lt;/span&gt;quick&lt;span class=&quot;nt&quot;&gt;&amp;lt;/sup&amp;gt;&lt;/span&gt; fox
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;&lt;a name=&quot;applicable-properties&quot;&gt;&lt;/a&gt;Only a &lt;a href=&quot;https://drafts.csswg.org/css-pseudo/#highlight-styling&quot;&gt;handful of properties&lt;/a&gt; are settable in highlight styles, for performance and privacy reasons.
In general, properties that you can’t set in highlight styles come from the &lt;em&gt;originating element&lt;/em&gt;, which is to say from the non-highlight styles.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex row_bag&quot;&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 3em; padding-right: 0.5em;&quot;&gt;
    &lt;span style=&quot;text-shadow: 0.25em 0.25lh lightblue;&quot;&gt;A&lt;/span&gt;&lt;big style=&quot;font-size: 2em; line-height: 1.5; text-shadow: 0.25em 0.25lh lightblue;&quot;&gt; A&lt;/big&gt;
&lt;/div&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;:root::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;text-shadow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.25em&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.25&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lh&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;lightblue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;transparent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;big&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2em&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;line-height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;aside&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;sup&amp;gt;&lt;/span&gt;quick&lt;span class=&quot;nt&quot;&gt;&amp;lt;/sup&amp;gt;&lt;/span&gt; fox
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
    &lt;p&gt;When selected, the &amp;lt;big&amp;gt; is still 2em/1, and the selection shadow takes that into account, even though you are not allowed to set ‘font-size’ or ‘line-height’ in :root::selection.&lt;/p&gt;
  &lt;/figcaption&gt;&lt;/figure&gt;

&lt;p&gt;This would conflict with the usual rules&lt;sup id=&quot;fnref:3&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; for decorating boxes, because descendants would get two decorations, one propagated and one inherited.
We resolved this by making decorations added by highlights not propagate to any descendants.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex row_bag&quot;&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 3em;&quot;&gt;
    &lt;div style=&quot;position: relative; color: transparent;&quot;&gt;
        &lt;div style=&quot;position: absolute; bottom: 0; text-decoration: underline; text-decoration-color: blue; text-decoration-thickness: 0.25rem; text-decoration-skip: none; text-decoration-skip-ink: none;&quot;&gt;
            &lt;span style=&quot;font-size: 0.75em;&quot;&gt;quick&lt;/span&gt; fox
        &lt;/div&gt;
        &lt;div style=&quot;position: absolute; bottom: 0; color: CanvasText;&quot;&gt;
            &lt;sup&gt;quick&lt;/sup&gt; fox
        &lt;/div&gt;
        &lt;!-- sizer --&gt;
        &lt;sup&gt;quick&lt;/sup&gt; fox
    &lt;/div&gt;
    &lt;div&gt;
        &lt;sup class=&quot;_spelling&quot;&gt;quikc&lt;/sup&gt; &lt;span class=&quot;_spelling&quot;&gt;fxo&lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;.blue&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;blue&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;underline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;:root::spelling-error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;red&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wavy&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;underline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;blue&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;sup&amp;gt;&lt;/span&gt;quick&lt;span class=&quot;nt&quot;&gt;&amp;lt;/sup&amp;gt;&lt;/span&gt; fox
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;contenteditable&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;spellcheck&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;lang=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;en&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;sup&amp;gt;&lt;/span&gt;quikc&lt;span class=&quot;nt&quot;&gt;&amp;lt;/sup&amp;gt;&lt;/span&gt; fxo
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
    &lt;p&gt;The blue decoration &lt;em&gt;propagates&lt;/em&gt; to the sup element from the decorating box, so there should be a single line at the normal baseline.
On the other hand, the spelling decoration is &lt;em&gt;inherited&lt;/em&gt; by sup::spelling-error, so there should be separate lines for “quikc” and “fxo” at their respective baselines.&lt;/p&gt;
  &lt;/figcaption&gt;&lt;/figure&gt;

&lt;p&gt;Unstyled highlight pseudos generally don’t change the appearance of the original content, so the default ‘color’ and ‘background-color’ in highlights are ‘currentColor’ and ‘transparent’ respectively, the latter being the property’s initial value.
But two highlight pseudos, ::selection and ::target-text, have UA default foreground and background colors.&lt;/p&gt;

&lt;p&gt;For compatibility with ::selection in older browsers, the UA default ‘color’ and ‘background-color’ (e.g. white on blue) is only used if &lt;em&gt;neither&lt;/em&gt; were set by the author.
This rule is known as &lt;em&gt;paired cascade&lt;/em&gt;, and for consistency it also applies to ::target-text.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;div class=&quot;flex&quot;&gt;&lt;table class=&quot;_sum&quot;&gt;
&lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;span style=&quot;color: white; background: var(--cr-highlight);&quot;&gt;default on default&lt;/span&gt;&lt;span style=&quot;color: rebeccapurple;&quot;&gt; plus more text&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;+&lt;/td&gt;&lt;td&gt;
            &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rebeccapurple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;yellow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;            &lt;/div&gt;
          &lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;=&lt;/td&gt;&lt;td&gt;&lt;span style=&quot;color: rebeccapurple; background: yellow;&quot;&gt;currentColor on yellow&lt;/span&gt;&lt;span style=&quot;color: rebeccapurple;&quot;&gt; plus more text&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;It’s common for selected text to almost invert the original text colors, turning &lt;span style=&quot;color: black; background: white;&quot;&gt;black on white&lt;/span&gt; into &lt;span style=&quot;color: white; background: var(--cr-highlight);&quot;&gt;white on blue&lt;/span&gt;, for example.
To guarantee that the original decorations remain as legible as the text when highlighted, which is especially important for decorations with semantic meaning (e.g. &lt;span style=&quot;text-decoration: line-through;&quot;&gt;line-through&lt;/span&gt;), originating decorations are recolored to the highlight ‘color’.
This doesn’t apply to decorations added by highlights though, because that would break the typical appearance of &lt;span class=&quot;_spelling&quot;&gt;spelling&lt;/span&gt; and &lt;span class=&quot;_grammar&quot;&gt;grammar&lt;/span&gt; errors.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex column_bag&quot;&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 3em;&quot;&gt;
    &lt;div&gt;
        do
        &lt;span style=&quot;text-decoration: line-through; text-decoration-color: darkred;&quot;&gt;not&lt;/span&gt;
        buy bread
    &lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: white; background: darkblue;&quot;&gt;
        do
        &lt;span style=&quot;text-decoration: line-through;&quot;&gt;not&lt;/span&gt;
        buy bread
    &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;del&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;darkred&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;line-through&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;white&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;darkblue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    do &lt;span class=&quot;nt&quot;&gt;&amp;lt;del&amp;gt;&lt;/span&gt;not&lt;span class=&quot;nt&quot;&gt;&amp;lt;/del&amp;gt;&lt;/span&gt; buy bread
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
    &lt;p&gt;This line-through decoration becomes white like the rest of the text when selected, even though it was explicitly set to ‘darkred’ in the original content.&lt;/p&gt;
  &lt;/figcaption&gt;&lt;/figure&gt;

&lt;p&gt;The default style rules for highlight pseudos might look something like this.
Notice the new ‘spelling-error’ and ‘grammar-error’ decorations, which authors can use to imitate native spelling and grammar errors.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;:root::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HighlightText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;:root::target-text&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Mark&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MarkText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;:root::spelling-error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spelling-error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;:root::grammar-error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grammar-error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
    &lt;p&gt;This doesn’t completely describe ::selection and ::target-text, due to paired cascade.&lt;/p&gt;
  &lt;/figcaption&gt;&lt;/figure&gt;

&lt;hr /&gt;

&lt;p&gt;The way the highlight pseudos have been designed naturally leads to some limitations.&lt;/p&gt;

&lt;h2 id=&quot;gotchas&quot;&gt;Gotchas&lt;/h2&gt;

&lt;h3 id=&quot;removing-decorations-and-shadows&quot;&gt;Removing decorations and shadows&lt;/h3&gt;

&lt;p&gt;Older browsers with ::selection tend to treat it purely as a way to &lt;em&gt;change&lt;/em&gt; the original content’s styles, including text shadows and other decorations.
&lt;a href=&quot;https://css-tricks.com/almanac/selectors/s/selection/&quot;&gt;Some tutorial content&lt;/a&gt; has even been written to that effect:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;One of the most helpful uses for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::selection&lt;/code&gt; is turning off a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-shadow&lt;/code&gt; during selection.
A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-shadow&lt;/code&gt; can clash with the selection’s background color and make the text difficult to read.
Set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-shadow: none;&lt;/code&gt; to make text clear and easy to read during selection.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Under the spec, highlight pseudos can no longer remove or really change the original content’s decorations and shadows.
Setting these properties in highlight pseudos to values other than ‘none’ &lt;em&gt;adds&lt;/em&gt; decorations and shadows to the overlays when they are active.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;del&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;line-through&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;text-shadow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2px&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2px&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;red&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;::highlight&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;undelete&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;text-shadow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
    &lt;p&gt;This code means that ::highlight(undelete) adds no decorations or shadows, not that it removes the line-through and red shadow when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;del&lt;/code&gt; is highlighted.&lt;/p&gt;
  &lt;/figcaption&gt;&lt;/figure&gt;

&lt;p&gt;While the new :has() selector might appear to offer a solution to this problem, pseudo-element selectors are not allowed in :has(), at least not yet.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;del&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;:has&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::highlight&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;undelete&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;text-shadow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
    &lt;p&gt;This code does not work.&lt;/p&gt;
  &lt;/figcaption&gt;&lt;/figure&gt;

&lt;p&gt;Removing shadows that might clash with highlight backgrounds (as suggested in the tutorial above) will no longer be as necessary anyway, since highlight backgrounds now paint &lt;em&gt;on top of&lt;/em&gt; the original text shadows.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex row_bag&quot;&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 2em; font-weight: bold; padding-right: 0.25em;&quot;&gt;
    &lt;div style=&quot;position: relative; color: transparent;&quot;&gt;
        &lt;div style=&quot;position: absolute; bottom: 0; text-shadow: 0.25em 0.25em magenta;&quot;&gt;
            &lt;span style=&quot;color: white; background: var(--cr-highlight);&quot;&gt;Faultlore&lt;/span&gt;
        &lt;/div&gt;
        &lt;!-- sizer --&gt;
        Faultlore
    &lt;/div&gt;
    &lt;div style=&quot;position: relative; color: transparent;&quot;&gt;
        &lt;div style=&quot;position: absolute; bottom: 0; text-shadow: 0.25em 0.25em magenta;&quot;&gt;
            &lt;span style=&quot;color: white; background: var(--cr-highlight-aC0h);&quot;&gt;Faultlore&lt;/span&gt;
        &lt;/div&gt;
        &lt;!-- sizer --&gt;
        Faultlore
    &lt;/div&gt;
&lt;/div&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;p&gt;&lt;a href=&quot;https://gankra.github.io&quot;&gt;→&lt;/a&gt;&lt;/p&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 2em; font-weight: bold; padding-right: 0.25em;&quot;&gt;
    &lt;div style=&quot;position: relative; color: transparent;&quot;&gt;
        &lt;div style=&quot;position: absolute; bottom: 0; text-shadow: 0.25em 0.25em magenta;&quot;&gt;
            Faultlore
        &lt;/div&gt;
        &lt;div style=&quot;position: absolute; bottom: 0;&quot;&gt;
            &lt;span style=&quot;color: white; background: var(--cr-highlight);&quot;&gt;Faultlore&lt;/span&gt;
        &lt;/div&gt;
        &lt;!-- sizer --&gt;
        Faultlore
    &lt;/div&gt;
    &lt;div style=&quot;position: relative; color: transparent;&quot;&gt;
        &lt;div style=&quot;position: absolute; bottom: 0; text-shadow: 0.25em 0.25em magenta;&quot;&gt;
            Faultlore
        &lt;/div&gt;
        &lt;div style=&quot;position: absolute; bottom: 0;&quot;&gt;
            &lt;span style=&quot;color: white; background: var(--cr-highlight-aC0h);&quot;&gt;Faultlore&lt;/span&gt;
        &lt;/div&gt;
        &lt;!-- sizer --&gt;
        Faultlore
    &lt;/div&gt;
&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;If you still want to ensure those shadows don’t clash with highlights in older browsers, you can set ‘text-shadow’ to ‘none’, which is harmless in newer browsers.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;text-shadow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
    &lt;p&gt;This rule might be helpful for older browsers, but note that like any universal rule, it can interfere with inheritance of ‘text-shadow’ when combined with more specific rules.&lt;/p&gt;
  &lt;/figcaption&gt;&lt;/figure&gt;

&lt;p&gt;As for line decorations, if you’re really determined, you can work around this limitation by using ‘-webkit-text-fill-color’, &lt;a href=&quot;https://compat.spec.whatwg.org/#the-webkit-text-fill-color&quot;&gt;a standard property&lt;/a&gt; (believe it or not) that controls the foreground fill color of text&lt;sup id=&quot;fnref:4&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:4&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;::highlight&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;undelete&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;transparent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;-webkit-text-fill-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CanvasText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
    &lt;p&gt;This hack hides any original decorations (in visual media), because those decorations are recolored to the highlight ‘color’, but it might change the text color too.&lt;/p&gt;
  &lt;/figcaption&gt;&lt;/figure&gt;

&lt;p&gt;Fun fact: because of ‘-webkit-text-fill-color’ and &lt;a href=&quot;https://compat.spec.whatwg.org/#the-webkit-text-stroke&quot;&gt;its stroke-related siblings&lt;/a&gt;, it isn’t always possible for highlight pseudos to avoid changing the foreground colors of text, at least not without out-of-band knowledge of what those colors are.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex column_bag&quot;&gt;
      &lt;div class=&quot;flex column_bag&quot;&gt;
    &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 3em; color: blue;&quot;&gt;
        the
        &lt;em style=&quot;-webkit-text-fill-color: yellow; -webkit-text-stroke: 1px green;&quot;&gt;
            quick
            fox
        &lt;/em&gt;
    &lt;/div&gt;
    &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
    ↓
    &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 3em; color: blue;&quot;&gt;
        the
        &lt;em style=&quot;-webkit-text-fill-color: yellow; -webkit-text-stroke: 1px green;&quot;&gt;
            &lt;span class=&quot;_spelling&quot; style=&quot;-webkit-text-fill-color: currentColor; -webkit-text-stroke: 0 currentColor;&quot;&gt;quikc&lt;/span&gt;
            fox
        &lt;/em&gt;
    &lt;/div&gt;
&lt;/div&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;blue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;em&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;-webkit-text-fill-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;yellow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;-webkit-text-stroke&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1px&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;green&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;:root::spelling-error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;/* default styles */&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;-webkit-text-fill-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;-webkit-text-stroke-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;text-decoration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spelling-error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;em&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::spelling-error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;/* styles needed to preserve text colors */&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;-webkit-text-fill-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;yellow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;-webkit-text-stroke&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1px&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;green&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
    &lt;p&gt;When a word in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;em&lt;/code&gt; is misspelled, it will become blue like the rest of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt;, unless the fill and stroke properties are set in ::spelling-error accordingly.&lt;/p&gt;
  &lt;/figcaption&gt;&lt;/figure&gt;

&lt;h3 id=&quot;accessing-global-constants&quot;&gt;Accessing global constants&lt;/h3&gt;

&lt;details&gt;
  &lt;summary&gt;&lt;strong&gt;Update (2024-04-29):&lt;/strong&gt; this section is no longer true (see &lt;a href=&quot;#custom-properties&quot;&gt;§ &lt;em&gt;Custom properties&lt;/em&gt;&lt;/a&gt;), but you can click here to read what I wrote originally.&lt;/summary&gt;

  &lt;p&gt;&lt;del&gt;Highlight pseudos also don’t automatically have access to custom properties set in the element tree, which can make things tricky if you have a design system that exposes a color palette via custom properties on :root.&lt;/del&gt;&lt;/p&gt;

  &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
      &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;--primary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#420420&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;--secondary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#C0FFEE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;--accent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#663399&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;--accent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;--secondary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;&lt;figcaption&gt;
      &lt;p&gt;This code does not work.&lt;/p&gt;
    &lt;/figcaption&gt;&lt;/figure&gt;

  &lt;p&gt;&lt;del&gt;You can work around this by adding selectors for the necessary highlight pseudos to the rule defining the constants, or if the necessary highlight pseudos are unknown, by rewriting each constant as a custom @property rule.&lt;/del&gt;&lt;/p&gt;

  &lt;figure&gt;
    &lt;div class=&quot;scroll&quot;&gt;
      &lt;div class=&quot;flex&quot;&gt;
        &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;:root&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nd&quot;&gt;:root::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;--primary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#420420&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;--secondary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#C0FFEE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;--accent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#663399&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
        &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
        &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;@property&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;--primary&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;initial-value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#420420&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;syntax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;inherits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;@property&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;--secondary&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;initial-value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#C0FFEE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;syntax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;inherits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;@property&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;--accent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;initial-value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#663399&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;syntax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;inherits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/figure&gt;
&lt;/details&gt;

&lt;h3 id=&quot;custom-properties&quot;&gt;Custom properties&lt;/h3&gt;

&lt;p&gt;You can &lt;em&gt;use&lt;/em&gt; custom properties in highlight styles, but you will not be able to set or override them there.
Custom property values come from the nearest originating element.&lt;/p&gt;

&lt;p&gt;This is unfortunate, but allowing you to set custom properties in highlight styles broke a lot of existing content on the web (and existing advice on Stack Overflow).
For more details, see these posts by my colleague Stephen Chenney:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://blogs.igalia.com/schenney/the-css-highlight-inheritance-model/&quot;&gt;The CSS Highlight Inheritance Model&lt;/a&gt; (January 2024)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blogs.igalia.com/schenney/css-custom-properties-in-highlight-pseudos/&quot;&gt;CSS Custom Properties in Highlight Pseudos&lt;/a&gt; (April 2024)&lt;/li&gt;
  &lt;li&gt;(and the CSSWG issues, &lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6641&quot;&gt;#6641&lt;/a&gt; and &lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/9909&quot;&gt;#9909&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;figure&gt;
  &lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;flex column_bag&quot;&gt;
      &lt;div class=&quot;_example&quot; style=&quot;width: max-content; font-size: 2em; color: black;&quot;&gt;
    &lt;span style=&quot;color: black; background: lightgreen;&quot;&gt;the fox jumps over the dog&lt;/span&gt;
    &lt;div&gt;
        &lt;span style=&quot;color: black; background: yellow;&quot;&gt;(the &lt;/span&gt;&lt;sup style=&quot;color: black; background: yellow;&quot;&gt;quick&lt;/sup&gt;&lt;span style=&quot;color: black; background: yellow;&quot;&gt; fox, mind you)&lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
      &lt;div class=&quot;gap&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;/* = *::selection (universal) */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;/* using --selection-color in ::selection is ok... */&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;--selection-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;/* 🙆‍♀️ */&lt;/span&gt;

        &lt;span class=&quot;c&quot;&gt;/* ...but you will no longer be allowed to set it! */&lt;/span&gt;
        &lt;span class=&quot;py&quot;&gt;--selection-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;red&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;/* 🙅‍♀️ */&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;py&quot;&gt;--selection-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;lightgreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;aside&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;py&quot;&gt;--selection-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;yellow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;the fox jumps over the dog
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;aside&amp;gt;&lt;/span&gt;
        (the &lt;span class=&quot;nt&quot;&gt;&amp;lt;sup&amp;gt;&lt;/span&gt;quick&lt;span class=&quot;nt&quot;&gt;&amp;lt;/sup&amp;gt;&lt;/span&gt; fox, mind you)
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;spec-issues&quot;&gt;Spec issues&lt;/h3&gt;

&lt;p&gt;While the design of the highlight pseudos has mostly settled, there are still some unresolved issues to watch out for.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;how to use spelling and grammar decorations with the UA default colors (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/7522&quot;&gt;#7522&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;values of non-applicable properties, e.g. ‘text-shadow’ with em units (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/7591&quot;&gt;#7591&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;the meaning of underline- and emphasis-related properties in highlights (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/7101&quot;&gt;#7101&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;whether ‘-webkit-text-fill-color’ and friends are allowed in highlights (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/7580&quot;&gt;#7580&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;some browsers “tweak” the colors or alphas set in highlight styles (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6853&quot;&gt;#6853&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;how the highlight pseudos are supposed to interact with SVG (&lt;a href=&quot;https://github.com/w3c/svgwg/issues/894&quot;&gt;svgwg#894&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-now&quot;&gt;What now?&lt;/h2&gt;

&lt;p&gt;The highlight pseudos are a radical departure from older browsers with ::selection, and have some significant differences with CSS as we know it.
Now that we have some experimental support, we want &lt;em&gt;your&lt;/em&gt; help to play around with these features and help us make them as useful and ergonomic as possible before they’re set in stone.&lt;/p&gt;

&lt;p&gt;Special thanks to &lt;a href=&quot;https://twitter.com/regocas&quot;&gt;Rego&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/briankardell&quot;&gt;Brian&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/meyerweb&quot;&gt;Eric&lt;/a&gt; (Igalia), &lt;a href=&quot;https://twitter.com/frivoal&quot;&gt;Florian&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/fantasai&quot;&gt;fantasai&lt;/a&gt; (CSSWG), &lt;a href=&quot;https://twitter.com/ecbos_&quot;&gt;Emilio&lt;/a&gt; (Mozilla), and &lt;a href=&quot;https://twitter.com/dandclark1&quot;&gt;Dan&lt;/a&gt; for their work in shaping the highlight pseudos (and this post).
We would also like to thank &lt;a href=&quot;https://www.bloomberg.com/company/&quot;&gt;Bloomberg&lt;/a&gt; for sponsoring this work.&lt;/p&gt;

&lt;hr /&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Dan, Fernando, Sanket, Luis, Bo, and anyone else I missed. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;See &lt;a href=&quot;https://codepen.io/dazabani13/full/KKqzOJp&quot;&gt;this demo&lt;/a&gt; for more details. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:2:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6829#issuecomment-1098255113&quot;&gt;CSSWG discussion&lt;/a&gt; also found that decorating box semantics are undesirable for decorations added by highlights anyway. &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:4&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;This is actually the case everywhere the WHATWG compat spec applies, at all times. If you think about it, the only reason why setting ‘color’ to ‘red’ makes your text red is because ‘-webkit-text-fill-color’ defaults to ‘currentColor’. &lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;</content><author><name></name></author><category term="home" /><category term="igalia" /><summary type="html">A year and a half ago, I was asked to help upstream a Chromium patch allowing authors to recolor spelling and grammar errors in CSS. At the time, I didn’t realise that this was part of a far more ambitious effort to reimagine spelling errors, grammar errors, text selections, and more as a coherent system that didn’t yet exist as such in any browser. That system is known as the highlight pseudos, and this post will focus on the design of said system and its consequences for authors.</summary></entry><entry><title type="html">Chromium spelling and grammar, part 2</title><link href="https://www.azabani.com/2021/12/16/spelling-grammar-2.html" rel="alternate" type="text/html" title="Chromium spelling and grammar, part 2" /><published>2021-12-16T12:30:00+00:00</published><updated>2021-12-16T12:30:00+00:00</updated><id>https://www.azabani.com/2021/12/16/spelling-grammar-2</id><content type="html" xml:base="https://www.azabani.com/2021/12/16/spelling-grammar-2.html">&lt;p&gt;Modern web browsers can help users with their word processing needs by drawing squiggly lines under possible &lt;span class=&quot;_spelling&quot;&gt;spelling&lt;/span&gt; or &lt;span class=&quot;_grammar&quot;&gt;&lt;span&gt;grammar&lt;/span&gt;&lt;/span&gt; errors in their input.
CSS will give authors more control over when and how they appear, with the new ::spelling- and ::grammar-error pseudo-elements, and spelling- and grammar-error text decorations.
&lt;a href=&quot;/2021/05/17/spelling-grammar.html&quot;&gt;Since part 1&lt;/a&gt; in May, we’ve done a fair bit of work in both Chromium and the CSSWG towards making this possible.&lt;/p&gt;

&lt;style&gt;
article figure &gt; img { max-width: 100%; }
article figure &gt; figcaption { max-width: 30rem; margin-left: auto; margin-right: auto; }
article pre, article code { font-family: Inconsolata, monospace, monospace; }
article blockquote { max-width: 27rem; margin-inline: auto; }
article blockquote &gt; footer { text-align: right; }
article &gt; /* gross and fragile hack */ :not(img):not(hr):not(blockquote):before { width: 13em; display: block; overflow: hidden; content: &quot;&quot;; }
._demo { font-style: italic; font-weight: bold; color: rebeccapurple; }
._spelling, ._grammar { text-decoration-thickness: /* iOS takes 0 literally */ 1px; text-decoration-skip-ink: none; }
._spelling { text-decoration: /* not a shorthand on iOS */ underline; text-decoration-style: wavy; text-decoration-color: red; }
._grammar { text-decoration: /* not a shorthand on iOS */ underline; text-decoration-style: wavy; text-decoration-color: green; }
._table { font-size: 0.75em; }
._table td, ._table th { vertical-align: top; border: 1px solid black; }
._table td:not(._tight), ._table th:not(._tight) { padding: 0.5em; }
._tight picture, ._tight img { vertical-align: top; }
._compare * + *, ._tight * + *, ._gifs * + * { margin-top: 0; }
._compare { max-width: 100%; border: 1px solid rebeccapurple; }
._compare &gt; div { max-width: 100%; position: relative; touch-action: pinch-zoom; --cut: 50%; }
._compare &gt; div &gt; * { vertical-align: top; max-width: 100%; }
._compare &gt; div &gt; :nth-child(1) { position: absolute; clip: rect(auto, auto, auto, var(--cut)); }
._compare &gt; div &gt; :nth-child(2) { position: absolute; width: var(--cut); height: 100%; border-right: 1px solid rebeccapurple; }
._compare &gt; div &gt; :nth-child(2):before { content: var(--left-label); color: rebeccapurple; font-size: 0.75em; position: absolute; right: 0.5em; }
._compare &gt; div &gt; :nth-child(2):after { content: var(--right-label); color: rebeccapurple; font-size: 0.75em; position: absolute; left: calc(100% + 0.5em); }
._sum td:first-of-type { padding-right: 1em; }
._gifs { position: relative; display: flex; flex-flow: column nowrap; }
._gifs &gt; video { transition: opacity 0.125s linear; }
._gifs &gt; button { transition: 0.125s linear; transition-property: color, background-color; }
._gifs._paused &gt; video { opacity: 0.5; }
._gifs._paused &gt; button { color: rebeccapurple; background: #66339940; }
._gifs &gt; button { position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; font-size: 7em; color: transparent; background: transparent; content: &quot;▶&quot;; }
._gifs &gt; button:focus-visible { outline: 0.25rem solid #663399C0; outline-offset: -0.25rem; }

._commits { position: relative; }
._commits &gt; :first-child { position: absolute; right: -0.1em; height: 100%; border-right: 0.2em solid rgba(102,51,153,0.5); }
._commits &gt; :last-child { position: relative; padding-right: 0.5em; }
* + ._commit, ._commit * + * { margin-top: 0; }
._commit { line-height: 2; margin-right: -1.5em; text-align: right; }
._commit &gt; img { width: 2em; vertical-align: middle; }
._commit &gt; a { padding-right: 0.5em; text-decoration: none; color: rebeccapurple; }
._commit &gt; a &gt; code { font-size: 1em; }
._commit-none &gt; a { color: rgba(102,51,153,0.5); }
&lt;/style&gt;

&lt;p&gt;The client funding this work had an internal patch that allowed you to change the colors of those squiggly lines, and our job was to upstream it.
The patch itself was pretty simple, but turning that into an upstream feature is a much bigger can of worms.
So far, we’ve landed over 30 patches, including dozens of new web platform tests, opened 8 spec issues, and run into some gnarly bugs going back to at least 2009.&lt;/p&gt;

&lt;p&gt;Check out &lt;a href=&quot;https://bucket.daz.cat/work/igalia/0/&quot;&gt;our project index&lt;/a&gt; for a complete list of demos, tests, patches, and issues.
For more details about the CSS highlight pseudos in particular, check out &lt;a href=&quot;https://www.youtube.com/watch?v=Vh2niGIqtOc&quot;&gt;my BlinkOn 15 talk&lt;/a&gt;, including the &lt;a href=&quot;https://bucket.daz.cat/work/igalia/0/29.html&quot;&gt;highlight painting visualiser&lt;/a&gt;.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;div class=&quot;flex&quot;&gt;
    &lt;iframe class=&quot;local-video&quot; width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube-nocookie.com/embed/Vh2niGIqtOc&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;&lt;/div&gt;&lt;figcaption&gt;
    (&lt;a class=&quot;_demo&quot; href=&quot;https://www.azabani.com/talks/2021-11-17-css-highlight-pseudos/&quot;&gt;slides&lt;/a&gt;)
&lt;/figcaption&gt;&lt;/figure&gt;

&lt;h2 id=&quot;contents&quot;&gt;Contents&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#impl-status&quot;&gt;Implementation status&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#charlie&quot;&gt;Charlie’s lawyerings&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#squiggly-lines&quot;&gt;Squiggly lines&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#platform-conventions&quot;&gt;Platform “conventions”&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#precise-wavy-decorations&quot;&gt;Precise wavy decorations&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#phase-locked-decorations&quot;&gt;Phase-locked decorations&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#bézier-bounding-box&quot;&gt;Bézier bounding box&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#cover-me&quot;&gt;Cover me!&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#highlight-inheritance&quot;&gt;Highlight inheritance&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#blink-style-101&quot;&gt;Blink style 101&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#blink-style-102&quot;&gt;How pseudo-elements work&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#status-quo&quot;&gt;Status quo&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#storing-highlight-styles&quot;&gt;Storing highlight styles&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#single-pass-resolution&quot;&gt;Single-pass resolution&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#multi-pass-resolution&quot;&gt;Multi-pass resolution&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#pathology-in-legacy&quot;&gt;Pathology in legacy&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#paired-cascade&quot;&gt;Paired cascade&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#fixing-tests&quot;&gt;Who’s got green?&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#what-now&quot;&gt;What now?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;impl-status&quot;&gt;Implementation status&lt;/h2&gt;

&lt;p&gt;Chromium 96 includes a rudimentary version of highlight inheritance, with support for ::highlight in Chromium 98 (&lt;a href=&quot;https://crrev.com/c/3237158&quot;&gt;Fernando Fiori&lt;/a&gt;).
This is currently behind a Blink feature:&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;--enable-blink-features=HighlightInheritance
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;Adding to our initial support for ::{spelling,grammar}-error, we’ve since made progress on the new {spelling,grammar}-error decorations.
While they are accepted but ignored in Chromium 96, you’ll be able to &lt;em&gt;see&lt;/em&gt; them in Chromium 98, with our early paint support.&lt;/p&gt;

&lt;p&gt;Chromium 96 also makes it possible to change the color of native squiggly lines by setting ‘text-decoration-color’ on either of the new pseudo-elements.
This feature, and the features above, are behind another flag:&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;--enable-blink-features=CSSSpellingGrammarErrors
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;/figure&gt;

&lt;h2 id=&quot;charlie&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=qcderLXiwa8&quot;&gt;C&lt;/a&gt;harlie’s &lt;del&gt;bird&lt;/del&gt; spec lawyerings&lt;/h2&gt;

&lt;p&gt;I’ve learned a lot of things while working on this project.
One interesting lesson was that no matter how clearly a feature is specified, and how much discussion goes into spec details, half the questions won’t become apparent until someone starts building it.&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;300&quot; height=&quot;300&quot; src=&quot;/images/spammar2-charlie.jpg&quot; class=&quot;flight&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;What happens when both highlight and originating content define text shadows? What if multiple highlights do the same? What order do we paint these shadows in? (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/3932&quot;&gt;#3932&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;What happens to the originating content’s decorations when highlighted? What happens when highlights define their own decorations? Which decorations get recolored to the foreground color for clarity? What’s the painting order? Does it even mean anything for a highlight to set ‘text-decoration-color’ only? (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6022&quot;&gt;#6022&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Some browsers invert the author’s ::selection background based on contrast with the foreground color. Should this be allowed, or does it do more harm than good? (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6150&quot;&gt;#6150&lt;/a&gt;)
    &lt;ul&gt;
      &lt;li&gt;What about other “tweaks”? What if a browser needs to force translucency to make its selection highlighting &lt;em&gt;work&lt;/em&gt;? (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6853&quot;&gt;#6853&lt;/a&gt;)&lt;/li&gt;
      &lt;li&gt;How do we even write reftests if they &lt;em&gt;are&lt;/em&gt; allowed? (no issue)&lt;/li&gt;
      &lt;li&gt;While we’re talking about testing, how do we even test ::{spelling,grammar}-error without a way to guarantee that some text is treated as an error? (&lt;a href=&quot;https://github.com/web-platform-tests/wpt/issues/30863&quot;&gt;wpt#30863&lt;/a&gt;)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;How does paired cascade work? Does “use” mean used value? Which properties are “highlight colors”? Do we really mean ::selection only, and color and background-color only? What does it mean for a highlight color to have been “specified by the author”? Does the user origin stylesheet count as “specified”? Do unset and revert count as “specified”? Does unset mean inherit even when the property is not normally inherited? (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6386&quot;&gt;#6386&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Should custom properties be allowed? What about variable references? Do we force non-inherited custom properties to become inherited like we do for non-custom properties? Should we provide a better way to set custom properties in a way that affects highlight pseudos? (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6264&quot;&gt;#6264&lt;/a&gt;, &lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6641&quot;&gt;#6641&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;What if existing content relies on implicitly inheriting a highlight foreground color when setting background-color explicitly, or vice versa? Do we need to accommodate this for compat? (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6774&quot;&gt;#6774&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;The spec effectively recommends that ::{spelling,grammar}-error (and &lt;em&gt;requires&lt;/em&gt; that ::highlight) force the text color to black by default. Surely we want to &lt;em&gt;not change&lt;/em&gt; the color by default? (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6779&quot;&gt;#6779&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Does color:currentColor point to the next &lt;em&gt;active&lt;/em&gt; highlight overlay below, or are inactive highlights included too? What happens when the author tries to getComputedStyle with ::selection? (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6818&quot;&gt;#6818&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Do decorations “propagate” to descendants in highlights like they would normally? How do we reconcile that with highlight inheritance? How do we ensure that “decorating box” semantics aren’t broken? (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6829&quot;&gt;#6829&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;squiggly-lines&quot;&gt;Squiggly lines&lt;/h2&gt;

&lt;div class=&quot;_commits&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;

    &lt;p&gt;Since landing ‘text-decoration-color’ support for the new pseudos, my colleague Rego has taken the lead on the rest of the core spelling and grammar features, starting with the new ‘text-decoration-line’ values.&lt;/p&gt;

    &lt;p&gt;Currently, when setting ‘text-decoration-color’ on the pseudos, we change the color, but ‘text-decoration-line’ is still ‘none’, which doesn’t really make sense.
This might sound like it required gross hacks, but the style system just gives us a blob of properties, where ‘color’ and ‘line’ are independent.
All of the logic that &lt;em&gt;uses&lt;/em&gt; them is in paint and layout.&lt;/p&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3162169&quot;&gt;&lt;code&gt;CL:3162169&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;p&gt;We started by adding the new values to the stylesheet parser.
While highlight painting still needs a lot more work before we can do so, the idea is that eventually the pseudos and decorations will meet in the default stylesheet.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;::spelling-error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;text-decoration-line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spelling-error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;::grammar-error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;text-decoration-line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grammar-error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3194336&quot;&gt;&lt;code&gt;CL:3194336&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;p&gt;Something that’s often neglected in CSS tests is &lt;em&gt;dynamic&lt;/em&gt; testing, which checks that the rendering updates correctly when styles are changed by JavaScript, since the easiest and most common way to write a rendering test involves no scripting at all.&lt;/p&gt;

    &lt;p&gt;In this case, only ::selection had dynamic tests, and only ::selection actually worked correctly, so we then fixed the other pseudos.&lt;/p&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3177663&quot;&gt;&lt;code&gt;CL:3177663&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;h3 id=&quot;platform-conventions&quot;&gt;Platform “conventions”&lt;/h3&gt;

    &lt;p&gt;Blink’s squiggly lines look quite different to anything CSS can achieve with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wavy&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dotted&lt;/code&gt; decorations, and they’re painted on unrelated codepaths (&lt;a href=&quot;/2021/05/17/spelling-grammar.html#cjk-css-unification&quot;&gt;more details&lt;/a&gt;).
We want to unify these codepaths, to make them easier to maintain and help us integrate them with CSS, but this creates a few complications.&lt;/p&gt;

    &lt;p&gt;The CSS codepath naïvely paints as many bézier curves as needed to span the necessary width, but the squiggly codepath has always painted a single rectangle with a cached texture, which is probably more efficient.
This texture used to be a hardcoded bitmap, but even when we made the decorations &lt;a href=&quot;https://codereview.chromium.org/2674003002&quot;&gt;scale with the user’s dpi&lt;/a&gt;, we still kept the same technique, so the approach we use for CSS decorations might be too slow.&lt;/p&gt;

    &lt;p&gt;Another question is the actual appearance of spelling and grammar decorations.
We don’t necessarily want to make them &lt;em&gt;identical&lt;/em&gt; to the default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wavy&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dotted&lt;/code&gt; decorations, because it might be nice to tell when, say, a wavy-decorated word is misspelled.&lt;/p&gt;

    &lt;p&gt;We also want to conform to platform conventions where possible, and you would think there’s at least a consistent convention for macOS… but not exactly.
One thing that’s clear is that gradients are no longer conventional.&lt;/p&gt;

    &lt;figure style=&quot;image-rendering: pixelated;&quot;&gt;
&lt;div class=&quot;scroll&quot;&gt;
&lt;table class=&quot;_table&quot;&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th colspan=&quot;4&quot;&gt;macOS (compare &lt;a class=&quot;_demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/0.html?color=red&amp;amp;style=dotted&amp;amp;line=underline&amp;amp;thickness=3px&amp;amp;ink=none&quot;&gt;demo&lt;sub&gt;0&lt;/sub&gt;&lt;/a&gt;)&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td class=&quot;_tight&quot; style=&quot;vertical-align: bottom;&quot;&gt;&lt;a href=&quot;/images/spammar2-safari.png&quot;&gt;&lt;img width=&quot;170&quot; height=&quot;90&quot; src=&quot;/images/spammar2-safari.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
            &lt;td class=&quot;_tight&quot; style=&quot;vertical-align: bottom;&quot;&gt;&lt;a href=&quot;/images/spammar2-notes.png&quot;&gt;&lt;img width=&quot;90&quot; height=&quot;39&quot; src=&quot;/images/spammar2-notes.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
            &lt;td class=&quot;_tight&quot; style=&quot;vertical-align: bottom;&quot;&gt;&lt;a href=&quot;/images/spammar2-textedit.png&quot;&gt;&lt;img width=&quot;53&quot; height=&quot;28&quot; src=&quot;/images/spammar2-textedit.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
            &lt;td class=&quot;_tight&quot; style=&quot;vertical-align: bottom;&quot;&gt;&lt;a href=&quot;/images/spammar2-keynote.png&quot;&gt;&lt;img width=&quot;96&quot; height=&quot;39&quot; src=&quot;/images/spammar2-keynote.png&quot; /&gt;&lt;br /&gt;&lt;img width=&quot;96&quot; height=&quot;39&quot; src=&quot;/images/spammar2-keynote@t.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
        &lt;tr&gt;&lt;th&gt;Safari&lt;/th&gt;&lt;th&gt;Notes&lt;/th&gt;&lt;th&gt;TextEdit&lt;/th&gt;&lt;th&gt;Keynote&lt;/th&gt;&lt;/tr&gt;
    &lt;/tfoot&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/figure&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3139819&quot;&gt;&lt;code&gt;CL:3139819&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-up.svg&quot; /&gt;&lt;/div&gt;

    &lt;p&gt;But anyway, if we’re adding new decoration values that mimic the native ones, which codepath do we paint them with?
We decided to go down the CSS route — leaving native squiggly lines untouched for now — and take this time to refactor and extend those decoration painters for the needs of spelling and grammar errors.&lt;/p&gt;

    &lt;div class=&quot;_commit _commit-none&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3275457&quot;&gt;&lt;code&gt;CL:3275457&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-none.svg&quot; /&gt;&lt;/div&gt;

    &lt;div class=&quot;_commit _commit-none&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3284869&quot;&gt;&lt;code&gt;CL:3284869&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-none.svg&quot; /&gt;&lt;/div&gt;

    &lt;div class=&quot;_commit _commit-none&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3290417&quot;&gt;&lt;code&gt;CL:3290417&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-none.svg&quot; /&gt;&lt;/div&gt;

    &lt;div class=&quot;_commit _commit-none&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3291658&quot;&gt;&lt;code&gt;CL:3291658&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-none.svg&quot; /&gt;&lt;/div&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3297885&quot;&gt;&lt;code&gt;CL:3297885&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;h3 id=&quot;precise-wavy-decorations&quot;&gt;Precise wavy decorations&lt;/h3&gt;

    &lt;p&gt;To that end, one of the biggest improvements we’ve landed is making wavy decorations start and stop exactly where needed, rather than falling short.
This includes the new spelling and grammar decoration values, other than on macOS.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;div class=&quot;flex&quot; style=&quot;flex-direction: column;&quot;&gt;&lt;div class=&quot;_gifs _paused&quot;&gt;
    &lt;!-- ffmpeg -y -video_size 384x216 -framerate 60 -f x11grab -i :0+15,307 \%03d.png --&gt;
    &lt;!-- convert -delay 2 *.png -layers Optimize +map foo.gif --&gt;
    &lt;!-- # can skip gif and 50/60 fps conversion with ffmpeg -pattern_type glob --&gt;
    &lt;!-- ( i=images/foo; ffmpeg -y -i $i.gif -vf &apos;setpts=50/60*PTS&apos; -r 60 -pix_fmt yuv420p -vcodec libx264 -crf 17 $i.mp4 ) --&gt;
    &lt;!-- ( i=images/foo; ffmpeg -y -i $i.gif -vf &apos;setpts=50/60*PTS&apos; -r 60 -pix_fmt yuv420p -vcodec libvpx -crf 10 -b:v 1M $i.webm ) --&gt;
    &lt;!-- &lt;img width=&quot;384&quot; height=&quot;216&quot; src=&quot;/images/spammar2-w0.gif&quot;&gt; --&gt;
    &lt;!-- &lt;img width=&quot;384&quot; height=&quot;216&quot; src=&quot;/images/spammar2-w1.gif&quot;&gt; --&gt;
    &lt;video loop=&quot;&quot; playsinline=&quot;&quot; tabindex=&quot;-1&quot; width=&quot;384&quot; height=&quot;216&quot; poster=&quot;/images/spammar2-w0.png&quot;&gt;&lt;source src=&quot;/images/spammar2-w0.mp4&quot; /&gt;&lt;source src=&quot;/images/spammar2-w0.webm&quot; /&gt;&lt;/video&gt;
    &lt;video loop=&quot;&quot; playsinline=&quot;&quot; tabindex=&quot;-1&quot; width=&quot;384&quot; height=&quot;216&quot; poster=&quot;/images/spammar2-w1.png&quot;&gt;&lt;source src=&quot;/images/spammar2-w1.mp4&quot; /&gt;&lt;source src=&quot;/images/spammar2-w1.webm&quot; /&gt;&lt;/video&gt;
    &lt;button type=&quot;button&quot; aria-label=&quot;play&quot;&gt;▶&lt;/button&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;figcaption&gt;
    Wavy decorations under ‘letter-spacing’, top version 96, bottom version 97 (&lt;a class=&quot;_demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/0.html?color=%2300C000&amp;amp;style=wavy&amp;amp;line=underline&amp;amp;thickness=auto&amp;amp;ink=none&amp;amp;trySpellcheck=1&amp;amp;wm=horizontal-tb&amp;amp;marquee&amp;amp;overlay&quot;&gt;&lt;strong&gt;demo&lt;sub&gt;0&lt;/sub&gt;&lt;/strong&gt;&lt;/a&gt;).
&lt;/figcaption&gt;&lt;/figure&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3237072&quot;&gt;&lt;code&gt;CL:3237072&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;div class=&quot;_commit _commit-none&quot;&gt;&lt;a href=&quot;https://crrev.com/c/3264203&quot;&gt;&lt;code&gt;CL:3264203&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-none.svg&quot; /&gt;&lt;/div&gt;

    &lt;p&gt;You may have noticed that the decorations in that last example sometimes extend to the right of “h”.
This is working as expected: ‘letter-spacing’ adds a space &lt;em&gt;after&lt;/em&gt; letters, not &lt;em&gt;between&lt;/em&gt; them, &lt;a href=&quot;https://www.w3.org/TR/css-text-3/#letter-spacing-property&quot;&gt;even though it &lt;span style=&quot;font-variant: small-caps;&quot;&gt;Really Should Not&lt;/span&gt;&lt;/a&gt;.
I tried wrapping the last letter of each word in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;span&lt;/code&gt;, but then the letter appears to have its own decoration, out of phase with the rest of the word.
This is because Blink lacks &lt;em&gt;phase-locked decorations&lt;/em&gt;.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;div class=&quot;flex&quot;&gt;&lt;div class=&quot;_gifs _paused&quot;&gt;
    &lt;video loop=&quot;&quot; playsinline=&quot;&quot; tabindex=&quot;-1&quot; width=&quot;384&quot; height=&quot;216&quot; poster=&quot;/images/spammar2-w4.png&quot;&gt;&lt;source src=&quot;/images/spammar2-w4.mp4&quot; /&gt;&lt;source src=&quot;/images/spammar2-w4.webm&quot; /&gt;&lt;/video&gt;
    &lt;button type=&quot;button&quot; aria-label=&quot;play&quot;&gt;▶&lt;/button&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;

  &lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;phase-locked-decorations&quot;&gt;Phase-locked decorations&lt;/h2&gt;

&lt;p&gt;Blink uses an inheritance hack to propagate decorations from parents to children, rather than properly implementing the concept of &lt;a href=&quot;https://www.w3.org/TR/2019/CR-css-text-decor-3-20190813/#line-decoration&quot;&gt;&lt;em&gt;decorating box&lt;/em&gt;&lt;/a&gt;.
In other words, we paint two independent decorations, whereas we &lt;em&gt;should&lt;/em&gt; paint one decoration that spans the entire word.
This has been the cause of &lt;a href=&quot;https://github.com/web-platform-tests/interop-2022/issues/23&quot;&gt;a lot of bugs&lt;/a&gt;, and is widely regarded as a bad move.&lt;/p&gt;

&lt;p&gt;Note that we don’t actually have to paint the decoration in a single pass, we only have to render &lt;em&gt;as if&lt;/em&gt; that was the case.
For example, when testing the same change in Firefox, the decoration appears to jitter near the last letter, which suggests that the decoration is probably being painted separately for that element.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;div class=&quot;flex&quot;&gt;&lt;div class=&quot;_gifs _paused&quot;&gt;
    &lt;video loop=&quot;&quot; playsinline=&quot;&quot; tabindex=&quot;-1&quot; width=&quot;384&quot; height=&quot;216&quot; poster=&quot;/images/spammar2-w5.png&quot;&gt;&lt;source src=&quot;/images/spammar2-w5.mp4&quot; /&gt;&lt;source src=&quot;/images/spammar2-w5.webm&quot; /&gt;&lt;/video&gt;
    &lt;button type=&quot;button&quot; aria-label=&quot;play&quot;&gt;▶&lt;/button&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;Gecko goes above and beyond with this, even synchronising &lt;em&gt;separate&lt;/em&gt; decorations introduced under the same block, which allows authors to make it look like their decorations change color partway through.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;img width=&quot;300&quot; height=&quot;90&quot; src=&quot;/images/spammar2-phase0.png&quot; srcset=&quot;/images/spammar2-phase0.png 2x&quot; /&gt;
    &lt;img width=&quot;256&quot; height=&quot;90&quot; src=&quot;/images/spammar2-phase1.png&quot; srcset=&quot;/images/spammar2-phase1.png 2x&quot; /&gt;
&lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;A related problem in the highlight painting space is that the spec calls for “recoloring” originating decorations to the highlight foreground color.
By making these decorations “lose their color”, we avoid situations where a decoration becomes illegible when highlighted, despite being legible in its original context.&lt;/p&gt;

&lt;p&gt;I’ve &lt;a href=&quot;https://crrev.com/c/2903387&quot;&gt;partially implemented&lt;/a&gt; this for ::selection in Chromium 95, by adding a special case that splits originating decorations into two clipped paints with different colors — though not yet the &lt;em&gt;correct&lt;/em&gt; colors — while carefully keeping them in phase.&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;_compare&quot; style=&quot;--left-label: &apos;actual&apos;; --right-label: &apos;ref3&apos;; width: 275px; margin: 0 auto;&quot;&gt;&lt;img width=&quot;275&quot; height=&quot;150&quot; src=&quot;/images/spammar2-split0.png&quot; /&gt;&lt;img width=&quot;275&quot; height=&quot;150&quot; src=&quot;/images/spammar2-split1.png&quot; /&gt;&lt;/div&gt;
&lt;figcaption&gt;
    &lt;p&gt;&lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-004.html&quot;&gt;highlight-painting-004&lt;/a&gt; and &lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-004-ref3.html&quot;&gt;-ref3&lt;/a&gt;, version 97. In this test, the originating element has a red underline, while ::selection introduces a purple line-through. The underline needs to become blue in the highlighted part, to match the ::selection ‘color’, but for now, we match its ‘text-decoration-color’.&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;To paint the highlighted part of the decoration, we clip the canvas to a rectangle as wide as the background, and paint the decoration in full.
To paint the rest, we clip “out” the canvas to the same rectangle, which means we don’t touch anything &lt;em&gt;inside&lt;/em&gt; the rectangle.&lt;/p&gt;

&lt;p&gt;But how &lt;em&gt;tall&lt;/em&gt; should that rectangle be? Short answer: infinity.&lt;/p&gt;

&lt;h3 id=&quot;bézier-bounding-box&quot;&gt;Bézier bounding box&lt;/h3&gt;

&lt;p&gt;Long answer: Skia doesn’t let us clip to an infinitely tall rectangle, so it depends on several things, including ‘text-decoration-thickness’, ‘text-underline-offset’, and in the case of wavy decorations, the amplitude of the bézier curves.&lt;/p&gt;

&lt;p&gt;In the code, there was a pretty diagram that illustrated the four relevant points to each “wave” repeated in the decoration.
Clearly, it suggested that the pattern &lt;em&gt;in that example&lt;/em&gt; was bounded by the control points, but I had no idea whether this was true for &lt;em&gt;all&lt;/em&gt; cubic béziers, my terrible search engine skills failed me again, and I don’t like assuming.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/*                   controlPoint1
 *                         +
 *
 *
 *                  . .
 *                .     .
 *              .         .
 * (x1, y1) p1 +           .            + p2 (x2, y2)
 *                          .         .
 *                            .     .
 *                              . .
 *
 *
 *                         +
 *                   controlPoint2
 */
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;To avoid getting stuck on those questions for too long, and because I genuinely didn’t know how to determine the amplitude of a bézier curve, I went with three times the background height.
This should be Good Enough™ for most content, but you can easily break it with, say, a very large ‘text-underline-offset’.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;div class=&quot;flex&quot;&gt;
    &lt;img width=&quot;275&quot; height=&quot;200&quot; src=&quot;/images/spammar2-clip.png&quot; srcset=&quot;/images/spammar2-clip.png 2x&quot; /&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;Weeks later, I stumbled upon a video by Freya Holmér &lt;a href=&quot;https://www.youtube.com/watch?v=aVwxzDHniEw&amp;amp;t=665&quot;&gt;answering that very question&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;So, how do we get [the bounding] box?&lt;/p&gt;

  &lt;p&gt;&lt;strong&gt;The naïve solution is to simply use the control points of the bézier curve.&lt;/strong&gt; This can be good enough, but what we &lt;em&gt;really&lt;/em&gt; want is the “tight bounding box”; in some cases, the difference between the two is &lt;em&gt;huge&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For now, the code still clips to a fixed three times the background height, but at least we now have some ideas for how to properly measure these decorations:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;use the minimum and maximum &lt;em&gt;y&lt;/em&gt; values of the control points (naïve)&lt;/li&gt;
  &lt;li&gt;find &lt;em&gt;better&lt;/em&gt; min and max &lt;em&gt;y&lt;/em&gt; values by evaluating the derivative at its zeros&lt;/li&gt;
  &lt;li&gt;use a dedicated function for this purpose like SkDCubic::convexHull?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;cover-me&quot;&gt;Cover me!&lt;/h3&gt;

&lt;p&gt;Writing the reference pages for that test was also a fun challenge.
When written the obvious way, Blink would actually fail, because in general we make no attempt to keep &lt;em&gt;any&lt;/em&gt; decoration paints in phase.&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;_compare&quot; style=&quot;--left-label: &apos;ref1&apos;; --right-label: &apos;ref3&apos;; width: 275px; margin: 0 auto;&quot;&gt;&lt;img width=&quot;275&quot; height=&quot;150&quot; src=&quot;/images/spammar2-split2.png&quot; /&gt;&lt;img width=&quot;275&quot; height=&quot;150&quot; src=&quot;/images/spammar2-split3.png&quot; /&gt;&lt;/div&gt;
&lt;figcaption&gt;
    &lt;p&gt;&lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-004-ref1.html&quot;&gt;highlight-painting-004-ref1&lt;/a&gt; and &lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-004-ref3.html&quot;&gt;-ref3&lt;/a&gt;, version 96.&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The ref that Blink ended up matching has five layers.
Each layer contains the word “quick” in full with any decorations spanning the whole word, but only part of the layer is shown.
This is achieved by an elaborate system of positioned “covers” and “hiders”: the former clips a layer from the right with a white rectangle, while the latter clips a layer from the left by way of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right:0&lt;/code&gt; wrapped in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;overflow:hidden&lt;/code&gt;.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;div class=&quot;flex&quot;&gt;
    &lt;a href=&quot;/images/spammar2-ref.jpg&quot;&gt;&lt;img width=&quot;432&quot; height=&quot;256&quot; src=&quot;/images/spammar2-ref.jpg&quot; srcset=&quot;/images/spammar2-ref.jpg 2x&quot; /&gt;&lt;/a&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;Wanna know the best part though?
&lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-004-ref1.html&quot;&gt;All&lt;/a&gt; &lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-004-ref2.html&quot;&gt;three&lt;/a&gt; &lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-004-ref3.html&quot;&gt;refs&lt;/a&gt; are identical in Firefox.
Someday, hopefully, this will also be true for Blink.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;highlight-inheritance&quot;&gt;Highlight inheritance&lt;/h2&gt;

&lt;p&gt;Presto (Opera), uniquely, supported inheritance for ::selection before it was cool, by mapping those styles to synthesised (internal) ‘selection-color’ and ‘selection-background’ properties that were marked as inherited.&lt;/p&gt;

&lt;p&gt;Blink also has internal properties for things like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/:visited&quot;&gt;:visited links&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors&quot;&gt;forced colors&lt;/a&gt;, where we need to keep track of both “original” and “new” colors.
This works well enough, but internal properties add a great deal of complexity to the code that applies and consumes styles.
Now that there are multiple highlight pseudos, supporting a lot more than just ‘color’ and ‘background-color’, this complexity is hard to justify.&lt;/p&gt;

&lt;p&gt;To understand the approach we went with, let’s look at how CSS works in Chromium.&lt;/p&gt;

&lt;h3 id=&quot;blink-style-101&quot;&gt;Blink style 101&lt;/h3&gt;

&lt;p&gt;CSS is managed by Blink’s style system, which at its highest level consists of the &lt;em&gt;engine&lt;/em&gt;, the &lt;em&gt;resolver&lt;/em&gt;, and the &lt;em&gt;ComputedStyle&lt;/em&gt; data structure.
The engine maintains all of the style-related state for a document, including all of its stylesheet rules &lt;em&gt;and&lt;/em&gt; the information needed to recalculate styles efficiently when the document changes.
The resolver’s job is to calculate styles for some element, writing the results to a new ComputedStyle object.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;img width=&quot;407&quot; height=&quot;167&quot; src=&quot;/images/spammar2-x0.png&quot; srcset=&quot;/images/spammar2-x0.png 2x&quot; /&gt;
&lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;ComputedStyle itself is also interesting.
Blink recognises over 600 properties, including internal properties, shorthands (like ‘margin’), and aliases (like ‘-webkit-transform’), so most of the fields and methods are actually generated (ComputedStyleBase) with the help of some Python scripts.&lt;/p&gt;

&lt;p&gt;These fields are “sharded” into &lt;em&gt;field groups&lt;/em&gt;, so we can &lt;a href=&quot;https://en.wikipedia.org/wiki/Copy-on-write&quot;&gt;efficiently reuse&lt;/a&gt; style data from ancestors and previous resolver outputs.
Some of these field groups are human-defined, like “surround” for all of the margin/border/padding properties, but there are also several &lt;em&gt;raredata&lt;/em&gt; groups generated from property popularity stats.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;img width=&quot;587&quot; height=&quot;293&quot; src=&quot;/images/spammar2-x1.png&quot; srcset=&quot;/images/spammar2-x1.png 2x&quot; /&gt;
&lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;When resolving styles, we usually clone an &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/initial_value&quot;&gt;“empty”&lt;/a&gt; ComputedStyle, then we copy over the inherited properties from the parent to this fresh new object.
Many of these live in the “inherited” field group, so all we need to do for them is copy a single pointer.
At this point, we have the parent’s inherited properties, and everything else as initial values, so if the element doesn’t have any rules of its own, we’re more or less done.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;img width=&quot;527&quot; height=&quot;334&quot; src=&quot;/images/spammar2-x2.png&quot; srcset=&quot;/images/spammar2-x2.png 2x&quot; /&gt;
&lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;Otherwise, we search for matching rules, &lt;a href=&quot;https://www.w3.org/TR/css-cascade-4/#cascading&quot;&gt;sort all of their declarations&lt;/a&gt; by things like specificity, then &lt;em&gt;apply&lt;/em&gt; the winning declarations by overwriting various ComputedStyle fields.
If the field we’re overwriting is in a field group, we need to clone the field group too, to avoid clobbering someone else’s styles.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;img width=&quot;527&quot; height=&quot;190&quot; src=&quot;/images/spammar2-x3.png&quot; srcset=&quot;/images/spammar2-x3.png 2x&quot; /&gt;
&lt;/div&gt;&lt;/figure&gt;

&lt;h3 id=&quot;blink-style-102&quot;&gt;Blink style 102: pseudo-elements&lt;/h3&gt;

&lt;p&gt;For ordinary elements, as well as pseudo-elements with a clear place in the DOM tree (e.g. ::before, ::marker), we resolve styles as part of &lt;em&gt;style&lt;/em&gt;’s regular tree traversal.
We start by updating :root’s styles, then any children affected by the update, and so on.
But for other pseudos we usually use a “lazy” approach, where we don’t bother resolving styles unless they are needed by a later phase of the rendering process, like layout or paint.&lt;/p&gt;

&lt;p&gt;Let’s say we’re resolving styles for some ordinary element.
When we’re searching for matching rules, if we find one that &lt;em&gt;actually&lt;/em&gt; matches our ::selection, we make a note in our &lt;em&gt;pseudo bits&lt;/em&gt; saying we’ve seen rules for that pseudo, but otherwise ignore the rule.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;img width=&quot;467&quot; height=&quot;107&quot; src=&quot;/images/spammar2-y0.png&quot; srcset=&quot;/images/spammar2-y0.png 2x&quot; /&gt;
&lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;Once we’re in &lt;em&gt;paint&lt;/em&gt;, if the user has selected some text, then we need to know our ::selection styles, so we check our pseudo bits.
If the ::selection bit was set, we call our &lt;em&gt;resolver&lt;/em&gt; with a special request for pseudo styles, then cache the result into a vector inside the originating element’s ComputedStyle.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;img width=&quot;547&quot; height=&quot;97&quot; src=&quot;/images/spammar2-y1.png&quot; srcset=&quot;/images/spammar2-y1.png 2x&quot; /&gt;
&lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;This is how ::selection used to work, and at first I tried to keep it that way.&lt;/p&gt;

&lt;div class=&quot;_commits&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;

    &lt;h3 id=&quot;status-quo&quot;&gt;Status quo&lt;/h3&gt;

    &lt;p&gt;My initial solution was to make &lt;em&gt;paint&lt;/em&gt; pass in a custom inheritance parent with its style request.
Normally pseudo styles inherit from the originating element, but here they would inherit from the parent’s highlight styles, which we would obtain recursively.
Then in the resolver, if we’re dealing with a highlight, we copy non-inherited properties too.&lt;/p&gt;

    &lt;p&gt;On the surface, this worked, but to make it &lt;em&gt;correct&lt;/em&gt;, we had to work around an optimisation where the resolver would bail out early if there were no matching rules.
Worse still, we had to bypass the pseudo cache entirely.
While we already had to do so under :window-inactive, the performance penalty there was at least pretty contained.&lt;/p&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/7&quot;&gt;&lt;code&gt;PS7&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;p&gt;If we copy over the parent’s inherited properties as usual, and for highlights, copy the non-inherited properties too, that more or less means we’re copying &lt;em&gt;all&lt;/em&gt; the fields, so why not do away with that and just clone the parent’s ComputedStyle?&lt;/p&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/7..10&quot;&gt;&lt;code&gt;PS10&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;p&gt;The pseudo cache is only designed for pseudos whose styles won’t need to change between the originating element’s style updates.
For most pseudos, this is true anyway, as long as we bypass the cache under pseudo-classes like :window-inactive.&lt;/p&gt;

    &lt;p&gt;These caches are rarely actually cleared, but when the next update happens, the whole ComputedStyle — including the cache — gets discarded.
Caching results with custom inheritance parents is usually frowned upon, because changing the parent you inherit your styles from can yield different styles.
But for highlights, we will always have the same parent throughout an update cycle, so surely we can use the cache here?&lt;/p&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/10..13&quot;&gt;&lt;code&gt;PS13&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;p&gt;…well, yes and no.&lt;/p&gt;

    &lt;p&gt;Given an element that inherits a bunch of highlight styles, the initial styles are correct.
But when those inherited values change in some ancestor, our highlight styles fail to update!
This is a classic &lt;em&gt;cache invalidation&lt;/em&gt; bug.
Our invalidation system wasn’t even the problem — it’s just unaware of lazily resolved styles in pseudo caches.
This is usually fine, because most pseudos inherit from the originating element, but not here.&lt;/p&gt;

    &lt;h3 id=&quot;storing-highlight-styles&quot;&gt;Storing highlight styles&lt;/h3&gt;

    &lt;p&gt;With the pseudo cache being unsuitable for highlight styles, we needed some other way of storing them.
Only a handful of properties are allowed in highlight styles, so why not make a dedicated type with only those fields?&lt;/p&gt;

    &lt;p&gt;The declarations and basic methods for CSS properties are entirely generated, so let’s write some new templates…&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-jinja highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;{%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;macro&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;declare_highlight_class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;fields&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;field_templates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;%}&lt;/span&gt;
class &lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt; : public RefCounted&lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt; {
 public:
  static scoped_refptr&lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt; Create() { /* ... */ }
  scoped_refptr&lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt; Copy() const { /* ... */ }
  bool operator==(const &lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;amp;&lt;/span&gt; other) const { /* ... */ }
  bool operator!=(const &lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;amp;&lt;/span&gt; other) const { /* ... */ }
  &lt;span class=&quot;cp&quot;&gt;{%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;fields&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%}&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;declare_storage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;{%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;endfor&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%}&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;{%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;fields&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%}&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;field_templates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;field.field_template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;err&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;decl_public_methods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;field.without_group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;indent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;{%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;endfor&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%}&lt;/span&gt;
 private:
  &lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;();
  CORE_EXPORT &lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;(const &lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;&lt;span class=&quot;ni&quot;&gt;&amp;amp;);&lt;/span&gt;
};
&lt;span class=&quot;cp&quot;&gt;{%&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;endmacro&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;p&gt;…then use them in the ComputedStyleBase template.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-jinja highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;declare_highlight_class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;&apos;StyleHighlightData&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;computed_style.all_fields&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;attribute&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;selectattr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;valid_for_highlight&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;field_templates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;indent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/13..16&quot;&gt;&lt;code&gt;PS16&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;p&gt;Trouble is, all of the methods that apply and serialise property values — and there are &lt;em&gt;hundreds&lt;/em&gt; of them — take a ComputedStyle, not some other type.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-c++ highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;blink&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Color&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ColorIncludingFallback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visited_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ComputedStyle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CSSValue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CSSValueFromComputedStyleInternal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ComputedStyle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LayoutObject&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;allow_visited_style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;p&gt;Combined with the fact that our copy-on-write field groups mitigate a lot of the wasted memory (well hopefully anyway), we quickly abandoned this dedicated type.&lt;/p&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/16..25&quot;&gt;&lt;code&gt;PS25&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;!-- &lt;div class=&quot;_commit _commit-none&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/24..25&quot;&gt;&lt;code&gt;PS25&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-none.svg&quot;&gt;&lt;/div&gt; --&gt;

    &lt;p&gt;We then optimised the top-level struct a bit, saving a few pointer widths by moving the four highlight style pointers into a separate type, but this was still less than ideal.
We were widening ComputedStyle by one pointer, but the vast majority of web content doesn’t use highlight pseudos at all, and ComputedStyle and ComputedStyleBase are very sensitive to size changes.
To give you an idea of how much it matters, Blink even throws a compile-time error if the size inadvertently changes!&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-c++ highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SameSizeAsComputedStyleBase&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;SameSizeAsComputedStyleBase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Alias&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pointers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Alias&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bitfields&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;nl&quot;&gt;private:&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pointers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bitfields&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SameSizeAsComputedStyle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SameSizeAsComputedStyleBase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                 &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RefCounted&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SameSizeAsComputedStyle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;SameSizeAsComputedStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Alias&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;own_pointers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;nl&quot;&gt;private:&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;own_pointers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;ASSERT_SIZE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ComputedStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SameSizeAsComputedStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;p&gt;To move highlights out of the top-level and into a &lt;em&gt;raredata&lt;/em&gt; group, we had to get rid of all the fancy generated code and Just write a plain struct, which has the added benefit of making the code easier to read.
Luckily, we were only using that code to loop through the four highlight pseudos at this point, not dozens or hundreds of properties.&lt;/p&gt;

    &lt;p&gt;Then all we needed was a bit of JSON to tell the code generator to add an “extra” field, &lt;em&gt;and&lt;/em&gt; find an appropriate field group for us (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;*&quot;&lt;/code&gt;).
Because this field is not for a popular CSS property, or a property at all really, it automatically goes in a &lt;em&gt;raredata&lt;/em&gt; group.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;HighlightData&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;inherited&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;field_template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;external&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;type_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;StyleHighlightData&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;include_paths&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;third_party/blink/renderer/core/style/style_highlight_data.h&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;default_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;wrapper_pointer_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;DataRef&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;field_group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;computed_style_custom_functions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;initial&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;getter&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;setter&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;resetter&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/25..35&quot;&gt;&lt;code&gt;PS35&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;h3 id=&quot;single-pass-resolution&quot;&gt;Single-pass resolution&lt;/h3&gt;

    &lt;p&gt;With our new storage ready, we now needed to actually write to it.
We want to resolve highlight styles as part of the regular style update cycle, so that they can eventually benefit from style invalidation.&lt;/p&gt;

    &lt;p&gt;Looking at the resolver, I thought wow, there does seem to be a lot of redundant work being done when resolving highlight styles in a separate request, so why not weave highlight resolution into the resolver while we’re at it?&lt;/p&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/35..36&quot;&gt;&lt;code&gt;PS36&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;@@ third_party/blink/renderer/core/css/css_selector.h @@&lt;/span&gt;
   enum RelationType {
&lt;span class=&quot;gi&quot;&gt;+    kHighlights,
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ third_party/blink/renderer/core/css/css_selector.cc @@&lt;/span&gt;
       case kShadowSlot:
&lt;span class=&quot;gi&quot;&gt;+      case kHighlights:
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ third_party/blink/renderer/core/css/element_rule_collector.h @@&lt;/span&gt;
   MatchedRule(const RuleData* rule_data,
               unsigned style_sheet_index,
               const CSSStyleSheet* parent_style_sheet,
&lt;span class=&quot;gi&quot;&gt;+              absl::optional&amp;lt;PseudoId&amp;gt; highlight)
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ third_party/blink/renderer/core/css/resolver/match_result.h @@&lt;/span&gt;
   void AddMatchedProperties(
       const CSSPropertyValueSet* properties,
       unsigned link_match_type = CSSSelector::kMatchAll,
       ValidPropertyFilter = ValidPropertyFilter::kNoFilter,
&lt;span class=&quot;gi&quot;&gt;+      absl::optional&amp;lt;PseudoId&amp;gt; highlight = absl::nullopt);
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ ... @@&lt;/span&gt;
   const MatchedPropertiesVector&amp;amp; GetMatchedProperties(
&lt;span class=&quot;gi&quot;&gt;+      absl::optional&amp;lt;PseudoId&amp;gt; highlight) const {
+    DCHECK(!highlight || highlight_matched_properties_.Contains(*highlight));
+    return highlight ? *highlight_matched_properties_.at(*highlight)
&lt;/span&gt;                      : matched_properties_;
&lt;span class=&quot;p&quot;&gt;@@ ... @@&lt;/span&gt;
   MatchedPropertiesVector matched_properties_;
&lt;span class=&quot;gi&quot;&gt;+  HeapHashMap&amp;lt;PseudoId, Member&amp;lt;MatchedPropertiesVector&amp;gt;&amp;gt;
+      highlight_matched_properties_;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ third_party/blink/renderer/core/css/resolver/style_cascade.h @@&lt;/span&gt;
   void Apply(CascadeFilter = CascadeFilter());
&lt;span class=&quot;gi&quot;&gt;+  void ApplyHighlight(PseudoId);
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ third_party/blink/renderer/core/css/resolver/style_cascade.cc @@&lt;/span&gt;
 const CSSValue* ValueAt(const MatchResult&amp;amp; result,
&lt;span class=&quot;gi&quot;&gt;+                        absl::optional&amp;lt;PseudoId&amp;gt; highlight,
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@@ ... @@&lt;/span&gt;
 const TreeScope&amp;amp; TreeScopeAt(const MatchResult&amp;amp; result,
&lt;span class=&quot;gi&quot;&gt;+                             absl::optional&amp;lt;PseudoId&amp;gt; highlight,
&lt;/span&gt;                              uint32_t position) {
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;blockquote&gt;
      &lt;p&gt;In general we must find a less intrusive way to implement this. We can not have |highlight| params on everything.&lt;/p&gt;

      &lt;footer&gt;— &lt;cite&gt;andruud&lt;/cite&gt;, Blink &lt;em&gt;style&lt;/em&gt; owner&lt;/footer&gt;
    &lt;/blockquote&gt;

    &lt;p&gt;You know what? Fair enough.&lt;/p&gt;

    &lt;div class=&quot;_commit _commit-none&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/36..37&quot;&gt;&lt;code&gt;⭯ PS35&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-none.svg&quot; /&gt;&lt;/div&gt;

    &lt;h3 id=&quot;multi-pass-resolution&quot;&gt;Multi-pass resolution&lt;/h3&gt;

    &lt;p&gt;Element::Recalc{,Own}Style are pretty big friends of the style system.
They drive the style update cycle by determining how the tree has changed, making a resolver request for the element, and determining which descendants also need to be updated.&lt;/p&gt;

    &lt;p&gt;This makes them the perfect place to update highlight styles.
All we need to do is make an additional resolver request for each highlight pseudo, store it in the highlight data, and bob’s your uncle.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-c++ highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;StyleRecalcChange&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Element&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RecalcOwnStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;StyleRecalcChange&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;change&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;StyleRecalcContext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;style_recalc_context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;StyleHighlightData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;highlights&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_style&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MutableHighlightData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_style&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;HasPseudoElementStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kPseudoIdSelection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;ComputedStyle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ParentComputedStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;HighlightData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Selection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;StyleRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kPseudoIdSelection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;highlights&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetSelection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;StyleForPseudoElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;style_recalc_context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// kPseudoIdTargetText&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// kPseudoIdSpellingError&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// kPseudoIdGrammarError&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// SetComputedStyle(new_style);&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/37..43&quot;&gt;&lt;code&gt;PS43&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;h3 id=&quot;pathology-in-legacy&quot;&gt;Pathology in legacy&lt;/h3&gt;

    &lt;p&gt;So far, I had been writing this patch as a &lt;em&gt;replacement&lt;/em&gt; for the old inheritance logic, but since we decided to defer highlight inheritance for ::highlight to a later patch, we had to undelete the old behaviour and switch between them with a Blink feature.&lt;/p&gt;

    &lt;p&gt;Another reason for the feature gate was performance.
Of the pages in the wild already using highlight pseudos, most of them probably use universal ::selection rules, if only because of how useless the old model was for more complex use cases.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;lime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;green&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;p&gt;But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::selection&lt;/code&gt; isn’t magic — it literally means &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*::selection&lt;/code&gt;, which makes the rule match everywhere in the ::selection tree.
When highlight inheritance is enabled, that means we end up cloning highlight styles for each descendant, only to apply the &lt;em&gt;same&lt;/em&gt; property values, which wastes time and memory.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;img width=&quot;448&quot; height=&quot;378&quot; src=&quot;/images/spammar2-z0.png&quot; srcset=&quot;/images/spammar2-z0.png 2x&quot; /&gt;
&lt;/div&gt;&lt;figcaption&gt;
The reality is a bit more complicated than this, because ‘color’ and ‘background-color’ are actually in field groups that would also need to be cloned.
&lt;/figcaption&gt;&lt;/figure&gt;

    &lt;p&gt;Under the old model, where lack of inheritance made this necessary, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*::selection&lt;/code&gt; rules suffered from roughly the same problem, but the lazy style resolution meant that time and memory was only wasted on the elements &lt;em&gt;directly containing&lt;/em&gt; selected content.&lt;/p&gt;

    &lt;p&gt;As a result, this will need to be fixed before we can enable the feature for everyone.&lt;/p&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/43..51&quot;&gt;&lt;code&gt;PS51&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;h3 id=&quot;paired-cascade&quot;&gt;Paired cascade&lt;/h3&gt;

    &lt;p&gt;Next we tried to reimplement &lt;em&gt;paired cascade&lt;/em&gt;.
For compatibility reasons, ::selection has special logic for the browser’s default ‘color’ and ‘background-color’ (e.g. white on blue), where we only use those colors if &lt;em&gt;neither&lt;/em&gt; of them were set by the author.
Otherwise, they default to initial values, usually black on transparent.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;div class=&quot;flex&quot;&gt;&lt;table class=&quot;_sum&quot;&gt;
&lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;span style=&quot;color: white; background: #3584e4;&quot;&gt;default on default&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;+&lt;/td&gt;&lt;td&gt;
                &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;yellow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;                &lt;/div&gt;
              &lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;=&lt;/td&gt;&lt;td&gt;&lt;span style=&quot;color: black; background: yellow;&quot;&gt;initial on yellow&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;

    &lt;p&gt;The spec says so in a mere 22 words:&lt;/p&gt;

    &lt;blockquote&gt;
      &lt;p&gt;The UA must use its own highlight colors for ::selection only when neither color nor background-color has been specified by the author.&lt;/p&gt;
    &lt;/blockquote&gt;

    &lt;p&gt;Brevity is a good thing, and this seemed clear enough to me in the past.
But once I actually had to implement it, I had questions about almost every word (&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6386&quot;&gt;#6386&lt;/a&gt;).
While they aren’t &lt;em&gt;entirely&lt;/em&gt; resolved, we’ve been getting pretty close over the last few weeks.&lt;/p&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/51..52&quot;&gt;&lt;code&gt;PS52&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

    &lt;h3 id=&quot;fixing-tests&quot;&gt;Who’s got green&lt;a href=&quot;https://www.youtube.com/watch?v=ul6VV8XW9xw&quot;&gt;?&lt;/a&gt;&lt;/h3&gt;

    &lt;p&gt;Much of the remaining work was to fix test failures and other bugs.
These included crashes under legacy layout, since we only implemented this for LayoutNG, and functional changes leaking out of the feature gate.
One of the reftest failures was also interesting to deal with.
Let’s minimise it and take a look.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;charset=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;active selection and background-color (basic)&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;main&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;fuchsia&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;red&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::selection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;green&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Pass if text is fuchsia on green, not fuchsia on red.
&lt;span class=&quot;nt&quot;&gt;&amp;lt;main&amp;gt;&lt;/span&gt;Selected Text&lt;span class=&quot;nt&quot;&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/* selectNodeContents(main); */&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;p&gt;In the past, the “Selected Text” would render as fuchsia on green, and the test passes.
But under highlight inheritance it fails, rendering as initial (black) on green, because we now inherit styles in a tree for each pseudo, not from the originating element.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;div class=&quot;flex&quot;&gt;&lt;div&gt;
&lt;span style=&quot;color: fuchsia; background: green;&quot;&gt;Selected Text&lt;/span&gt;
→
&lt;span style=&quot;color: black; background: green;&quot;&gt;Selected Text&lt;/span&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;

    &lt;p&gt;So if the test is wrong, then how do we fix it?
Well… it depends on the &lt;em&gt;intent&lt;/em&gt; of the test, at least if we want to Do The Right Thing and preserve that.
Clearly the &lt;em&gt;primary&lt;/em&gt; intent of the test is ‘background-color’, given the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, but tests can also have secondary, less explicit intents.
In this case, the flavour text&lt;sup id=&quot;fnref:2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; even mentions fuchsia!&lt;/p&gt;

    &lt;p&gt;It might have helped if the test had a &lt;a href=&quot;https://web-platform-tests.org/writing-tests/reftest-tutorial.html#writing-the-test-file&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;meta name=assert&amp;gt;&lt;/code&gt;&lt;/a&gt;, an optional field dedicated to conveying intent, but probably not.
Most of the assert tags I’ve seen are poorly written anyway, being a more or less verbose adaptation of the title or flavour text, and there’s a good chance that the intent for fuchsia (if any) was simply to inherit it from the originating element, so we would still need to invent a new intent.&lt;/p&gt;

    &lt;p&gt;We could change the reference to initial (black) on green, which would serve as a secondary test that we &lt;em&gt;don’t&lt;/em&gt; inherit from the originating element, or remove the existing ‘color’, which would serve as a secondary test for &lt;a href=&quot;#paired-cascade&quot;&gt;paired cascade&lt;/a&gt;.
But I didn’t think it through that far at the time, so I gave ::selection a new ‘color’, achieving neither.&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
        &lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; main::selection {
&lt;span class=&quot;gi&quot;&gt;+ color: aqua;
&lt;/span&gt;  background: green; }
 &amp;lt;/style&amp;gt;
 &amp;lt;p&amp;gt;Pass if text is
&lt;span class=&quot;gd&quot;&gt;- fuchsia
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+ aqua
&lt;/span&gt; on green, not fuchsia on red.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/div&gt;&lt;/figure&gt;

    &lt;p&gt;Because the selected and unselected text colors were now different, I created &lt;em&gt;another&lt;/em&gt; test failure, though only under legacy layout.
The reference for this test was straightforward: aqua on green, no mention of fuchsia.
This makes sense on the surface, given that all of the text under test was selected.&lt;/p&gt;

    &lt;p&gt;In this case, the tip of the “t” was crossing the right edge of the selection as ink overflow, and were carefully painting the overflow in the unselected color.
The test would have failed under LayoutNG too, if not for an optimisation that skips this technique when everything is selected.
Let me illustrate with an exaggerated example:&lt;/p&gt;

    &lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;img width=&quot;167&quot; height=&quot;102&quot; src=&quot;/images/spammar2-ink-overflow.png&quot; srcset=&quot;/images/spammar2-ink-overflow.png 2x&quot; /&gt;
&lt;/div&gt;&lt;/figure&gt;

    &lt;p&gt;This behaviour is generally considered desirable, at least when there are unselected characters, so Blink isn’t exactly &lt;em&gt;wrong&lt;/em&gt; here.
It’s definitely possible to make the active-selection tests account for this — and the tools to do so already exist in the Web Platform Tests — but I don’t have the time to pursue this right now.&lt;/p&gt;

    &lt;div class=&quot;_commit&quot;&gt;&lt;a href=&quot;https://crrev.com/c/2850068/52..76&quot;&gt;&lt;code&gt;PS76&lt;/code&gt;&lt;/a&gt;&lt;img width=&quot;40&quot; height=&quot;40&quot; src=&quot;/images/badapple-commit-dot.svg&quot; /&gt;&lt;/div&gt;

  &lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;what-now&quot;&gt;What now?&lt;/h2&gt;

&lt;p&gt;After the holidays, we plan to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Resolve the remaining &lt;a href=&quot;#charlie&quot;&gt;spec issues&lt;/a&gt;.&lt;/strong&gt; These issues are critical for finishing highlight inheritance and allowing highlights to add their own decorations.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Port ::selection’s painting logic to the other highlights.&lt;/strong&gt; We might even use this as an opportunity to &lt;a href=&quot;https://docs.google.com/document/d/1Rfelx4qv-RhQYHUJ74QBU5MjEbmb9Wol9gvyjkywqgE&quot;&gt;roll ::selection into the marker system&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other work needed before we can ship the spelling and grammar features:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Ship highlight inheritance.&lt;/strong&gt; This includes addressing any spec resolutions, fixing the performance issues, and &lt;a href=&quot;https://docs.google.com/document/d/1eJn5QIX4JFGackDYmdLxWXEmTDkSGj_ZGz5XY4uCKbY&quot;&gt;adding devtools support&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Integrate spelling and grammar errors with decoration painting (&lt;a href=&quot;https://crbug.com/1257553&quot;&gt;bug 1257553&lt;/a&gt;).&lt;/li&gt;
  &lt;li&gt;Make automated testing possible for spelling and grammar errors (&lt;a href=&quot;https://github.com/web-platform-tests/wpt/issues/30863&quot;&gt;wpt#30863&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Special thanks to &lt;a href=&quot;https://twitter.com/regocas&quot;&gt;Rego&lt;/a&gt;, Frédéric (Igalia), &lt;a href=&quot;https://twitter.com/runeLi&quot;&gt;Rune&lt;/a&gt;, andruud (Google), &lt;a href=&quot;https://twitter.com/frivoal&quot;&gt;Florian&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/fantasai&quot;&gt;fantasai&lt;/a&gt; (CSSWG), &lt;a href=&quot;https://twitter.com/ecbos_&quot;&gt;Emilio&lt;/a&gt; (Mozilla), and Fernando (Microsoft).
We would also like to thank &lt;a href=&quot;https://www.bloomberg.com/company/&quot;&gt;Bloomberg&lt;/a&gt; for sponsoring this work.&lt;/p&gt;

&lt;hr /&gt;

&lt;script&gt;
    (() =&gt; {
        function click({ currentTarget: x }) {
            x.classList.toggle(&apos;_paused&apos;);
            x.querySelectorAll(&quot;video&quot;).forEach(v =&gt; {
                v.paused ? v.play() : v.pause();
            });
        }
        document.querySelectorAll(&quot;._gifs&quot;).forEach(x =&gt; {
            x.addEventListener(&quot;click&quot;, click);
        });
    })();

    [...document.querySelectorAll(&quot;._compare&quot;)].forEach(x =&gt; {
        const p = x.firstChild;
        const q = x.lastChild;

        const inner = document.createElement(&quot;div&quot;);
        x.prepend(inner);
        inner.append(p, q);
        p.after(document.createElement(&quot;div&quot;));

        inner.style.setProperty(&quot;--cut&quot;, `${inner.getBoundingClientRect().width / 2}px`);

        inner.addEventListener(&quot;mousemove&quot;, event =&gt; {
            inner.style.setProperty(&quot;--cut&quot;, `${event.offsetX}px`);
        });

        inner.addEventListener(&quot;touchmove&quot;, event =&gt; {
            inner.style.setProperty(&quot;--cut&quot;, `${event.targetTouches.item(0).clientX - inner.getBoundingClientRect().left}px`);
        });
    });
&lt;/script&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:2&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;This is an automated reftest, so the instructions in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;p&amp;gt;&lt;/code&gt; have no effect on the outcome. &lt;a href=&quot;https://web-platform-tests.org/writing-tests/reftests.html#writing-a-good-reftest&quot;&gt;We require them anyway&lt;/a&gt;, because they add a bit of redundancy that helps humans understand and verify the test’s assertions. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;</content><author><name></name></author><category term="home" /><category term="igalia" /><summary type="html">Modern web browsers can help users with their word processing needs by drawing squiggly lines under possible spelling or grammar errors in their input. CSS will give authors more control over when and how they appear, with the new ::spelling- and ::grammar-error pseudo-elements, and spelling- and grammar-error text decorations. Since part 1 in May, we’ve done a fair bit of work in both Chromium and the CSSWG towards making this possible.</summary></entry><entry><title type="html">Chromium spelling and grammar features</title><link href="https://www.azabani.com/2021/05/17/spelling-grammar.html" rel="alternate" type="text/html" title="Chromium spelling and grammar features" /><published>2021-05-17T10:30:00+00:00</published><updated>2021-05-17T10:30:00+00:00</updated><id>https://www.azabani.com/2021/05/17/spelling-grammar</id><content type="html" xml:base="https://www.azabani.com/2021/05/17/spelling-grammar.html">&lt;p&gt;Back in September, I wrote about &lt;a href=&quot;/2020/09/27/my-internship-with-igalia.html&quot;&gt;my wonderful internship&lt;/a&gt; with Igalia’s web platform team.
I’m thrilled to have since joined Igalia full-time, starting in the very last week of last year.
My first project has been implementing the new CSS spelling and grammar features in Chromium.
Life has been pretty hectic since Aria and I moved back to Perth, but more on that in another post.
For now, let’s step back and review our progress.&lt;/p&gt;

&lt;style&gt;
article &gt; figure &gt; img { max-width: 100%; }
article &gt; figure &gt; figcaption { max-width: 30rem; margin-left: auto; margin-right: auto; }
article &gt; pre, article &gt; code { font-family: Inconsolata, monospace, monospace; }
.local-demo { font-style: italic; font-weight: bold; color: rebeccapurple; }
.local-spelling, .local-grammar { text-decoration-thickness: 0; text-decoration-skip-ink: none; }
.local-spelling { text-decoration: red wavy underline; }
.local-grammar { text-decoration: green wavy underline; }
.local-table { font-size: 0.75em; }
.local-table td, .local-table th { vertical-align: top; border: 1px solid black; }
.local-table td:not(.local-tight), .local-table th:not(.local-tight) { padding: 0.5em; }
.local-tight picture, .local-tight img { vertical-align: top; }
.local-compare * + *, .local-tight * + * { margin-top: 0; }
.local-compare { max-width: 100%; border: 1px solid rebeccapurple; }
.local-compare &gt; div { max-width: 100%; position: relative; touch-action: pinch-zoom; --cut: 50%; }
.local-compare &gt; div &gt; * { vertical-align: top; max-width: 100%; }
.local-compare &gt; div &gt; :nth-child(1) { position: absolute; clip: rect(auto, auto, auto, var(--cut)); }
.local-compare &gt; div &gt; :nth-child(2) { position: absolute; width: var(--cut); height: 100%; border-right: 1px solid rebeccapurple; }
.local-compare &gt; div &gt; :nth-child(2):before { content: &quot;actual&quot;; color: rebeccapurple; font-size: 0.75em; position: absolute; right: 0.5em; }
.local-compare &gt; div &gt; :nth-child(2):after { content: &quot;ref&quot;; color: rebeccapurple; font-size: 0.75em; position: absolute; left: calc(100% + 0.5em); }
&lt;/style&gt;

&lt;p&gt;The squiggly lines that indicate possible &lt;span class=&quot;local-spelling&quot;&gt;spelling&lt;/span&gt; or &lt;span class=&quot;local-grammar&quot;&gt;grammar&lt;/span&gt; errors have been a staple of word processing on computers for decades.
But on the web, these indicators are powered by the browser, which doesn’t always have the information needed to place and render them most appropriately.
For example, authors might want to provide their own grammar checker (placement), or tweak colors to improve contrast (rendering).&lt;/p&gt;

&lt;p&gt;To address this, the CSS pseudo and text decoration specs have defined new pseudo-elements ::spelling-error and ::grammar-error, allowing authors to style those indicators, and new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-decoration-line&lt;/code&gt; values &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spelling-error&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grammar-error&lt;/code&gt;, allowing authors to mark up their text with the same kind of decorations as native indicators.&lt;/p&gt;

&lt;h2 id=&quot;contents&quot;&gt;Contents&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#current-status&quot;&gt;Current status&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#cjk-css-unification&quot;&gt;CSS unification&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#fifteen-years-in-the-making&quot;&gt;Fifteen years in the making&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#highlight-painting&quot;&gt;Highlight painting&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#shadows-and-backgrounds&quot;&gt;Shadows and backgrounds&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#shadow-clipping&quot;&gt;Shadow clipping&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#vertical-vertigo&quot;&gt;Vertical vertigo&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#cursed&quot;&gt;Cursed&lt;/a&gt; &lt;!-- and [screaming](#-aaaaaaaaaaaaa) --&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#processing-model&quot;&gt;Processing model&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#stay-tuned&quot;&gt;Stay tuned!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;current-status&quot;&gt;Current status&lt;/h2&gt;

&lt;p&gt;I’ve sent &lt;a href=&quot;https://groups.google.com/a/chromium.org/g/blink-dev/c/8UEcRJViPEU/m/YZml0HGxCQAJ&quot;&gt;an Intent to Prototype&lt;/a&gt;, as well as requests for positions from &lt;a href=&quot;https://github.com/mozilla/standards-positions/issues/470&quot;&gt;Mozilla&lt;/a&gt; and &lt;a href=&quot;https://lists.webkit.org/pipermail/webkit-dev/2021-January/031660.html&quot;&gt;Apple&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’ve landed &lt;a href=&quot;https://crrev.com/c/2606878&quot;&gt;a patch&lt;/a&gt; that paves the way for ::spelling-error + ::grammar-error support internally, and I’m hopefully(!) around halfway done with implementing both the new painting rules and the new processing model.&lt;/p&gt;

&lt;p&gt;The spec updates, led by Florian Rivoal, were largely done by the end of 2017.
As the first impl of both the features themselves &lt;em&gt;and&lt;/em&gt; much of the underlying highlight specs, there were always going to be questions and rough edges to be clarified.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/2474&quot;&gt;Two&lt;/a&gt; &lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/3932&quot;&gt;issues&lt;/a&gt; were raised before we even started, I’ve since sent in &lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6022&quot;&gt;another&lt;/a&gt; &lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/6264&quot;&gt;two&lt;/a&gt;, and I’ll need to raise at least two more by the time we’re done.
I’ve also landed &lt;a href=&quot;https://crrev.com/c/2624328&quot;&gt;three&lt;/a&gt; &lt;a href=&quot;https://crrev.com/c/2670609&quot;&gt;WPT&lt;/a&gt; &lt;a href=&quot;https://crrev.com/c/2706442&quot;&gt;patches&lt;/a&gt;, including &lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-001.html&quot;&gt;three&lt;/a&gt; &lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-002.html&quot;&gt;new&lt;/a&gt; &lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-003.html&quot;&gt;tests&lt;/a&gt; and fixes for countless more.&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;local-compare&quot; style=&quot;width: 300px; margin: 0 auto;&quot;&gt;&lt;img src=&quot;/images/spammar-6.png&quot; /&gt;&lt;img src=&quot;/images/spammar-7.png&quot; /&gt;&lt;/div&gt;
&lt;figcaption&gt;
    &lt;p&gt;&lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-003.html&quot;&gt;highlight-painting-003.html&lt;/a&gt;&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;In the course of my work on these features, I’ve already fixed at least &lt;a href=&quot;https://crbug.com/474335&quot;&gt;two&lt;/a&gt; &lt;a href=&quot;https://crbug.com/1078474&quot;&gt;other&lt;/a&gt; bugs that weren’t of my own creation, and reported four more:&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;&lt;table class=&quot;local-table&quot;&gt;
&lt;tr&gt;&lt;th&gt;&lt;a href=&quot;https://crbug.com/1171741&quot;&gt;1171741&lt;/a&gt;&lt;/th&gt;&lt;td&gt;Selecting text causes emphasis marks to be painted twice&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;th&gt;&lt;a href=&quot;https://crbug.com/1172177&quot;&gt;1172177&lt;/a&gt;&lt;/th&gt;&lt;td&gt;Erroneous viewport-size-dependent clipping of some text shadows&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;th&gt;&lt;a href=&quot;https://crbug.com/1176649&quot;&gt;1176649&lt;/a&gt;&lt;/th&gt;&lt;td&gt;text-shadow paints with incorrect offset for vertical scripts in vertical writing modes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;th&gt;&lt;a href=&quot;https://crbug.com/1180068&quot;&gt;1180068&lt;/a&gt;&lt;/th&gt;&lt;td&gt;text-shadow erroneously paints over text proper in mixed upright/sideways fragments&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;

&lt;h2 id=&quot;cjk-css-unification&quot;&gt;&lt;del&gt;CJK&lt;/del&gt; CSS unification&lt;/h2&gt;

&lt;p&gt;My colleague Rego noticed that the squiggly lines for spelling and grammar errors look slightly different to a naïve &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;red&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;green wavy underline&lt;/code&gt;.
How, why, and should we unify squiggly and wavy lines?
Some further investigation revealed that the two kinds of decorations are drawn very differently with completely separate code paths.&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;scroll&quot;&gt;
&lt;table class=&quot;local-table&quot;&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th colspan=&quot;2&quot;&gt;non-macOS (&lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/0.html?color=red&amp;amp;style=wavy&amp;amp;line=underline&amp;amp;thickness=0&amp;amp;ink=none&quot;&gt;demo&lt;sub&gt;0&lt;/sub&gt;&lt;/a&gt;)&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;a href=&quot;/images/spammar-0.png&quot;&gt;&lt;img src=&quot;/images/spammar-0@t.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;a href=&quot;/images/spammar-1.png&quot;&gt;&lt;img src=&quot;/images/spammar-1@t.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
        &lt;tr&gt;&lt;th&gt;100%&lt;/th&gt;&lt;th&gt;200%&lt;/th&gt;&lt;/tr&gt;
    &lt;/tfoot&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;figcaption&gt;
    &lt;p&gt;Left (bolder text): nearest &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wavy&lt;/code&gt; decorations.
&lt;br /&gt;Right (lighter text): native squiggly lines.&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The case for unifying squiggly and wavy lines became a lot more complicated too.
For example, our squiggly lines are actually dots on macOS.
More specifically, they are round dots with an alpha gradient, matching the platform’s native controls.
These details are beyond what can be expressed in terms of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dotted underline&lt;/code&gt;, so if we were to unify by making squiggly lines equivalent to such a decoration, we would lose that benefit.&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;scroll&quot;&gt;
&lt;table class=&quot;local-table&quot;&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th colspan=&quot;2&quot;&gt;macOS (&lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/0.html?color=red&amp;amp;style=dotted&amp;amp;line=underline&amp;amp;thickness=3px&amp;amp;ink=none&quot;&gt;demo&lt;sub&gt;0&lt;/sub&gt;&lt;/a&gt;)&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;a href=&quot;/images/spammar-2.png&quot;&gt;&lt;img src=&quot;/images/spammar-2@t.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;a href=&quot;/images/spammar-3.png&quot;&gt;&lt;img src=&quot;/images/spammar-3@t.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
        &lt;tr&gt;&lt;th&gt;100%&lt;/th&gt;&lt;th&gt;200%&lt;/th&gt;&lt;/tr&gt;
    &lt;/tfoot&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;figcaption&gt;
&lt;figcaption&gt;
      &lt;p&gt;Left (bolder text): nearest &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dotted&lt;/code&gt; decorations.
&lt;br /&gt;Right (lighter text): native squiggly lines.&lt;/p&gt;
    &lt;/figcaption&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The spec doesn’t require that spelling-error and grammar-error lines be expressible in terms of other decoration lines, so unification won’t block shipping.
I decided it would be best to revisit this once I landed some patches and familiarised myself with the code.&lt;/p&gt;

&lt;h2 id=&quot;fifteen-years-in-the-making&quot;&gt;Fifteen years in the making&lt;/h2&gt;

&lt;p&gt;::spelling-error and ::grammar-error are defined as &lt;em&gt;highlight pseudo-elements&lt;/em&gt;, together with ::selection and ::target-text.
The spec’s processing model and rendering rules are both very different to how ::selection (or ::target-text) has been implemented in any browser so far.
Now that we’re implementing more than just the first couple of pseudos, we really ought to comply with the new spec, which complicates our job somewhat.&lt;/p&gt;

&lt;p&gt;I’ll talk about ::selection a fair bit below, because most of the spec discussion I found happened before the others were defined, going back as far as 2006.
Highlight pseudos like ::selection are tricky because they aren’t &lt;em&gt;tree-abiding&lt;/em&gt;: the selected parts of the document aren’t generally a child of any one element.&lt;/p&gt;

&lt;p&gt;But even then, how hard could it be?&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;What &lt;em&gt;is&lt;/em&gt; ::selection? How does it interact with other pseudo-elements? Is it a singleton, or does each element have a ::selection pseudo-element? How do we reconcile the ::selection “tree”, if any, with the element tree?&lt;/li&gt;
  &lt;li&gt;Can child ::selection styles override parent ::selection styles? What about the child’s “real element” styles? How exactly do parent ::selection styles propagate to child ::selection styles? Do we use a tweaked cascade or tweaked inheritance?&lt;/li&gt;
  &lt;li&gt;What happens when authors specify ::selection styles that affect layout? What about styles that rely on how ::selection relates to the element tree, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;outline&lt;/code&gt; or translucent &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-color&lt;/code&gt;?&lt;/li&gt;
  &lt;li&gt;What happens when child ::selection styles specify only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;color&lt;/code&gt; or only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-color&lt;/code&gt; but not both? Does the other inherit as usual? If we want a special case tying these two properties together, how does it interact with other properties?&lt;/li&gt;
  &lt;li&gt;Does the ::selection &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-color&lt;/code&gt; paint over text, or under it? What about “replaced” content like images? If we paint over text, do we need to make the author’s color translucent, and if so, how?&lt;/li&gt;
  &lt;li&gt;Is text in the ::selection &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;color&lt;/code&gt; painted in addition to, or instead of, the same text in its original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;color&lt;/code&gt;? What about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-color&lt;/code&gt;?&lt;/li&gt;
  &lt;li&gt;Can the default UA stylesheet describe the platform’s ::selection style? How?&lt;/li&gt;
  &lt;li&gt;How naughty were browsers that implemented ::selection without a -vendor-prefix before it was standardised? Are vendor prefixes even a good idea?&lt;/li&gt;
  &lt;li&gt;Most importantly, how do we introduce a new processing model and rendering rules without breaking existing content?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For answers to most of these questions, check out my &lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/5.html&quot;&gt;notes&lt;sub&gt;5&lt;/sub&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;By the time I started to understand the problem space, two weeks had passed.&lt;/p&gt;

&lt;figure&gt;
    &lt;img src=&quot;/images/spammar-charlie.jpg&quot; /&gt;
    &lt;figcaption&gt;Pretty intense for my very first foray into www-style!&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;highlight-painting&quot;&gt;Highlight painting&lt;/h2&gt;

&lt;p&gt;The current spec isolates each highlight pseudo into an “overlay”, and allows each of them to have independent backgrounds, shadows, and other decorations.&lt;/p&gt;

&lt;p&gt;Like other browsers, Chromium implemented an older model, where matching ::selection rules are only used to &lt;em&gt;change&lt;/em&gt; things like the text color and shadows (except for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-color&lt;/code&gt;, which has always been independent).&lt;/p&gt;

&lt;p&gt;But the closer I looked, the deeper the problems ran.&lt;/p&gt;

&lt;h3 id=&quot;shadows-and-backgrounds&quot;&gt;Shadows and backgrounds&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;everyone’s shadow code is complete made-up horseshit but mostly i blame the fact that someone decided to add ‘shadow’ to the (very small!) special list of styles ::selection could modify&lt;/p&gt;

  &lt;p&gt;— Gankra, &lt;a href=&quot;https://twitter.com/Gankra_/status/1351020287790358530&quot;&gt;2021&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I whipped up a quick &lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/3.html&quot;&gt;demo&lt;sub&gt;3&lt;/sub&gt;&lt;/a&gt; with some backgrounds and shadows, and the result was… not good.
“So the originating text shadow (yellow) paints over the ::selection background (grey), except when it paints under, and sometimes it even paints over the text (black)?
Why is the ::selection shadow clipped to the ::selection background?
&lt;em&gt;What?”&lt;/em&gt;&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;local-compare&quot; style=&quot;width: 300px; margin: 0 auto;&quot;&gt;&lt;img src=&quot;/images/spammar-4.png&quot; /&gt;&lt;img src=&quot;/images/spammar-5.png&quot; /&gt;&lt;/div&gt;
&lt;figcaption&gt;
    &lt;p&gt;&lt;a href=&quot;https://wpt.live/css/css-pseudo/highlight-painting-001.html&quot;&gt;highlight-painting-001.html&lt;/a&gt; (based on &lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/3.html&quot;&gt;demo&lt;sub&gt;3&lt;/sub&gt;&lt;/a&gt;)&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Some of these were easier to fix than others.
To fix backgrounds, we essentially push the code that paints the background waaaaay down NG­Text­Fragment­Painter, so that it’s before painting the selected text but after pretty much everything else.
We then fix shadows similarly, reordering the text paints from “before with shadows, after with shadows, selected with shadows” to an order that keeps shadows behind text.&lt;/p&gt;

&lt;p&gt;These initial fixes are now live in Chromium 90, but we still need to deal with the ::selection shadow clipping.
What’s up with that?&lt;/p&gt;

&lt;h3 id=&quot;shadow-clipping&quot;&gt;Shadow clipping&lt;/h3&gt;

&lt;p&gt;The weird shadow clipping was a side effect of how we ensured that the ::selection text color changes &lt;em&gt;exactly&lt;/em&gt; where the ::selection background starts:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;we clip out and paint the selected text in original color, then&lt;/li&gt;
  &lt;li&gt;we clip (in) and paint the selected text in ::selection color.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is useful for both subtle reasons, like ink overflow…&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;scroll&quot;&gt;
&lt;table class=&quot;local-table&quot;&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th colspan=&quot;2&quot;&gt;&lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/6.html?t=d%C3%AElan&amp;amp;wm=horizontal-tb&amp;amp;tcu=none&amp;amp;fs=italic&amp;amp;p=0&amp;amp;q=1&amp;amp;minimal&quot;&gt;demo&lt;sub&gt;6&lt;/sub&gt;&lt;/a&gt;&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;picture&gt;
                &lt;source srcset=&quot;/images/spammar-8.png 2x&quot; /&gt;
                &lt;img src=&quot;/images/spammar-8.png&quot; /&gt;
            &lt;/picture&gt;&lt;/td&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;picture&gt;
                &lt;source srcset=&quot;/images/spammar-8@q.png 1x&quot; /&gt;
                &lt;img src=&quot;/images/spammar-8@q.png&quot; /&gt;
            &lt;/picture&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;…and not so subtle reasons, like allowing the user to clearly and precisely select graphemes in ligature-heavy languages like Sorani.
In this example, &lt;span lang=&quot;ckb&quot;&gt;یلا&lt;/span&gt; is three letters (&lt;span lang=&quot;kmr&quot;&gt;îla&lt;/span&gt;), but only two glyphs.
This isn’t explicitly required by any spec, but it’s definitely intentional.&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;scroll&quot;&gt;
&lt;table class=&quot;local-table&quot;&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th colspan=&quot;2&quot;&gt;&lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/6.html?t=%D8%AF%DB%8C%D9%84%D8%A7%D9%86&amp;amp;wm=horizontal-tb&amp;amp;tcu=none&amp;amp;fs=normal&amp;amp;p=2&amp;amp;q=3&amp;amp;minimal&quot;&gt;demo&lt;sub&gt;6&lt;/sub&gt;&lt;/a&gt;&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;picture&gt;
                &lt;source srcset=&quot;/images/spammar-9.png 2x&quot; /&gt;
                &lt;img src=&quot;/images/spammar-9.png&quot; /&gt;
            &lt;/picture&gt;&lt;/td&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;picture&gt;
                &lt;source srcset=&quot;/images/spammar-9@q.png 1x&quot; /&gt;
                &lt;img src=&quot;/images/spammar-9@q.png&quot; /&gt;
            &lt;/picture&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;If you use Chromium, you may notice that the ref for that demo appears to select more text.
What we’re really doing with ::selection painting is &lt;em&gt;pretending&lt;/em&gt; that ligatures are divisible into horizontal parts and &lt;em&gt;guessing&lt;/em&gt; how wide each part is.
Current font technology just doesn’t provide the metadata to do this more “correctly”.&lt;/p&gt;

&lt;p&gt;Firefox always allows splitting ligature styles, including with real elements, and there are &lt;a href=&quot;https://gankra.github.io/blah/text-hates-you/#style-can-change-mid-ligature&quot;&gt;at least two good arguments&lt;/a&gt; in favour of this approach.
Chromium has (reasonably) decided that while the technique is ok for ::selection, perhaps even desirable, it’s &lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=1147859#c9&quot;&gt;not the way to go for ordinary markup&lt;/a&gt;.&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;scroll&quot;&gt;
&lt;table class=&quot;local-table&quot;&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th&gt;&lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/4.html&quot;&gt;demo&lt;sub&gt;4&lt;/sub&gt;&lt;/a&gt;&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;picture&gt;
                &lt;source srcset=&quot;/images/spammar-a.png 2x&quot; /&gt;
                &lt;img src=&quot;/images/spammar-a.png&quot; /&gt;
            &lt;/picture&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;But anyway, back to the point at hand.
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-shadow&lt;/code&gt; means “paint the text again, under the text proper, with these colors and offsets”.
We want to clip the ::selection shadow for the same reasons we clip the text proper in ::selection color, but the coordinates need to be offset for each shadow.
That we &lt;em&gt;don’t&lt;/em&gt; is the bug here.&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;scroll&quot;&gt;
&lt;table class=&quot;local-table&quot;&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th&gt;&lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/6.html?t=quick&amp;amp;wm=horizontal-tb&amp;amp;tcu=none&amp;amp;fs=normal&amp;amp;p=1&amp;amp;q=4&amp;amp;noyellow&amp;amp;scbug&quot;&gt;demo&lt;sub&gt;6&lt;/sub&gt;&lt;/a&gt;&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;picture&gt;
                &lt;source srcset=&quot;/images/spammar-b.png 2x&quot; /&gt;
                &lt;img src=&quot;/images/spammar-b.png&quot; /&gt;
            &lt;/picture&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;figcaption&gt;When painting the ::selection shadow (blue), we need to clip the canvas to the dotted line, but we were actually clipping to the solid line.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Consensus seems to be that &lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/3932&quot;&gt;not doing so is undesirable&lt;/a&gt;, and in theory, fixing this would be straightforward, but in practice… 😵‍💫&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;The first confounding factor was that NG­Text­Fragment­Painter and NG­Text­Painter were… a tangled mess.
Even the owners weren’t sure this was the most helpful architecture:&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-c++ highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// TODO(layout-dev): Does this distinction make sense?&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CORE_EXPORT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NGTextPainter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TextPainterBase&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;Years of typographical features have been duct-taped on without a systemic approach to managing complexity, including decorations, shadows, ellipses, background clipping, RTL text, vertical text, ruby text, emphasis marks, print rendering, drag-and-drop rendering, selections, highlights, “markers”, and SVG features like stroke and fill.&lt;/p&gt;

&lt;p&gt;A third of the logic was in Text­Painter­Base, so good luck not breaking legacy.
Shadows were painted with a now-deprecated Skia feature called a Draw­Looper, which allows you to repeat a procedure a bunch of times with different tweaks, such as canvas transformations and color changes.
It’s almost specifically designed for shadows, but it’s technically possible to repeat procedures that have nothing to do with drawing text.&lt;/p&gt;

&lt;figure&gt;&lt;div class=&quot;scroll&quot;&gt;
    &lt;div class=&quot;language-c++ highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// SkCanvas* canvas;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// SkPaint paint;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// SkScalar x, y;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// sk_sp&amp;lt;SkTextBlob&amp;gt; blob;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// sk_sp&amp;lt;SkDrawLooper&amp;gt; looper;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;looper&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;paint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;](&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SkCanvas&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SkPaint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// procedure to be looped&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;drawTextBlob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;&lt;/figure&gt;

&lt;p&gt;My solution was based on the observation that loopers draw offset shadows by “moving” the canvas with a transform before each iteration, but transforming the canvas only affects &lt;em&gt;subsequent&lt;/em&gt; operations.
We were clipping the canvas once, before running the looper, but if we could somehow reclip the canvas after each transform, the clip region would “move” together with each shadow, and we wouldn’t even need to change the coordinates!&lt;/p&gt;

&lt;p&gt;I prototyped a fix that seemed to handle everything I threw at it, and informed by the challenges that involved, I also refactored out the code for selections, highlights, and markers.
Stephen and I decided that adding clipping as a fixed function to Draw­Looper made more sense than adding it to the procedure.
At the time, this was true.&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;scroll&quot;&gt;
&lt;table class=&quot;local-table&quot;&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th colspan=&quot;2&quot;&gt;&lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/6.html?t=%D8%AF%DB%8C%D9%84%D8%A7%D9%86&amp;amp;wm=vertical-rl&amp;amp;tcu=none&amp;amp;fs=normal&amp;amp;p=1&amp;amp;q=4&quot;&gt;demo&lt;sub&gt;6&lt;/sub&gt;&lt;/a&gt;&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;picture&gt;
                &lt;source srcset=&quot;/images/spammar-c.png 1x&quot; /&gt;
                &lt;img src=&quot;/images/spammar-c.png&quot; /&gt;
            &lt;/picture&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;figcaption&gt;
    &lt;p&gt;The prototype made my most complex test case (at the time) pass, with the exception of ink overflow color, which was a limitation of my ref (&lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=1147859#c11&quot;&gt;both renderings are acceptable&lt;/a&gt;).&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I then took a couple weeks off to move to Perth.&lt;/p&gt;

&lt;h3 id=&quot;vertical-vertigo&quot;&gt;Vertical vertigo&lt;/h3&gt;

&lt;p&gt;“Wait… isn’t the original purpose of vertical writing modes, you know, vertical &lt;em&gt;scripts&lt;/em&gt;? I wonder if those work as well as horizontal scripts being rotated sideways…”&lt;/p&gt;

&lt;p&gt;“…what? Let’s see what they look like &lt;em&gt;without&lt;/em&gt; my patch…”&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“…what?”&lt;/em&gt;&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;scroll&quot;&gt;
&lt;table class=&quot;local-table&quot;&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th colspan=&quot;2&quot;&gt;&lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/6.html?wm=vertical-rl&amp;amp;t=%E4%BD%A0%E5%A5%BD&amp;amp;range=1,2&quot;&gt;demo&lt;sub&gt;6&lt;/sub&gt;&lt;/a&gt;&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;picture&gt;
                &lt;source srcset=&quot;/images/spammar-d.png 2x&quot; /&gt;
                &lt;img src=&quot;/images/spammar-d.png&quot; /&gt;
            &lt;/picture&gt;&lt;/td&gt;
            &lt;td class=&quot;local-tight&quot;&gt;&lt;picture&gt;
                &lt;source srcset=&quot;/images/spammar-e.png 2x&quot; /&gt;
                &lt;img src=&quot;/images/spammar-e.png&quot; /&gt;
            &lt;/picture&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;figcaption&gt;
    &lt;p&gt;Left: vertical script in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vertical-rl&lt;/code&gt;, with patch.
&lt;br /&gt;Right: same test case, without patch.&lt;/p&gt;

    &lt;p&gt;Notice how the shadows are offset in the wrong direction.
They should be painted southeast of the text proper, but were being painted northeast.&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;When painting a text fragment with a vertical &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;writing-mode&lt;/code&gt;, we rotate the canvas by 90° cw (or ccw for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sideways-lr&lt;/code&gt;).
This is good for horizontal scripts like Latin or Sorani, because they usually need to be painted sideways.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;Except when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-orientation&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;upright&lt;/code&gt;, which overrides the usual behaviour.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;But for vertical scripts like Han, we usually need to keep the canvas unrotated.
A single text fragment can contain text in multiple scripts, so we actually achieve this by rotating the canvas &lt;em&gt;back&lt;/em&gt; for the parts in vertical scripts.&lt;/p&gt;

&lt;aside&gt;
  &lt;p&gt;Except when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-orientation&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sideways&lt;/code&gt;, which overrides the usual behaviour.&lt;/p&gt;

  &lt;p&gt;Note that the way &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-orientation&lt;/code&gt; is defined means that none of its values are actually supposed to affect the rendering of &lt;em&gt;vertical-only&lt;/em&gt; scripts like Mongolian.
I would suggest not thinking about this too hard.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;So far so good right?&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;This is what we were doing when painting text with vertical scripts and shadows (example limited to a single script and single shadow for simplicity):&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Let &lt;em&gt;space&lt;/em&gt; be our original “physical” coordinate space&lt;/li&gt;
  &lt;li&gt;Let &lt;em&gt;offset&lt;/em&gt; be the shadow’s offset in &lt;em&gt;space&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Let &lt;em&gt;selection&lt;/em&gt; be the selection rect coordinates in &lt;em&gt;space&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Vertical writing mode, so rotate canvas by 90°, yielding &lt;em&gt;space′&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Let &lt;em&gt;offset′&lt;/em&gt; be the result of mapping &lt;em&gt;offset&lt;/em&gt; into &lt;em&gt;space′&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Let &lt;em&gt;selection′&lt;/em&gt; be the result of mapping &lt;em&gt;selection&lt;/em&gt; into &lt;em&gt;space′&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Old:&lt;/strong&gt; clip the canvas to &lt;em&gt;selection′&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Configure a Draw­Looper that will:
    &lt;ul&gt;
      &lt;li&gt;move the canvas by &lt;em&gt;offset′&lt;/em&gt;&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;New:&lt;/strong&gt; clip the canvas to &lt;em&gt;selection′&lt;/em&gt;&lt;/li&gt;
      &lt;li&gt;draw the text for the shadow&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Vertical script, so rotate canvas back by 90°, yielding &lt;em&gt;space″&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Run the Draw­Looper, which carries out the steps above&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The looper is told to move and clip the canvas to &lt;em&gt;offset′&lt;/em&gt; and &lt;em&gt;selection′&lt;/em&gt;, which are coordinates in &lt;em&gt;space′&lt;/em&gt;, but when it eventually tries to do that, the canvas is in &lt;em&gt;space″&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;offset′&lt;/em&gt; being in the wrong space is why shadows have always been painted in the wrong place for vertical scripts.
By reordering the clip to &lt;em&gt;selection′&lt;/em&gt; so it happens after the rotation to &lt;em&gt;space″&lt;/em&gt;, we were now clipping the canvas to the wrong coordinates, which in turn made the text invisible in our &lt;a class=&quot;local-demo&quot; href=&quot;https://bucket.daz.cat/work/igalia/0/6.html?wm=vertical-rl&amp;amp;t=%E4%BD%A0%E5%A5%BD&amp;amp;range=1,2&quot;&gt;demo&lt;sub&gt;6&lt;/sub&gt;&lt;/a&gt;!&lt;/p&gt;

&lt;h3 id=&quot;cursed&quot;&gt;Cursed&lt;/h3&gt;

&lt;p&gt;Fixing this again proved harder than it seemed on the surface, because text painting in Chromium involves the coordination of four components: &lt;em&gt;paint&lt;/em&gt;, &lt;em&gt;shaping&lt;/em&gt;, &lt;em&gt;cc&lt;/em&gt;, and &lt;em&gt;Skia&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In &lt;em&gt;paint&lt;/em&gt;, the text painters are given a “fragment” of text to be painted in a given style.
They know the writing mode, because that’s part of the style, but they know very little about the text itself.
The first rotation (for the vertical writing mode) happens here, and we configure the Draw­Looper here (except for its procedure, which we pass in &lt;em&gt;shaping&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;In &lt;em&gt;shaping&lt;/em&gt;, we find the best glyphs for each character, and determine what scripts the text fragment is made of, then split the text into “blobs”.
The second rotation (for the vertical script) happens here, and we throw in a skew transform too if the text we’re painting is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oblique&lt;/code&gt; (or fake &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;italic&lt;/code&gt;, which is again known only to &lt;em&gt;shaping&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;In &lt;em&gt;cc&lt;/em&gt;, we expose a &lt;em&gt;Skia&lt;/em&gt;-like API that can either dispatch to &lt;em&gt;Skia&lt;/em&gt; immediately or collect operations into a queue for later.
Draw­Looper is in the process of being moved here, because the &lt;em&gt;Skia&lt;/em&gt; maintainers don’t want it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Skia&lt;/em&gt; provides a stateful canvas, which more or less creates visible output.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;With each canvas transform, existing coordinates need to be remapped into the new space before they can be used again, and we were doing them &lt;em&gt;imperatively&lt;/em&gt; in two different components.
Worse still, while layout (ng) — the phase that happens before &lt;em&gt;paint&lt;/em&gt; — uses the type system to enforce correct handling of coordinates (e.g. Physical­Offset, Logical­Rect), the same is not true for &lt;em&gt;paint&lt;/em&gt; onwards.&lt;/p&gt;

&lt;p&gt;Everything is in Physical­Rect and friends, often erroneously, or in “untyped” coordinates like Float­Rect or Sk­Rect.
In one case, a Physical­Offset is used in both physical and non-physical (rotated for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;writing-mode&lt;/code&gt;) spaces, to refer to two &lt;em&gt;different&lt;/em&gt; points at &lt;em&gt;different&lt;/em&gt; corners of the text.
Here… let me illustrate.&lt;/p&gt;

&lt;figure&gt;
&lt;div class=&quot;scroll&quot;&gt;
    &lt;picture&gt;
        &lt;source srcset=&quot;/images/spammar-f.png 1x&quot; /&gt;
        &lt;img src=&quot;/images/spammar-f.png&quot; /&gt;
    &lt;/picture&gt;
&lt;/div&gt;
&lt;figcaption&gt;
    &lt;p&gt;When painting horizontal text in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vertical-rl&lt;/code&gt;, we rotate the canvas 90° cw around &lt;em&gt;A&lt;/em&gt; so that the text’s left descent corner lands on &lt;em&gt;B&lt;/em&gt;.
The left ascent corner moves from &lt;em&gt;B&lt;/em&gt; to &lt;em&gt;C&lt;/em&gt;.&lt;/p&gt;

    &lt;p&gt;That single variable was used to intentionally refer to both &lt;em&gt;B&lt;/em&gt; and &lt;em&gt;C&lt;/em&gt; at different times in a function, because the coordinates for &lt;em&gt;B&lt;/em&gt; in &lt;em&gt;space&lt;/em&gt; happen to be numerically the same as those for &lt;em&gt;C&lt;/em&gt; in &lt;em&gt;space′&lt;/em&gt;.
aaaa­aaaA­AAAA­AAAA­AAAA-&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;-aaaaaaaaaaaaa&quot;&gt;-AAAAAAAAAAAAA&lt;/h3&gt;

&lt;p&gt;To be fair, each of these flaws has a reasonable explanation.&lt;/p&gt;

&lt;p&gt;Layout is a confusing place where we constantly need to deal with different coordinate spaces, so ideally we would iron everything out so that paint can work purely in physical space.
&lt;em&gt;Half the point&lt;/em&gt; of types like Logical­Rect is to provide getters and setters for concepts like “inline start” and “block end”.&lt;/p&gt;

&lt;p&gt;For most of the things we paint, this is ok, even desirable.
Rects like ::selection backgrounds &lt;em&gt;must&lt;/em&gt; be painted in physical space, so we can round the coordinates to integers for crisp edges.
Text is the only exception: the history of computer typography means that vertical text is, to some extent, seen internally as rotated horizontal text.&lt;/p&gt;

&lt;p&gt;Draw­Looper is handy for painting shadows, and it might&lt;sup&gt;[citation needed]&lt;/sup&gt; even reduce serialisation overhead in &lt;em&gt;cc&lt;/em&gt;.
But the way we currently configure them, baking coordinates into them before shaping, makes it even harder to handle vertical text correctly.&lt;/p&gt;

&lt;p&gt;Last but not least, Chromium’s pre-standard text painting order was “all rects for highlights and markers first, then all texts”.
This made the imperative canvas rotations &lt;em&gt;almost&lt;/em&gt; acceptable, if you ignore the shadow bugs, because we didn’t need to rotate the canvas back and forth nearly as many times.&lt;/p&gt;

&lt;p&gt;Once I moved to Perth, I spent over three weeks trying to find a systemic solution to these problems, but I just wasn’t getting anywhere meaningful.
In the interests of working a bit more breadth-first and avoiding burnout, I’ve shelved highlight painting for now.&lt;/p&gt;

&lt;h2 id=&quot;processing-model&quot;&gt;Processing model&lt;/h2&gt;

&lt;p&gt;Let’s return to how computed styles for highlight selectors should work.&lt;/p&gt;

&lt;p&gt;The consensus was that parent ::selection styles should &lt;em&gt;somehow&lt;/em&gt; propagate to the ::selection styles of their children, so authors can use their existing CSS skills to define both general ::selection styles &lt;em&gt;and&lt;/em&gt; more specific styles under certain elements.
This was unlike all existing implementations, where the only selector that worked the way you would expect was &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::selection&lt;/code&gt;, that is to say, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*::selection&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At first, that “somehow” was by tweaking the &lt;a href=&quot;https://www.w3.org/TR/css-cascade-4/#cascade-sort&quot;&gt;cascade&lt;/a&gt; to take parent ::selection rules into account.
Emilio raised &lt;a href=&quot;https://github.com/w3c/csswg-drafts/issues/2474&quot;&gt;performance concerns&lt;/a&gt; with this, so the spec was changed, instead tweaking &lt;a href=&quot;https://www.w3.org/TR/css-cascade-4/#inheriting&quot;&gt;inheritance&lt;/a&gt; to make ::selection styles inherit from parent ::selection styles (and never from originating or “real” elements).&lt;/p&gt;

&lt;p&gt;This is what I’m working on now.
I’ve got a patch that gets most of the way, first by fixing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inherit&lt;/code&gt;, then by fixing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unset&lt;/code&gt;, then with a couple more fixes for styles where the cascade doesn’t yield any value, but there are still a few kinks ahead:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;impl work has raised at least three questions that need CSSWG clarification;&lt;/li&gt;
  &lt;li&gt;we need to optimise it, maybe more than before, to avoid perf regressions;&lt;/li&gt;
  &lt;li&gt;we still need to check if style invalidation works correctly; and&lt;/li&gt;
  &lt;li&gt;we probably want new devtools features to visualise highlight inheritance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;stay-tuned&quot;&gt;Stay tuned!&lt;/h2&gt;

&lt;p&gt;Beyond my colleagues at Igalia, special thanks go to Stephen, &lt;a href=&quot;https://twitter.com/runeLi&quot;&gt;Rune&lt;/a&gt;, Koji (Google), and &lt;a href=&quot;https://twitter.com/ecbos_&quot;&gt;Emilio&lt;/a&gt; (Mozilla) for putting up with all of my questions, not to mention Florian and fantasai from the CSSWG, plus &lt;a href=&quot;https://twitter.com/Gankra_&quot;&gt;Gankra&lt;/a&gt; (Mozilla) for her writing about text rendering, which has proved both inspiring and reassuring.&lt;/p&gt;

&lt;script&gt;
    [...document.querySelectorAll(&quot;.local-compare&quot;)].forEach(x =&gt; {
        const p = x.firstChild;
        const q = x.lastChild;

        const inner = document.createElement(&quot;div&quot;);
        x.prepend(inner);
        inner.append(p, q);
        p.after(document.createElement(&quot;div&quot;));

        inner.style.setProperty(&quot;--cut&quot;, `${inner.getBoundingClientRect().width / 2}px`);

        inner.addEventListener(&quot;mousemove&quot;, event =&gt; {
            inner.style.setProperty(&quot;--cut&quot;, `${event.offsetX}px`);
        });

        inner.addEventListener(&quot;touchmove&quot;, event =&gt; {
            inner.style.setProperty(&quot;--cut&quot;, `${event.targetTouches.item(0).clientX - inner.getBoundingClientRect().left}px`);
        });
    });
&lt;/script&gt;</content><author><name></name></author><category term="home" /><category term="igalia" /><summary type="html">Back in September, I wrote about my wonderful internship with Igalia’s web platform team. I’m thrilled to have since joined Igalia full-time, starting in the very last week of last year. My first project has been implementing the new CSS spelling and grammar features in Chromium. Life has been pretty hectic since Aria and I moved back to Perth, but more on that in another post. For now, let’s step back and review our progress.</summary></entry><entry><title type="html">My internship with Igalia</title><link href="https://www.azabani.com/2020/09/27/my-internship-with-igalia.html" rel="alternate" type="text/html" title="My internship with Igalia" /><published>2020-09-27T09:00:00+00:00</published><updated>2020-09-27T09:00:00+00:00</updated><id>https://www.azabani.com/2020/09/27/my-internship-with-igalia</id><content type="html" xml:base="https://www.azabani.com/2020/09/27/my-internship-with-igalia.html">&lt;p&gt;I was looking for a job late last year when I saw &lt;a href=&quot;https://twitter.com/andywingo/status/1190917731312439296&quot;&gt;a tweet&lt;/a&gt; about a place called &lt;a href=&quot;https://www.igalia.com/about/&quot;&gt;Igalia&lt;/a&gt;.
The more I learned about them, the more interested I became, and before long I &lt;a href=&quot;https://www.igalia.com/jobs/web_platform_engineer&quot;&gt;applied to join&lt;/a&gt; their Web Platform team.
I didn’t have enough experience for a permanent position, but they &lt;em&gt;did&lt;/em&gt; offer me a place in their &lt;a href=&quot;https://www.igalia.com/coding-experience/&quot;&gt;Coding Experience&lt;/a&gt; program, which as far as I can tell is basically an internship, and I thoroughly enjoyed it.
Here’s an overview of what I did and what I learned.&lt;/p&gt;

&lt;style&gt;
figure { text-align: center; }
figure img { max-width: 100%; }
&lt;/style&gt;

&lt;h2 id=&quot;contents&quot;&gt;Contents&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#why-igalia&quot;&gt;Why Igalia?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#ſtylesheet&quot;&gt;ſtylesheet&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#fixing-the-bug&quot;&gt;Fixing the bug&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#writing-some-tests&quot;&gt;Writing some tests&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#mathml-tasks&quot;&gt;MathML tasks&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#momaxsize&quot;&gt;mo@maxsize&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#stixgeneral&quot;&gt;STIXGeneral&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#padding--border--margin&quot;&gt;padding + border + margin&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#acknowledgements&quot;&gt;Acknowledgements&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;why-igalia&quot;&gt;Why Igalia?&lt;/h2&gt;

&lt;p&gt;There’s a wide range of work I can do as a computer programmer, but the vast majority of it seems to be in closed-source web applications, as an employee with a limited voice in the decisions that affect my work.&lt;/p&gt;

&lt;p&gt;At the time, all of my work since I graduated had been exactly that, or in builds and releases for said applications.
That was interesting enough for a while, but I wanted to make a bigger impact, work on something I actually cared about of my own volition, and ideally move towards getting paid to do systems programming.&lt;/p&gt;

&lt;p&gt;Igalia appeals to me, with their focus on open-source projects, systems programming, and standards work.
Even better, as a field, the web platform has been my one true love, and building things on it is how I got into programming over 15 years ago.
But what cements their place as my “dream job” is &lt;em&gt;how&lt;/em&gt; they work: as a distributed &lt;a href=&quot;https://en.wikipedia.org/wiki/Worker_cooperative&quot;&gt;worker’s cooperative&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What I mean by “distributed” is that members can work from anywhere in the world, paid in a way that fairly adjusts for location, and in whatever setting they thrive in (such as home).
This alone was huge, as someone who can’t sustainably work in an office five days a week, had to move 4000 km away from home to do so, and had just left an employer that was actively hostile to remote work.&lt;/p&gt;

&lt;p&gt;Andy Wingo (author of &lt;a href=&quot;https://twitter.com/andywingo/status/1190917731312439296&quot;&gt;that tweet&lt;/a&gt;) offers some insight into the “worker’s cooperative” part in &lt;a href=&quot;https://wingolog.org/archives/2013/06/05/no-master&quot;&gt;these&lt;/a&gt; &lt;a href=&quot;https://wingolog.org/archives/2013/06/13/but-that-would-be-anarchy&quot;&gt;three&lt;/a&gt; &lt;a href=&quot;https://wingolog.org/archives/2013/06/25/time-for-money&quot;&gt;posts&lt;/a&gt;.
Igalia’s rough goal here, as far as I can tell, is that everyone gets a voice in deciding what the collective works on and how (to the extent that those decisions affect them), equal ownership of the business, and equivalent pay modulo effort and cost of living.
This appeals to me &lt;a href=&quot;/notes/anarchism101.html&quot;&gt;as an anarchist&lt;/a&gt;, but also as a worker that has often been on the receiving end of unethical work, poor working conditions, and lack of autonomy.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;ſtylesheet&quot; style=&quot;font-family: Symbola;&quot;&gt;ſtylesheet&lt;/h2&gt;

&lt;p&gt;One goal of my internship was to help the Web Platform team with their MathML work, but I was also there to familiarise myself with working on the web platform, and my first task was purely for the latter.&lt;/p&gt;

&lt;p&gt;Many parts of the web platform have case-insensitive keywords that control an API or language feature, like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-rel&quot;&gt;link@rel&lt;/a&gt; (the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;link rel=&quot;...&quot;&amp;gt;&lt;/code&gt; attribute), but thanks to Unicode, there’s more than one level of case-insensitivity.
Unicode case-insensitivity &lt;a href=&quot;https://unicode.org/faq/casemap_charprop.html#13&quot;&gt;won’t break&lt;/a&gt; backwards compatibility of web content over time, but to improve interoperability and simplify implementations, things like the HTML spec tend to explicitly call for ASCII case-insensitivity, at least for keywords that are nominally ASCII.&lt;/p&gt;

&lt;p&gt;That makes &lt;a href=&quot;https://en.wikipedia.org/wiki/Blink_(browser_engine)&quot;&gt;Blink’s&lt;/a&gt; widespread use of Unicode case-insensitivity in these situations a bug, and my job was to fix that bug, which sounds simple enough, until you realise that doing so is technically a breaking change.
You see, there are already a couple of non-ASCII characters that can introduce esoteric ways to write many of those keywords.&lt;/p&gt;

&lt;p&gt;More importantly, the web platform is almost&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; unique in that breaking existing content is, in general, not allowed.
But this time a breaking change was unavoidable, like any time where an implementation is fixed to align with the standard, or some behaviour is standardised after incompatible implementations appear.
There might be content out there that relies on something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;link rel=&quot;ſtylesheet&quot;&amp;gt;&lt;/code&gt; because it worked on Chromium.&lt;/p&gt;

&lt;p&gt;There are &lt;a href=&quot;https://www.chromium.org/blink/platform-predictability/compat-tools&quot;&gt;a few ways&lt;/a&gt; to minimise the impact of these breaking changes, like adding analytics to browsers to count how many pages would be affected, or searching archives of web content, but in this case we decided the risk was low enough that I could simply fix the bug and write some tests.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.chromestatus.com/feature/5734362161086464&quot;&gt;Chrome Platform Status entry&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.google.com/document/d/1uZ0wMBF63eLJNbW3yz1oGK7qkO3VqRUwdmNAbiLanN8&quot;&gt;intent to remove&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://groups.google.com/a/chromium.org/d/topic/blink-dev/sFOpNuQ91UU&quot;&gt;blink-dev thread&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bucket.daz.cat/crbug-627682.html&quot;&gt;analysis of deprecated call sites&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crbug.com/627682&quot;&gt;issue 627682: tracking bug for deprecated string operations&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crbug.com/1060477&quot;&gt;issue 1060477: HTMLElement::ApplyAlignmentAttributeToStyle&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crbug.com/1060495&quot;&gt;issue 1060495: HiddenInputType::AppendToFormData&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crbug.com/1060499&quot;&gt;issue 1060499: &amp;lt;param name=”src” value=”…”&amp;gt; + &amp;lt;object data=”…”&amp;gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crrev.com/c/1997014&quot;&gt;CL 1997014: Element#insertAdjacentElement + Element#insertAdjacentText&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crrev.com/c/2015875&quot;&gt;CL 2015875: DeprecatedEqual: safe subset part 1/2 (NFC)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crrev.com/c/2032654&quot;&gt;CL 2032654: DeprecatedEqual: safe subset part 2/2 (NFC)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crrev.com/c/2032655&quot;&gt;CL 2032655: DeprecatedEqual: HTML attribute values (including WPT)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://github.com/web-platform-tests/wpt/pull/22064&quot;&gt;web-platform-tests/wpt#22064: new web platform tests (automatic PR)&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crrev.com/c/2106983&quot;&gt;CL 2106983: DeprecatedEqual: @import + @charset&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crrev.com/c/2108441&quot;&gt;CL 2108441: DeprecatedEqual: all other ASCII literals&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crrev.com/c/2113394&quot;&gt;CL 2113394: DeprecatedEqual: all other ASCII constants&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crrev.com/c/2114510&quot;&gt;CL 2114510: DeprecatedLower: where only compared with ASCII&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://crrev.com/c/2121937&quot;&gt;CL 2121937: simplify MapDataParamToSrc (NFC)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;fixing-the-bug&quot;&gt;Fixing the bug&lt;/h3&gt;

&lt;p&gt;It’s hard to get a usable &lt;a href=&quot;https://microsoft.github.io/language-server-protocol/&quot;&gt;LSP&lt;/a&gt; setup going for a project as big as a browser.
I switched between &lt;a href=&quot;https://github.com/MaskRay/ccls&quot;&gt;ccls&lt;/a&gt; and &lt;a href=&quot;https://clangd.llvm.org&quot;&gt;clangd&lt;/a&gt; a bunch of times, but I never quite got either working too well.
My main machine is also getting pretty long in the tooth, which made indexing take forever and updating my branches expensive.&lt;/p&gt;

&lt;p&gt;I considered writing an LSP client that would allow me to kick off an index on one of Igalia’s 128-thread build boxes without an editor, but I eventually settled on using &lt;a href=&quot;https://source.chromium.org/&quot;&gt;Chromium Code Search&lt;/a&gt; to jump around and investigate things.
Firefox similarly has &lt;a href=&quot;https://searchfox.org&quot;&gt;Searchfox&lt;/a&gt;&lt;sup id=&quot;fnref:2&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;, but WebKit doesn’t yet have a public counterpart&lt;sup id=&quot;fnref:3&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;I was looking for callers of three deprecated functions, but not all of them were relevant to the bug, and not all of &lt;em&gt;those&lt;/em&gt; needed tests, and so on.
To help me analyse and categorise all of the potential call sites, I wrote some pretty intricate regular expressions for Sublime Text 2.
This one finds all callers of DeprecatedEqualIgnoringCase, with two arguments, where one of them is an ASCII literal that wouldn’t need new tests (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skSK&lt;/code&gt;):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(?x-i)
(?&amp;lt;escape&amp;gt;\\[&apos;&quot;?\\abfnrtv]){0}
(?&amp;lt;literal&amp;gt;&quot;(?:(?=[ -~])[^&quot;skSK]|(?&amp;amp;escape))*&quot;){0}
(?&amp;lt;any&amp;gt;(?:[^(),]|(\((?:[^()]*|(?-1))\)))*+){0}
DeprecatedEqualIgnoringCase
(\s*\(\s*+(?:
    (?&amp;amp;literal)\s*,\s*+(?&amp;amp;any)
    |(?&amp;amp;any)\s*,\s*+(?&amp;amp;literal)
)\s*\))
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After &lt;a href=&quot;https://chromium-review.googlesource.com/c/chromium/src/+/1997014&quot;&gt;my first patch&lt;/a&gt;, which I wrote by hand, I also used those to do the actual replacing, maintaining &lt;a href=&quot;https://bucket.daz.cat/crbug-627682.html&quot;&gt;a huge analysis&lt;/a&gt; of all the cases that remained after &lt;a href=&quot;https://chromium-review.googlesource.com/c/chromium/src/+/2015875&quot;&gt;my second patch&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;writing-some-tests&quot;&gt;Writing some tests&lt;/h3&gt;

&lt;p&gt;Each of the major engines has its own web content tests, and automated tests are strongly preferred over manual tests if at all possible.
All of the tests I wrote were automated, and most were &lt;strong&gt;&lt;a href=&quot;https://web-platform-tests.org&quot;&gt;Web Platform Tests&lt;/a&gt;&lt;/strong&gt;, which are especially cool because they’re a shared suite of web content tests that can be run on any browser.
Chromium and Firefox even automatically upstream changes to their vendored WPT trees!&lt;/p&gt;

&lt;p&gt;Many of my tests were for values of HTML attributes whose &lt;strong&gt;invalid value default&lt;/strong&gt; was a different state to the keyword’s state.
In these cases, I didn’t even need to assert anything about the attribute’s actual behaviour!
All I had to do was write a tag, read the attribute in JavaScript, and check if the value we get back corresponds to the intended feature (bad) or the invalid value default (good).&lt;/p&gt;

&lt;figure&gt;&lt;a href=&quot;/images/igalia-0.png&quot;&gt;&lt;img src=&quot;/images/igalia-0.png&quot; /&gt;&lt;/a&gt;&lt;/figure&gt;

&lt;p&gt;Some legacy HTML attributes are now specified in terms of CSS “presentational hints”, so I checked the results of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle&quot;&gt;getComputedStyle&lt;/a&gt; for those, but the coolest tests I learned to write were &lt;strong&gt;reftests&lt;/strong&gt;.
Very few web platform features guarantee that every user agent on every platform will render them identically down to the pixel, and over time, unrelated platform changes can affect a test’s expected rendering.
Both of these things are ok, but they make it impractical for tests to compare web content against screenshots.
Reftests consist of a test page that uses the feature being tested, and a reference page that should look the same without using the feature.
The reference page is like a screenshot, but it’s subject to all of the same variables as the test page, such as font rendering.&lt;/p&gt;

&lt;figure&gt;&lt;a href=&quot;/images/igalia-1.png&quot;&gt;&lt;img src=&quot;/images/igalia-1.png&quot; /&gt;&lt;/a&gt;&lt;a href=&quot;/images/igalia-2.png&quot;&gt;&lt;img src=&quot;/images/igalia-2.png&quot; /&gt;&lt;/a&gt;&lt;/figure&gt;

&lt;p&gt;Ever heard of the &lt;a href=&quot;https://www.acidtests.org&quot;&gt;Acid Tests&lt;/a&gt;?
&lt;a href=&quot;http://acid2.acidtests.org&quot;&gt;Acid2&lt;/a&gt; is more or less a reftest, because it has &lt;a href=&quot;http://acid2.acidtests.org/reference.html&quot;&gt;a reference page&lt;/a&gt; that only uses a screenshot for the platform-independent parts.
&lt;a href=&quot;http://acid1.acidtests.org&quot;&gt;Acid1&lt;/a&gt; uses &lt;a href=&quot;https://www.w3.org/Style/CSS/Test/CSS1/current/sec5526c.gif&quot;&gt;a screenshot&lt;/a&gt; of the whole test, hence “except font rasterization and form widgets”.&lt;/p&gt;

&lt;p&gt;I had a lot of fun writing my &lt;a href=&quot;http://wpt.live/html/semantics/forms/the-input-element/hidden-charset-case-sensitive.html&quot;&gt;two&lt;/a&gt; &lt;a href=&quot;http://wpt.live/html/semantics/forms/the-textarea-element/wrap-enumerated-ascii-case-insensitive.html&quot;&gt;form-related&lt;/a&gt; tests, because I actually had to submit forms to observe those features’ behaviour.
WPT has server-side testing infrastructure that can help with this, and for such tests, I would need to spin up the provided web server or run the finished product with &lt;a href=&quot;http://wpt.live&quot;&gt;wpt.live&lt;/a&gt;&lt;sup id=&quot;fnref:4&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:4&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;In both cases, I avoided the need for that with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form method=&quot;GET&quot;&amp;gt;&lt;/code&gt; that targets an iframe, plus a helper page that sends its query string back to the test page.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;mathml-tasks&quot;&gt;MathML tasks&lt;/h2&gt;

&lt;p&gt;MathML was meant to be the native language for mathematics on the web, and that’s still true today, but two decades later, browser support &lt;a href=&quot;https://aperiodical.com/2013/11/dark-days-for-mathml-support-in-browsers/&quot;&gt;still has a long way to go&lt;/a&gt;.
There are several reasons for this, notably including &lt;a href=&quot;https://web.archive.org/web/20141214030114/http://www.maths-informatique-jeux.com/blog/frederic/?post%2F2013%2F10%2F12%2FFunding-MathML-Developments-in-Gecko-and-WebKit&quot;&gt;the largely volunteer-driven development&lt;/a&gt; of MathML and its implementations, but over the last few years, Igalia has helped change that on three fronts: &lt;a href=&quot;https://mathml.igalia.com&quot;&gt;writing a Chromium implementation&lt;/a&gt;, improving the Firefox and WebKit implementations, and &lt;a href=&quot;https://www.w3.org/community/mathml4/&quot;&gt;improving the specs themselves&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.w3.org/TR/MathML3/&quot;&gt;MathML 3&lt;/a&gt; was made a Recommendation in 2014, and like any spec, it has shortcomings that only subsequent experience could identify.
Proposals by the &lt;a href=&quot;https://www.w3.org/community/mathml4/&quot;&gt;MathML Refresh CG&lt;/a&gt; like &lt;a href=&quot;https://mathml-refresh.github.io/mathml-core/&quot;&gt;MathML Core&lt;/a&gt; are trying to address them in a bunch of ways, like simplifying the spec, setting clearer expectations around rendering, and redefining features in terms of better-supported CSS constructs.
My remaining tasks touched on some of these.&lt;/p&gt;

&lt;h3 id=&quot;momaxsize&quot;&gt;mo@maxsize&lt;/h3&gt;

&lt;p&gt;Moving onto WebKit, my next task was to remove some dead code.
Past versions of MathML specify a very complex &amp;lt;mstyle&amp;gt; with its own inheritance system that’s incompatible with CSS, as well as several attributes that were rarely if ever used by authors, both of which are a burden on implementors.&lt;/p&gt;

&lt;p&gt;One of those attributes was mstyle@maxsize, which would serve as the default mo@maxsize instead of infinity.
With the former removed from the spec, there was no longer a need for an explicit infinity value, so I removed the code for that.&lt;/p&gt;

&lt;p&gt;It turns out WebKit never got around to implementing mstyle@maxsize anyway, so there was no functional change.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mathml-refresh/mathml/issues/1&quot;&gt;mathml-refresh/mathml#1: simplify the mstyle element&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mathml-refresh/mathml/issues/107&quot;&gt;mathml-refresh/mathml#107: remove explicit mo@maxsize = infinity&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://trac.webkit.org/changeset/259785&quot;&gt;r259785: remove mo@maxsize value “infinity” (NFC)&lt;/a&gt; (&lt;a href=&quot;https://bugs.webkit.org/show_bug.cgi?id=202720&quot;&gt;bug 202720&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;stixgeneral&quot;&gt;STIXGeneral&lt;/h3&gt;

&lt;p&gt;There’s a lot of MathML content that gets rendered like any other text, but stretchy and large operators are a bit more involved than just drawing a single glyph at a single size.
A well-known example of a stretchy operator is square root notation, which consists of a &lt;strong&gt;radical&lt;/strong&gt; (the squiggly part) and a &lt;strong&gt;vinculum&lt;/strong&gt; (the overline part) that stretches to cover the expression being rooted.&lt;/p&gt;

&lt;math display=&quot;block&quot;&gt;
    &lt;msqrt&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/msqrt&gt;
    &lt;mo&gt;=&lt;/mo&gt;
    &lt;msqrt&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;/msqrt&gt;
    &lt;msqrt&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/msqrt&gt;
    &lt;!-- &lt;mspace width=&quot;1em&quot;/&gt;
    &lt;mo&gt;∀&lt;/mo&gt;
    &lt;mi&gt;x&lt;/mi&gt;&lt;mo&gt;:&lt;/mo&gt;
    &lt;mi&gt;x&lt;/mi&gt;&lt;mo&gt;∈&lt;/mo&gt;&lt;mi mathvariant=&quot;double-struck&quot;&gt;R&lt;/mi&gt;
    &lt;mo&gt;∧&lt;/mo&gt;
    &lt;mi&gt;x&lt;/mi&gt;&lt;mo&gt;≥&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt; --&gt;
&lt;/math&gt;

&lt;p&gt;Traditionally this was achieved by knowing where the glyphs for the separate parts lived in each font, so we could stretch and draw them independently.
Unicode assignments for stretchy operator parts helped, but that wasn’t enough to yield ideal rendering, because many fonts use Private Use Area characters for some operators, and ordinary fonts don’t give applications the necessary tools to control mathematical layout precisely.&lt;/p&gt;

&lt;figure&gt;&lt;a href=&quot;/images/igalia-a.png&quot;&gt;&lt;img src=&quot;/images/igalia-a.png&quot; /&gt;&lt;/a&gt;&lt;/figure&gt;

&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/typography/opentype/spec/math&quot;&gt;OpenType MATH tables&lt;/a&gt; eventually solved this problem, but that meant Firefox essentially had three code paths: one for OpenType MATH fonts, one with font-specific operator data, and one generic Unicode path for all other fonts.
That second one adds a lot of complexity, and there was only one font left with its own operator data: STIXGeneral.&lt;/p&gt;

&lt;p&gt;The goal was ultimately to remove that code path, dropping support for the font.
That sounded easy enough until we realised that STIXGeneral remains preinstalled on macOS, as the only stock mathematics font, to this day.&lt;/p&gt;

&lt;p&gt;My task here was to add a feature flag that disables the code path on nightly builds, and gather data around how many pages would be affected.
The patch was straightforward, with one change to allow &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Document::WarnOnceAbout&lt;/code&gt; to work with parameterised l10n messages, and I wrote a cute little data URL test page for the warning messages.&lt;/p&gt;

&lt;pre style=&quot;white-space: pre-wrap; word-wrap: break-word;&quot;&gt;&lt;code&gt;data:text/html;base64,PCFkb2N0eXBlIGh0bWw+CjxtZXRhIGNoYXJzZXQ9InV0Zi04Ij4KPHN0eWxlPgogIG1hdGg6Zmlyc3Qtb2YtdHlwZSB7CiAgICBmb250LWZhbWlseTogTGF0aW4gTW9kZXJuIE1hdGg7CiAgfQogIG1hdGggewogICAgZm9udC1mYW1pbHk6IFNUSVhHZW5lcmFsLCBMYXRpbiBNb2Rlcm4gTWF0aDsKICB9Cjwvc3R5bGU+CjxtYXRoIGRpc3BsYXk9ImJsb2NrIiBtYXRoc2l6ZT0iN2VtIj4KICA8bW8+4oiRPC9tbz48bW8gZGlzcGxheXN0eWxlPSJmYWxzZSI+4oiRPC9tbz4KPC9tYXRoPgo8YnV0dG9uIHR5cGU9ImJ1dHRvbiI+U1RJWEdlbmVyYWw8L2J1dHRvbj4KPHNjcmlwdD4KICBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCJidXR0b24iKS5hZGRFdmVudExpc3RlbmVyKCJjbGljayIsICh7IHRhcmdldCB9KSA9PiB7CiAgICBjb25zdCBzb3VyY2UgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCJtYXRoIik7CiAgICB0YXJnZXQuYWZ0ZXIoc291cmNlLmNsb25lTm9kZSh0cnVlKSk7CiAgfSk7Cjwvc2NyaXB0Pgo=&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Turning the feature flag on broke a test though, and I couldn’t for the life of me reproduce it locally.
Fred and I tried every possible strategy we could imagine short of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/QA/Running_automated_tests/TaskCluster_interactive_session&quot;&gt;interactively debugging CI&lt;/a&gt;, on and off for six weeks, but it looked like the flaky behaviour involved some sort of race against &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@font-face&lt;/code&gt; loading.
Eventually we gave up and disabled the feature flag just for that test, and I landed my patch.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://groups.google.com/g/mozilla.dev.tech.mathml/c/PlVCil2X598/m/LfLuZfSVKyYJ&quot;&gt;mozilla.dev.tech.mathml: original context&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://groups.google.com/g/mozilla.dev.platform/c/ufT7Oc42MEc/m/xiOlQxIECQAJ&quot;&gt;mozilla.dev.platform: intent to deprecate&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.fxsitecompat.dev/en-CA/docs/2020/stretching-mathml-operators-with-stix-general-fonts-have-been-deprecated/&quot;&gt;Firefox Site Compatibility note&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1648335&quot;&gt;bug 1648335: STIXGeneral pref gate breaks semantics-1.xhtml&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://phabricator.services.mozilla.com/D73833&quot;&gt;D73833: STIXGeneral use counter and deprecation warning&lt;/a&gt; (&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1630935&quot;&gt;bug 1630935&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://phabricator.services.mozilla.com/D77067&quot;&gt;D77067: refactor FontFamilyName + FontFamilyList + nsMathMLChar (NFC)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;padding--border--margin&quot;&gt;padding + border + margin&lt;/h3&gt;

&lt;p&gt;Another way to improve the relationship between MathML and CSS has been defining how existing CSS constructs from the HTML world, including the box model properties, apply to MathML content.
In this case, the consensus was that these properties would “inflate” the &lt;strong&gt;content box&lt;/strong&gt; as necessary, making the element occupy more space.&lt;/p&gt;

&lt;p&gt;Existing implementations in WebKit and Firefox didn’t really handle them at all because it wasn’t in the spec, so the last task I had time for was to change that.&lt;/p&gt;

&lt;p&gt;A modern browser starts by parsing documents into an &lt;strong&gt;element tree&lt;/strong&gt;, which is also exposed to authors as the DOM, but when it comes to rendering, that tree is converted to a &lt;strong&gt;layout tree&lt;/strong&gt;, which represents the boxes to be drawn in a hierarchy of position/size influence.
The layout tree consists of &lt;strong&gt;layout nodes&lt;/strong&gt; (Chromium), &lt;strong&gt;renderer nodes&lt;/strong&gt; (WebKit), or &lt;strong&gt;frame nodes&lt;/strong&gt; (Firefox), but these all refer to the same concept.&lt;/p&gt;

&lt;p&gt;I started with Firefox and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mspace&quot;&gt;&amp;lt;mspace&amp;gt;&lt;/a&gt; because that was the only element that could not contain children.
&amp;lt;mspace&amp;gt; represents, well, a space.
It has attributes for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;width&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;height&lt;/code&gt; (height above the baseline), and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;depth&lt;/code&gt; (height below the baseline), each of which can be negative to bring surrounding elements closer together.&lt;/p&gt;

&lt;p&gt;I found the element’s frame node and noticed this method:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;void nsMathMLmspaceFrame::Reflow(nsPresContext* aPresContext,
                                 ReflowOutput&amp;amp; aDesiredSize,
                                 const ReflowInput&amp;amp; aReflowInput,
                                 nsReflowStatus&amp;amp; aStatus) {
  // [...]

  mBoundingMetrics = nsBoundingMetrics();
  mBoundingMetrics.width = mWidth;
  mBoundingMetrics.ascent = mHeight;
  mBoundingMetrics.descent = mDepth;
  mBoundingMetrics.leftBearing = 0;
  mBoundingMetrics.rightBearing = mBoundingMetrics.width;

  aDesiredSize.SetBlockStartAscent(mHeight);
  aDesiredSize.Width() = std::max(0, mBoundingMetrics.width);
  aDesiredSize.Height() = aDesiredSize.BlockStartAscent() + mDepth;

  // [...]
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Reflow is the process of traversing the layout tree and figuring out the positions and sizes of all of its nodes, and in Firefox that involves a depth-first tree of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nsIFrame::Reflow&lt;/code&gt; calls, starting from the &lt;strong&gt;initial containing block&lt;/strong&gt;.
An &amp;lt;mspace&amp;gt; frame never has children, so our reflow logic was more or less to take the three attributes, then return a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ReflowOutput&lt;/code&gt; that tells the parent we need that much space.&lt;/p&gt;

&lt;p&gt;To handle padding and border, we add that to our desired size.
“Physical” here means the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nsMargin&lt;/code&gt; in terms of absolute directions like left and right, as opposed to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LogicalMargin&lt;/code&gt; in terms of &lt;strong&gt;flow-relative&lt;/strong&gt; directions, which are aware of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; (LTR + RTL) and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;writing-mode&lt;/code&gt; (horizontal + vertical + sideways).
We want to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LogicalMargin&lt;/code&gt; in most situations, but MathML Core is &lt;a href=&quot;https://mathml-refresh.github.io/mathml-core/#css-styling&quot;&gt;&lt;em&gt;currently&lt;/em&gt; strictly &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;horizontal-tb&lt;/code&gt;&lt;/a&gt; and sums of left and right are inherently &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt;-safe, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nsMargin&lt;/code&gt; was the way to go here.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;auto borderPadding = aReflowInput.ComputedPhysicalBorderPadding();
aDesiredSize.Width() = std::max(0, mBoundingMetrics.width) + borderPadding.LeftRight();
aDesiredSize.Height() = aDesiredSize.BlockStartAscent() + mDepth + borderPadding.TopBottom();
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That was enough to pass the &amp;lt;mspace&amp;gt; cases in the Web Platform Tests, but &lt;a href=&quot;https://bucket.daz.cat/07d7eb508eaab690.html&quot;&gt;the test page&lt;/a&gt; I had put together to play around with my patch yielded both good news and bad news.
Let’s look at &lt;a href=&quot;https://bucket.daz.cat/21b093f316aa04d9.html&quot;&gt;the reference&lt;/a&gt;, which uses &amp;lt;div&amp;gt; elements and flexbox rather than MathML.&lt;/p&gt;

&lt;figure&gt;&lt;a href=&quot;/images/igalia-3.png&quot;&gt;&lt;img src=&quot;/images/igalia-3.png&quot; /&gt;&lt;/a&gt;&lt;/figure&gt;

&lt;p&gt;The good news was that Firefox already drew borders, or at least border colours, even though the layout of them was all wrong.&lt;/p&gt;

&lt;figure&gt;&lt;a href=&quot;/images/igalia-4.png&quot;&gt;&lt;img src=&quot;/images/igalia-4.png&quot; /&gt;&lt;/a&gt;&lt;/figure&gt;

&lt;p&gt;The bad news was that while my patch made each element look Bigger Than Before, the baselines were misaligned.
More importantly, the &amp;lt;mspace&amp;gt; elements and even the whole &amp;lt;math&amp;gt; elements still overlapped each other… almost as if… their parents were unaware of how much space they needed when positioning them!&lt;/p&gt;

&lt;figure&gt;&lt;a href=&quot;/images/igalia-5.png&quot;&gt;&lt;img src=&quot;/images/igalia-5.png&quot; /&gt;&lt;/a&gt;&lt;/figure&gt;

&lt;p&gt;I fixed the first two problems by adding the padding and border to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nsBoundingMetrics&lt;/code&gt; as well, because that controls the sizes and positions of MathML content.
That left the overlapping of the &amp;lt;math&amp;gt; elements, because while they &lt;em&gt;contain&lt;/em&gt; MathML content, they themselves are HTML content as far as their ancestors are concerned.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;auto borderPadding = aReflowInput.ComputedPhysicalBorderPadding();
mBoundingMetrics.width = mWidth + borderPadding.LeftRight();
mBoundingMetrics.ascent = mHeight + borderPadding.Side(eSideTop);
mBoundingMetrics.descent = mDepth + borderPadding.Side(eSideBottom);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;figure&gt;&lt;a href=&quot;/images/igalia-6.png&quot;&gt;&lt;img src=&quot;/images/igalia-6.png&quot; /&gt;&lt;/a&gt;&lt;/figure&gt;

&lt;p&gt;It turns out that in Firefox, MathML frames also need to report their width to their parent via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nsMathMLContainerFrame::MeasureForWidth&lt;/code&gt;.
With the &amp;lt;mspace&amp;gt; counterpart updated, plus the WPT &lt;strong&gt;expectations&lt;/strong&gt; files updated to mark the &amp;lt;mspace&amp;gt; test cases as passing, my patch was ready to land.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/* virtual */
nsresult nsMathMLmspaceFrame::MeasureForWidth(DrawTarget* aDrawTarget,
                                              ReflowOutput&amp;amp; aDesiredSize) {
  // [...]

  auto offsets = IntrinsicISizeOffsets();
  mBoundingMetrics.width = mWidth + offsets.padding + offsets.border;

  // [...]
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;figure&gt;&lt;a href=&quot;/images/igalia-7.png&quot;&gt;&lt;img src=&quot;/images/igalia-7.png&quot; /&gt;&lt;/a&gt;&lt;/figure&gt;

&lt;p&gt;I also put together &lt;a href=&quot;https://bucket.daz.cat/d0c44db2dd05c7e5.html&quot;&gt;a test page&lt;/a&gt; (&lt;a href=&quot;https://bucket.daz.cat/31ec55671c10bddc.html&quot;&gt;reference&lt;/a&gt;) for the interaction between negative mspace@width and padding, which more or less rendered as expected, but it potentially revealed a bug in the layout of &amp;lt;math&amp;gt; elements that are flex items.
My guess is that flex items use a code path that clamps negative sizes to zero at some point, like we have to do in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ReflowOutput&lt;/code&gt;, resulting in excess space for the item.&lt;/p&gt;

&lt;figure&gt;
    &lt;a href=&quot;/images/igalia-8.png&quot;&gt;&lt;img src=&quot;/images/igalia-8.png&quot; /&gt;&lt;/a&gt;
    &lt;figcaption&gt;Reftest for padding with negative mspace@width: reference page, without patch, with patch.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Margins were trickier to implement because, with Firefox and MathML content at least, the positions of elements are the parent’s responsibility to calculate.
I spent a &lt;em&gt;very&lt;/em&gt; long time reading &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nsMathMLContainerFrame&lt;/code&gt;, which is the base implementation for most MathML parents, and eventually figured out where and how to handle margins.
With a patch that updates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RowChildFrameIterator&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Place&lt;/code&gt;, and &lt;a href=&quot;https://bucket.daz.cat/05499718d719a59b.html&quot;&gt;yet another test page&lt;/a&gt; (&lt;a href=&quot;https://bucket.daz.cat/31ec55671c10bddc.html&quot;&gt;reference&lt;/a&gt;) that passed with my patch, we were close to having a template for the remaining MathML elements!&lt;/p&gt;

&lt;figure&gt;
    &lt;a href=&quot;/images/igalia-9.png&quot;&gt;&lt;img src=&quot;/images/igalia-9.png&quot; /&gt;&lt;/a&gt;
    &lt;figcaption&gt;Reftest for margin: reference page, without patch, with patch.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;You can see my approach over at &lt;a href=&quot;https://phabricator.services.mozilla.com/D87594&quot;&gt;D87594&lt;/a&gt;, but the patch needed reworking and I ran out of time before I could land it.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mathml-refresh/mathml/issues/14&quot;&gt;mathml-refresh/mathml#14: padding + border + margin&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1658135&quot;&gt;bug 1658135: &amp;lt;math&amp;gt; layout changes depending on presence of &amp;lt;mrow&amp;gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://phabricator.services.mozilla.com/D86471&quot;&gt;D86471: implement padding/border layout for &amp;lt;mspace&amp;gt;&lt;/a&gt; (&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1658121&quot;&gt;bug 1658121&lt;/a&gt;)
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://github.com/web-platform-tests/wpt/pull/25505&quot;&gt;web-platform-tests/wpt#25505: workaround for bug 1658135 (automatic PR)&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://phabricator.services.mozilla.com/D87594&quot;&gt;D87594: implement margin for nsMathMLContainerFrame children&lt;/a&gt; (&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1663867&quot;&gt;bug 1663867&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bucket.daz.cat/07d7eb508eaab690.html&quot;&gt;reftest for padding + border on &amp;lt;mspace&amp;gt;&lt;/a&gt; (&lt;a href=&quot;https://bucket.daz.cat/21b093f316aa04d9.html&quot;&gt;reference page&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bucket.daz.cat/d0c44db2dd05c7e5.html&quot;&gt;reftest for padding with negative mspace@width&lt;/a&gt; (&lt;a href=&quot;https://bucket.daz.cat/4e8f5c01f4642893.html&quot;&gt;reference page&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bucket.daz.cat/05499718d719a59b.html&quot;&gt;reftest for margin on &amp;lt;mspace&amp;gt;&lt;/a&gt; (&lt;a href=&quot;https://bucket.daz.cat/31ec55671c10bddc.html&quot;&gt;reference page&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;acknowledgements&quot;&gt;Acknowledgements&lt;/h2&gt;

&lt;p&gt;This internship was incredibly valuable.
While I was only able to finish the first trimester for mental health reasons, over the last nine months I’ve learned C++, learned how the web platform and browser engines work, gained ample experience reading specs, worked with countless people in the open-source community, and contributed to three major engines plus the Web Platform Tests.&lt;/p&gt;

&lt;p&gt;Were I able to continue, I would also look forward to (&lt;a href=&quot;https://github.com/whatwg/html/pull/3072&quot;&gt;more&lt;/a&gt;) experience contributing to specs, and probably helping Igalia with their &lt;a href=&quot;https://mathml.igalia.com&quot;&gt;MathML in Chromium&lt;/a&gt; project.
In any case, my time with the collective has only strengthened my desire to someday join full-time.&lt;/p&gt;

&lt;p&gt;Thanks to Caitlin for her advice and support, Eva and Javier and Pablo for getting me settled in so quickly, Manuel and Fred and Rob from the Web Platform team, and Yoav and Emilio for their help on the Chromium and Firefox parts of my work.&lt;/p&gt;

&lt;hr /&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Windows is the other major platform that does this. Check out &lt;em&gt;The Old New Thing&lt;/em&gt; by Raymond Chen to learn more. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Searchfox more or less &lt;a href=&quot;https://billmccloskey.wordpress.com/2016/06/07/searchfox/&quot;&gt;supersedes&lt;/a&gt; MXR and DXR. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;Igalia has a Searchfox-based WebKit code browser, and I found it useful, but it’s not yet ready for public consumption. &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:4&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;See also &lt;a href=&quot;https://wpt.fyi&quot;&gt;wpt.fyi&lt;/a&gt;, which tracks results of each test case across major browsers. &lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;</content><author><name></name></author><category term="home" /><category term="igalia" /><summary type="html">I was looking for a job late last year when I saw a tweet about a place called Igalia. The more I learned about them, the more interested I became, and before long I applied to join their Web Platform team. I didn’t have enough experience for a permanent position, but they did offer me a place in their Coding Experience program, which as far as I can tell is basically an internship, and I thoroughly enjoyed it. Here’s an overview of what I did and what I learned.</summary></entry></feed>