Beej's Bit Bucket

 ⚡ Tech and Programming Fun

2010-12-18

m4 Macros and CSS

I'm certainly not the first person to consider this, but it's a simple enough technique for managing CSS that it bears repeating.

The problem: your CSS has a bunch of constants hard-coded in it all over the place, because CSS doesn't support the concept of variables.

Perhaps the original theme color was #1122ff, and so you have CSS like this, where the string 1122ff appears about 8 million times in your various CSS files:

#somediv {
    background-color: #1122ff;
}

.othertext {
    color: #1122ff;
}

But then you find out that the new theme color is going to be #2211ff and can you please change it. Sure there's search-and-replace, but there might be false positives, and it's not easy to verify that everything replaced well.

But there is a better way!

Warning

There are some pretty full-fledged tools to work specifically with CSS. Most notably we have less and Sass, but there are others. These are great tools that work specifically on CSS and have some wonderful features.

See the "silver lining" part of the next section for why you might want to use m4, anyway. At least this page serves as a simple introduction to m4!

The Basics

Using the concept of "macros", we can substitute one string of characters for another.

Macro processors have been around for quite some time. C has one built into it, in fact. Most assemblers have them. And there's a rather infamous macro processor known as "m4".

Many people have exposure to m4 in both the configuration of sendmail, and GNU's autoconf tools, and, to be frank, it's rare you hear good things about them. I chalk this up to the fact that m4 can be made over-the-top-super-zany-9000 and when normal people look at it, they flip out.

That's OK.

Also, m4 doesn't work with multibyte character encodings, but in many circles these are rare in CSS.

There is a silver lining, and it's something like this:

  1. m4 can be used in a basic sense in a way that's still human-readable.

  2. m4 is lightweight and fast.

  3. m4 is easy to run.

  4. m4 is easy to install, with very few dependencies.

  5. m4 is part of POSIX, and is widely available—and might already be installed on your system.

  6. GNU m4 will support multibyte characters someday. Probably.

Let's write an m4 script, call it "foobar.css.m4" that takes that color constant out and puts it in a macro. (If you're going to copy this, take care to note the open and close quotes are backticks (`) and apostrophes (')—more on that later):

define(`THEMECOLOR', `#2211ff')dnl

#somediv {
    background-color: THEMECOLOR;
}

.othertext {
    color: THEMECOLOR;
}

And then we run m4 from the command line and redirect the output to a file (or don't and just see it on standard-out):

$ m4 foobar.css.m4 > foobar.css

Then this is the result:

#somediv {
    background-color: #2211ff;
}

.othertext {
    color: #2211ff;
}

Ta-daa! So now if you want to change the color, all you have to do is change the define() at the top of the file and run it through m4 again.

Let's check out that m4 define:

define(`THEMECOLOR', `#2211ff')dnl

There are two things that are weirdly unexpected here.

First of all, note the quotes around the elements. The open quote is a backtick (`) and the closing quote is an apostrophe ('). What this does is tells m4 to not expand any macros it finds between the quotes. So here, just in case you have a macro called "THE", it won't replace the THE in THEMECOLOR, because `THEMECOLOR' is quoted.

(If you don't like the quotes, you can change them to other characters using m4's changequote() built-in.)

The second weird thing to notice is that dangling "dnl" at the end of the line. This tells m4 to "delete through newline", or, in order words, ignore everything from here to the start of the next line. If you don't have this there, the newline at the end of the define() line will be emitted into the final output, leading to an extra line in the file at that point. For CSS, it's harmless, so the dnl isn't really necessary, likely. And maybe you're running it through a CSS compressor anyway, if this is all part of an automated build. But if you don't like the extra newlines, add the dnl. It's really very very common in m4 code.

One more final note is that if you want your macro directly beside other text, you'll have to put parentheses after the name to separate it from that text. The parentheses tell m4 that you're invoking the macro with no arguments. This is the same as the normal case where you use no parens, except it's explicit.

To demonstrate, let's say we want to show the string "3490", and we have it defined in two parts, "34" and "90":

define(`THIRTYFOUR', `34')
define(`NINETY', `90')

THIRTYFOURNINTY

When we run this, it outputs:

THIRTYFOURNINETY

What happened to the macro expansion? Well, obviously we didn't declare a macro called THIRTYFOURNINETY, so m4 didn't substitute anything. But if we use the parens to explicitly say the macros have no arguments:

define(`THIRTYFOUR', `34')
define(`NINETY', `90')

THIRTYFOUR()NINETY

We get our expected result:

3490

So that's the basics.

Including External Files

Maybe you have a few other CSS files and you want to include them into the main final CSS file during the m4 run. This is easily accomplished with the include() built-in:

define(`THEMECOLOR', `#2211ff')dnl
include(`common.css.m4')dnl

This will bring in another file, just as if you'd entered it there.

(There's also sinclude() which will silently do nothing if the file doesn't exist.)

Doing Math

One of the niftier things you can do with m4 is integer math with the eval() macro.

Now I've been struggling to think of an example where this would be required, since virtually everything can be done with relative and absolute positioning in CSS, with proper nesting of the elements in the HTML. But nevertheless, here is an example using two absolutely-positioned divs, where we want thing2 to be 40 pixels to the right of thing1:

define(`XPOS', `300')dnl

#thing1 {
    left: XPOS()px;  /* m4 expands this to "300px" */
}

#thing2 {
    left: eval(XPOS + 40)px;  /* m4 expands this to "340px" */
}

m4 and HTML

You might be considering m4 for HTML, too. Why not? You can include common HTML sidebars and header information, substitute various strings, build parts of the site conditionally, and so on.

My only caveats are:

  1. Don't get carried away and make some kind of crazy m4 megastructure that no one else can ever maintain.
  2. m4 doesn't do multibyte character encodings, such as UTF-8 (past the first 127 characters, anyway!)

Consider command-line PHP for prebuilding static HTML, instead.

More Info

Probably the most notable things I haven't talked about here are macro expansion with arguments, and string manipulation. But that stuff's pretty straightforward if you have a handle on the stuff I've already mentioned, above.

To that end, here are some more sites with m4 information:

Blog  ⚡  Email beej@beej.us  ⚡  Home page