They said it couldn’t be done. That HTML + CSS wasn’t a real programming language. Hah!
I’ve proved them wrong! I’ve finally solved Advent of Code 2021 Problem #1 using (basically) only HTML and CSS.
Solution with test data – Solution with “real” data.
What to do when you’ve done everything Link to heading
I might not have done exactly everything, but I have done a lot of different kinds programming. I’ve programmed network stuff. I’ve programmed competitive programming. I’ve programmed games. I’ve programmed a CLI program that bookmarks your terminal. I’ve programmed a wedding webpage.
Doing lots of different things is great. However, it becomes increasingly hard to do something that you’ve never done before. Which might be why I’ve focused a bit on non-programming things over the last couple of years, like bread baking.
I know, I know. People are dying in Ukraine. Children are starving in Yemen. And here I am trying to find more problems. Whatever, I’m interested in solving my problems, not the worlds problems.
For the longest time I’ve had no problem that really engages me. That was until a few days ago. As usual, I was out on a walk thinking about this and that. I started thinking about Advent of Code. It is common to try something new with each AoC and I like that tradition. It keeps you on your toes, and it often leads to new knowledge and insights. Since I’ve dabbled a little bit more in CSS, React, and Material UI lately I’ve been more exposed than normal to Web technologies than usual. A thought struck me that I should try to use HTML + CSS to solve Advent of Code this year.
At first I thought it was a stupid idea. But the more I thought about it, the more sense it made. “Wait, why isn’t HTML + CSS turing complete?”. You can do so many magic things with it nowadays. Maybe HTML + CSS turned turing complete and we just didn’t notice? I abruptly changed course and headed home to program HTML + CSS until I could answer my burning question.
Solution deep dive Link to heading
Problem: I will give you numbers a, b, c, d, …. You will figure out how many steps in the sequence go from a smaller to a greater number.
Example: 1, 2, 3, 0, 5. Answer is 3.
Problem #2: Same thing, but instead of a, b, c, d we will compare a + b + c with b + c + d.
Representing the data Link to heading
My first problem was in how I would represent the data. HTML doesn’t help me calculate anything, and JavaScript is off the table. The only candidate left is CSS. I had to perform a multi-step approach just to access the input data of the problem.
HTML let’s you define data attributes. These are attributes that are attached to an HTML element, and are intended to have application-specified meaning. It is a simple and extensible way of representing data.
The list
- 1
- 5
- 20
is turned into
<section>
<div data-integer-value="1"></div>
<div data-integer-value="5"></div>
<div data-integer-value="20"></div>
</section>
Then, in our CSS, we can create a selector for these data attributes like this:
div[data-integer-value="1"] {
}
div[data-integer-value="5"] {
}
div[data-integer-value="20"] {
}
The last piece of the puzzle in representing the data is accessing them as variables in CSS. Since CSS is declarative we can’t just call a method like div.getDataAttributeValue(). We have to be a bit more heretical unorthodox.
In each selector with a data-attribute we need to introduce a CSS variable. Formally called CSS Custom Property. The above CSS then becomes:
div[data-integer-value="1"] {
--integer-value: 1;
}
div[data-integer-value="5"] {
--integer-value: 5;
}
div[data-integer-value="20"] {
--integer-value: 20;
}
Now, inside a selector that is shared between the above three, I will have access to the variable --integer-value. For example I could change the width of any div to the --integer-value like this:
div[data-integer-value] {
width: var(--integer-value);
}
In order to get this to work I had to code a teeeeny tiny bit of python. Hey, hey, no need to get your pitchforks out. It is just to generate the HTML and CSS. It is the CSS-variant of creating the array sequence = [1, 5, 20].
If you’ve looked at the examples, then the generated HTML are the divs in the <section> and the CSS is in the file helper-vars.css. Since Firefox and Chrome was obviously never built to be IDEs for real CSS and HTML programming, I had to prune the CSS rules so that only rules matching the numbers from the input would be kept. In practice we need to prune it, but theoretically there is nothing stopping us from adding a CSS rule for every number in the world.
Okay, we’ve got data attributes in HTML → a bunch of data attribute selectors in CSS → CSS custom properties and we have finally managed to represent our data in CSS by using the --integer-value variable.
Comparing Values Link to heading
The problem we are trying to solve is answering if a < b. To do this, we need two values we can compare. We’ve only found a way to capture a inside --integer-value. We need another variable for b. I tried multiple approaches until I found something that worked
- Having the user click a button for each input number together with
:focusand the sibling selector.- The idea was that I might be able to simulate a
for-loop somehow and skip having to represent two values. I got thefor-loop functionality, but not the capability to sum a variable.
- The idea was that I might be able to simulate a
- Using
divs indivs and jump between carrying a--lhsvariable and--rhsvariable.- This is just not how custom properties work.
- Using the ultra-new
:has()pseudo-class.- While technically possible I had to enable strange Chrome flags to get it to work. I can’t show this to someone and say “Oh, and you have to enter this super secret flag which destroys your warranty, and totally won’t do you any harm if you go to the bad part of the internet”
- And I might also have crashed Chrome using
:has()together with the recursivedivs. ¯\_(ツ)_/¯
- And I might also have crashed Chrome using
- While technically possible I had to enable strange Chrome flags to get it to work. I can’t show this to someone and say “Oh, and you have to enter this super secret flag which destroys your warranty, and totally won’t do you any harm if you go to the bad part of the internet”
The solution I ended up with was a lot simpler than most of my prior attempts. You can just use the sibling selector. Let’s say I want any div following a div[data-integer-value="123"] to have a variable --prior-div-integer-value representing the prior divs value (but accessible in the “current” div). Then I could do the following:
div[data-integer-value="123"] + div {
--prior-div-integer-value: 123;
}
And just like that we now have --integer-value and --prior-div-integer-value as variables we can compare and calculate in each relevant div.
Comparing values - But with Math Link to heading
In most other programming language environments we have complicatedly advanced branching constructs like if. In CSS we don’t have that luxury. However… We have Maths™.
Inspired by a technique common in optimization problems like sorting, binary search, and other competitive programming tasks. We can use math to change a branching statement into a single expression. Andrei Alexandrescu walks through this concept in one of his CppCon talks. The talk is pretty good (he is such a good speaker) and well worth a watch. An example of this branchless technique is shown around 21:05.
In CSS we have access to max() and min() as logical comparisons. In a sense we can hijack the if-else found inside the max() and min() functions and use that branching logic in our code. It’s like the logic of the function is seeping out into our program.
Example: We want to compare a < b. We use max(0, b - a). We then have two cases. a) a is greater or equal to b. max(0, b - a) gives us 0 b) b is greater than a and max(0, b - a) gives us a positive number.
We can clamp the value as well (either using clamp() or min(1, max(0, ...))). With this clamping method we will get a resulting value of 1 if a < b and 0 if a >= b.
Since we are responsible coders , who are writing algorithms in CSS, and value clean code we create yet another variable to hold our value:
div[data-integer-value] {
--resulting-value: calc(
var(--integer-value) - var(--prior-div-integer-value)
);
--clamped-resulting-value: min(1, max(0, var(--resulting-value)));
}
In a language like python we would simply calculate the sum of all --clamped-resulting-value, but surely at this point you realize we aren’t looking for the simple solution.
Summing values using Counters Link to heading
Summing was easy. We can use CSS Counters to store our value. In CSS we can use counter-increment: [counter-name] [counter-value] to increase the counter with the specified name with a specified value like var(--clamped-resulting-value).
I some sense we can use counters as »arbitrary registers storing arbitrary values«.
To display our calculated value we have to use a bit of CSS magic again. We can use the ::after pseudo-element to basically create a completely new element after some other selectable element. Let’s say I am not that grammarly gifted and forget to add dots at the end of my sentences, but I always wrap them in <p>-tags.
I can then use a rule like the following to make the browser add . at the end of my sentences.
p::after {
content: ".";
}
A common use-case with ::before – ::afters cousin – is to display indices for a list using, you guessed it, counters. A counter can be displayed as such:
p::after {
content: counter(counter-name);
}
Some nice styling Link to heading
With all my newfound knowledge of CSS, I decided to make the divs a bit prettier as well. A green background if a < b, and a red background if a >= b, and a blue background for the first element. I also decided to display the integer value using the ::after selector and center the text in the div.
At this point I was rather proud of myself for managing to solve the first part of the problem and displaying the solution nicely. So I went to bed to tackle the next part another day.
Solving the second part Link to heading
The second part of the AoC problems tend to be harder, so I was interested in seeing how hard it would be in comparison to the first.
Instead of comparing a and b, we just had to sum a, b, c together and compare with b, c, and d. Not that much harder unfortunately.
I expanded the helper variables by creating variables that not only spanned the prior div. But also the prior divs prior. And the prior divs prior divs prior. Like this:
div[data-test-attribute="20"] {
}
div[data-test-attribute="20"] + div {
}
div[data-test-attribute="20"] + div + div {
}
div[data-test-attribute="20"] + div + div + div {
}
After this I just had to do basically the same calculations again.
I did still run into some issues tough. Counters….
My previous assumption about counters was incorrect. Counters rely on context in the HTML. I’ve not read and understood everything about it, but if I have five HTML elements next to each other, each adding 1 to the counter, I will be able to display 5. It works like you would expect. However, if I instead move the additions into child elements, the counter will turn out to be 0. There is also a relation between parent components and child components. If I re-use the same counter in child components and use the CSS function counters(...) (notice the “s”), I will get a string with every “level” of the counter. For example 1.2.3, 5.4.4, or 20.1.2.
This realization had implications for my solution.
For example, I could not find a way to keep two counters active at the same time. Only the first took precedence and the other was reset to 0.
I did not have » an arbitrary number of registers storing arbitrary values «.
I had » a register storing an arbitrary value «. Sigh.
I had to resort to yet another unholy unorthodox technique. Bit packing. My guesstimate was that the number would be somewhere in the range 0-100000, therefore I can reserve the first 100000 numbers for the first answer and the rest of the numbers for the second answer. In code it is as easy as counter += first_value * 100000 + second_value. This is also why you see the answer on such a strange format (X[0]+Y).
Summary Link to heading
With all the techniques I’ve listed I did manage to solve the first problem of AoC 2021. I honestly didn’t expect to do that when I started this journey.
While researching I also found that some beautiful nerds people online have proven “turing completeness” by encoding Rule 110 in CSS3, assuming you allow user interactions.
Myself, I don’t feel like I’m done exploring how far from grace you can fall using just HTML and CSS. Right now I plan on using HTML + CSS for this years AoC just to see how far you can stretch real HTML and CSS programming.