My Journey Creating CSS Extractor

I recently came across a blog post written by another engineer. He stated the following: “When I work on a project, I always include doing a write-up post about it in the scoping.” I found that thought process very interesting. So I decided to follow some of this advice and write about one of my favorite side projects that I created recently.

It’s called CSS extractor. It was not a commercial success but there a few lessons that it tought me. Many of these lesson we’re realized when doing this write up.

Building a Website

When I was working a technology consultant for Law Firms, I began working on a website that would become the Integrand main website. Many of the same pieces that started up in that code-base ended up in the same blog that you’re reading today. Developing that web framework is a discussion for a different time.

I set out to find a website template since my design skills and implementing a design in CSS were pretty trash. What I ended up finding when trying to find a template was that the most beutiful websites and starter kits we’re built with Tailwind CSS. I always have found myself trying to fall back to the most simple tools I could and learning another styling framework was not in the cards. So I made an interesting decisison to go ahead and copy the output source code that was being rendered onto my browser.

This was a manual nightmare. I would spend hours building this components by manaually copy and pasting the definitions from the source css stylesheet into my own. To speed this process up, I spent money on contractors that I would teach how to read ths input HTML and CSS stylesheets and copy what I wanted into the format that I wanted. This was time and money that was just spent on copy and pasting styles. There had to be a better way.

Solving the Problem with Automation

Turns out when your a programmer, you have the power to automate things with software. I’d figured that I spent a copious amount of time doing this, that I would go ahead and automate the process for myself. At this point, I was in Dubai and I was sitting in a Starbucks that I would go to work. It was located in a random Emar(the company that owns practically all the land in the Emirate) office park.

Every morning I would get up and march over to this Starbucks and grind out code. During one of these sessions, I had met another developer name Keem based out of the UK. He had asked me if I used ChatGPT and AI to code. I hadn’t but after talking with him I figured I give it a try and see if I could get it to help me in some what.

Becoming a Prompt Engineer

My prompt engineering skills we’re garbage and I would write things like a noob to ChatGPT(These are taken right out of my ChatGPT chat console):

> Write a python script that extracts css class of a given css classname of a css file

> improve the script to include nested classes in "@media" queries

> This does not work when there are multiple classes inside the "@media" query. 
Fix it so it works with multiple class definitions in the "@media" query

> This does not work with the example given

What I would come to find when programming like this is that it would actually work - kinda. It gave me back a solution written in Python that would rely on regex pattern matching to find a specific class and then replace it. Except for all the edge cases.

Okay it so it spit something out but would not actaully work. After trying to beat my head against the wall to tell ChatGPT to fix the crap that it spit out, I got a very limited version of it to work. The breakdown ended up looking like the following:

1. Split CSS stylesheet into regular and @Media styles
2. Split the regular CSS into psudo selectors and regular CSS class
3. Split the @Media CSS into psudo selectors and regular CSS class
4. Combine the classes for a single element into a a single new class
5. Do some messy string building to output a new CSS file

An example of these AI slop and my clean could would end up looking like:

def css_class_converter(css_content: str, new_class_name: str, class_name_array: List[str]):
    regular_css_buffer = ''
    at_media_css_buffer = ''
    not_found_regular_css_class = []
    not_found_media_css_class = []

    for class_name in class_name_array:
        regular_class_buffer = []
        at_media_class_buffer = []
        
        reg_res = extract_regular_css_class(css_content, class_name)
        if reg_res:
            regular_class_buffer.append(reg_res)
        else:
            not_found_regular_css_class.append(class_name)
        
        at_media_res = extract_class_from_media_queries(css_content, class_name)
        if at_media_res:
            at_media_class_buffer.append(at_media_res)
        else:
            not_found_media_css_class.append(class_name)

        if regular_class_buffer:
            regular_css_string = '\n'.join(regular_class_buffer)
            regular_css_buffer = regular_css_buffer + "\n" + regular_css_string

        if at_media_class_buffer:
            at_media_css_string = '\n'.join(at_media_class_buffer)
            at_media_css_buffer = at_media_css_buffer + "\n" + at_media_css_string

    all_regular_css = write_regular_css_to_new_class(regular_css_buffer, new_class_name)
    all_at_media_css = write_at_media_to_new_class(at_media_css_buffer, new_class_name)

    classes_not_found = list(set(not_found_regular_css_class).intersection(not_found_media_css_class))

    new_CSS_buffer = ''
    new_CSS_buffer = new_CSS_buffer + all_regular_css
    new_CSS_buffer = new_CSS_buffer +"\n\n"
    new_CSS_buffer = new_CSS_buffer + all_at_media_css
    if classes_not_found:
        new_CSS_buffer = new_CSS_buffer +"\n"
        classes_not_found_string = ' '.join(classes_not_found)
        new_CSS_buffer = new_CSS_buffer +"\n"
        new_CSS_buffer = new_CSS_buffer + f"/* Classes not found for {new_class_name}*/"
        new_CSS_buffer = new_CSS_buffer +"\n"
        new_CSS_buffer = new_CSS_buffer + f"/* {classes_not_found_string} */"
    new_CSS_buffer = new_CSS_buffer +"\n\n"
    return new_CSS_buffer

