Update: Contrary to my whining down below, I spent the weekend playing with tree-sitter, found the bug, and have submitted a PR.

When you've got twenty years of developer experience, there is one source of frustration at work that can be greater than any other: when you can see the promised land but you know you can't get there and, worst of all, the wall between you and there is barely ankle high.

I am not, by any stretch of the imagination, an expert on parsers and scanners. I've written a few DSLs in my time and always leaned on something commonly available, usually either s-expressions, a readily available configuration language like YAML or TOML, or just a cut-down version of the host language with specificity for my DSL needs.

So let's talk about Shatterfly.

Patternfly is the CSS library we use at authentik to define our product, but our web framework (toolkit, actually. Library? Not a framework. Anyways...) is Lit. Lit is a toolkit for building Web Components.

Web Components use the browser's ShadowDOM, and each individual ShadowDOM has its own stylesheet, almost completely independent of anything in its parent page. This makes styling Web Components in a consistent way notoriously difficult.

Patternfly 4 was okay with this, but it has no useful light/dark mode story. And Patternfly has moved on: Patternfly 5 has a better light/dark mode, and Patternfly 6 more or less treats it as a solved problem via CSS Custom Properties.

Between Patternfly 4 and Patternfly 5, IBM bought Patternfly. (Well, IBM bought RedHat, which was the principle sponsor and code-owner of Patternfly). Patternfly 5 is optimized for React, and Patternfly 6 is basically, "Yeah, we're just a react toolkit at this point." But the base library is still there, and still usable, but for one problem.

That problem can be seen with just a few lines of code:

:root {
  --pf-v5-c-page--BackgroundColor: var(--pf-v5-global--BackgroundColor--light-300);
  --pf-v5-c-page--inset: var(--pf-v5-global--spacer--md);
  --pf-v5-c-page--xl--inset: var(--pf-v5-global--spacer--lg);
  ...
}  

You see that :root there? That's what you use if you want to define variables that apply to the whole page. But the ShadowDOM doesn't have :root, it has :host, and if you want the global background color to apply globally to your components, you need to define an entire new collection of resets that apply to your ShadowDOM components.

Which Patternfly doesn't have.

There are other issues, the biggest of which is that Patternfly sometimes mixes CSS Custom Property declarations with concrete declarations. On the other paw, Patternfly does provide every component with a "pure HTML" document entry with its designs. Each demo is written in Glimmer, a successor to Handlebars.

Patternfly is actually written in SCSS, so I had this absolutely lunatic idea: What if I could automatically shatter Patternfly into smaller, more modular units, and each unit would be a mixin you could import into your component at will? Both Vite and ESBuild support importing CSS into Web Components via bundling, and both have Sass plug-ins.

Patternfly is too big to do this by hand, and too complex to do it with Perl. (I don't know Raku at all, sorry.) If I had a proper syntactical parser, I could break it into the AST, discover the prefix I would need (the source example above would become something like @mixin page-root-properties and @mixin page-root-defaults), and then write those to separate files, followed by a unifying file that would just say:

@use './page-root-properties';
@use './page-root-defaults';
:root {
    @mixin page-root-properties.page-root-properties;
    @mixin page-root-defaults.page-root-defaults;
}

... and now I could easily import and mix and match those into any component I cared. Or I could pre-process them against nanoCSS and strip them down to the bare minimum. With further pre-processing and some intelligent namespacing I could have a minimal CSS package that worked well for each component, without having to Import The World.

Lunatic idea, right? Parsers are a pain in the neck.

Enter Tree-sitter. Tree-sitter is this amazing new parsing algorithm and toolkit that lets you write GLR parsers in a fairly straightforward language based on JavaScript. There are existing grammars for literally dozens of programming languages, including SCSS!

I will not go into what it took to get Tree-sitter to build on MacOS. Let's just say that Apple has one of the most developer-unfriendly environments I've ever worked in, and figuring out that G++ was symlinked to Clang++, which in turn is hardlinked to Clang, was just gross. I hope whoever made that decision has changed their name, entered a monastery, and asks God every day for forgiveness.

Unfortunately, tree-sitter-scss has a bug. The Dart implementation of SCSS straight up says "Sass's grammar requires arbitrary backtracking," which is not a feature Tree-sitter supports. [Edit: Tree-sitter does support arbitrary backtracking; it's a GLR parser, after all. You have to enable it for any rulesets that support it, because it's exponentially slower than the LR(1) parsing Tree-sitter normally uses.] And right now, if the tree-sitter-scss tries to parse a CSS custom property with a Sass variable embedded in it, and that variable references another CSS custom property with a Sass variable embedded in it, it thinks its looking at a nested pseudo-class definition.

Patternfly uses a lot of references that way.

I've been hacking all day on the grammar for tree-sitter-scss (and tree-sitter-css). As far as I can tell, the double-dash prefix for CSS custom properties is unique to custom properties and can't be used for class names, identifiers, or other such objects.

But they can still be block identifiers when used with @color-profile, @font-palette-values, and probably a whole lot of other things. And the newest CSS Custom Properties @property syntax is a whole new can of worms. I thought I could just distinguish between places where the identifier is a "classic" identifier (one where CSS Custom Properties wouldn't be legal), but my parse-fu is weak and I just made the test fail.

My whole plan relied on there being a safe, easy way to make Patternfly easier to import in smaller, more atomic units. But Tree-sitter-scss is just out of reach at the moment, much to my frustration. I can see the promised land, but there's this tiny bug in the way.

Dammit.

Worse yet, the Glimmer code for most of these components is ridiculously straightforward, and the Patternfly people have been wonderfully consistent. An accessory to Shatterfly could take the Glimmer files and process the HBS contents into a web component with a pre-configured render() method, the conditionals converted into Lit @property() declarations and the embedded blocks converted into @slots(). You get 80% of the way to a full Web Component version of Patternfly for free by leveraging the patterns people have honored.

But, sigh, no dice. Shatterfly is dead until I can figure out why SCSS is parsed differently from the CSS when Sass variables are involved, and that's not gonna happen anytime soon, which is just frustrating as hell.

The promised land is visible Right. Over. There. and the only thing between it and me is this ankle-high bug.