This was a valuable lession. AI can be helpful if you can breakdown the problem into smaller bit size pieces. Similar to working in a company and you feed junior devs small stories.

Searching For a Better Solution

If you know anything about CSS, is there are lot of edge cases and rules to think about. I did not know this at the time. I had worked with a ton of css styles myself, so I knew the rules in myhead but had no idea about how to encode that. It’s funny how you can develop for how something works but don’t know the technical specification of it. That’s just the power of working at something for a long period of time.

I got very basic examples to work. However, I was still running into subtle bugs that we’re irritating to solve. I did some digging to stackoverflow where I came across a reponse that said something along the lines of:

Regex is a poor solution for CSS parsing

This lined up exactly to the experience I was having. So I went on the offensive and began to research a CSS parsing library.

Finding TinyCSS2

Okay so I new I needed to actually parse the CSS to get it to do what I wanted. I did some digging and turned up some libraries. I had this script working in Python so I was only interest in Python based solutions. I found TinyCSS2 which seemed exactly what I was looking for. Heres the introductory snippet

tinycss2 is a low-level CSS parser and generator written in Python: it can parse strings, return objects representing tokens and blocks, and generate CSS strings corresponding to these objects.

Okay great. But there was plenty more work to do after reading the next paragraph

Based on the CSS Syntax Level 3 specification, tinycss2 knows the grammar of CSS but doesn’t know specific rules, properties or values supported in various CSS modules.

Damn still have to encode a bunch of rules.

Implementing TinyCSS2 for Parsing CSS

Turns out the structure that I had created previously was useful. This is becuase I had already ecoded some of the rules of CSS. I already had done some of this by seperating out the logic between regular CSS, @Media queries and Psudo selectors. Now I just had to replace the regex based parsing to the TinyCSS2 based parsing.

From Project to Product

I figured that if I was suffering from this problem of copying CSS from websites I liked, that others probably had a similar issue. I decided to package this code that I wrote into a product that others could use.

Open Source Implementation

The first version was a command-line utility that required manual file handling. This first version was also released as a open source project.

Initially, it was a file-based solution where users provided input HTML and CSS files. These were transformed into a mapping file (encoded in JSON), which was then used to generate the output CSS and HTML files. However, this workflow was cumbersome.

Creating a Web App

Eventually, I shifted to a web app for a more user-friendly experience. Users could paste input files into text areas and easily copy the output into their website builder. I built this using Django

Marketing Go Around

If you want a product to be used and payed for by other people, you need to market it. As it turns out, I completely sucked at this.

My marketing strategy ended up looking like the following:

1. Release Open Source Repo
2. Post About it on open source and software subreddits.
3. Release the web app version
4. Update the Repository Readme to link to the the website
5. Do a launch on Reddit that the web version is avaliable.

Starting out as an open source project I was able to pick up some buzz around the initial repository. I used this as a way to validate the that idea is worth pursuing. Turns out people who like open source software may not be the same as those who will pay for a simplified web version.

Looking back this was a very rudimentary strategy. Turns out that online marketing is a bit harder than drafting up a couple of posts and linking to websites. My execution sucked, copy was shitty and it was hard to discern what the product did unless you we’re a technical person.

Looking back there was so much I didn’t do right and spent a lot of time on. This has been the most painful part of the process. It really exposed some holes in my skillset.

Reflecting on (Non)-Success

Was this project a success? Commercially, not really—there isn’t a large audience for this type of tool. Even I stopped using it after acquiring enough components. It didn’t solve a big enough problem.

However, the project contributed to my personal growth. I successfully leveraged AI to assist with coding, figured out how to create and deploy a Django application and many marketing lessons. Additionally, it deepened my understanding of Django, giving me a newfound appreciation for web frameworks like Django and Rails.

Moving forward, I plan to document my experiences with future projects in a similar manner. Stay tuned for more!

More sweet content?

Stay up to date!

I don't sell your data. Read my privacy policy .

Related Articles