Ex CSS (sass simile in pure elixir)

Hi,

As part of maybe starting a project for a CMS + static site generator I decided to create a library that allows similar CSS pre-processing as Sass/SCSS. It’s not yet finished but it already works for most part (it’s missing the ability to create ad-hoce functions) and similar in many aspects to Sass, with the exception that it allows using EEx code blocks and arbitrary Elixir terms as assigns (on top of normal string based “variables”).

It also has a basic watcher that pre-processes your CSSEx files automatically and it’s made in a way to accommodate the needs I imagined for the possible CMS+SSG (programatically pass variables & assigns while generating css, etc).

You can check it at GitHub repo
Along with some basic examples.

As it is still lacking what is described in the roadmap I haven’t put it on hex yet, nor done proper benchmarks with more than small toy sized sass/cssex files, but in those cases I did it was fairly faster.
If you give it a try or have suggestions, bug reports, or want to contribute open an issue on the repo!

If you would like to see it in Erlang help me solve the issue of the EEx blocks and how to pass assigns into them - having that solved would allow to write it almost as is in Erlang (I also have no idea if anyone writing erlang needs a css preprocessor to be honest…)

(forgot to put on the readme but it also supports of course the “&” nested notation of sass)

13 Likes

Great idea. If you are introducing elixir grammar into CSS, I’d like to see something that is completely elixir; ie a full set of elixir macros and functions to write CSS in 100% elixir grammar, sort of like what ecto does for SQL.

The current landscape of various CSS techs is sad. The only thing that has momentum is tailwind, and it is incredibly slow to compile. Also the new version has the brilliant idea of changing whitespace-no-wrap to whitespace-nowrap for no apparent reason.

1 Like

Thanks :slight_smile:

What syntax/api are you thinking? And how would that work? IT would generate the files on compile?

I personally like how CSS works - just miss not having some basic constructs to generate repetitions with slight changes or not being able to do nested selectors (with sass’ &) that results in repeating a lot of declarations. I haven’t used tailwind that much, I can see it working but I think it adds a lot of class declarations to the html being generated (although some people swear by it and its maintainability), I’m just not sure it’s the best approach to CSS.

Great! I don’t want to put you to any bother, but could you please in the next step replace webpack and npm?

4 Likes

I haven’t think it through, but I think a elixir-ish DSL that compiles down to plain CSS is doable. I prefer it to be true elixir instead of some eex template so I can use the full power of elixir. If it can express 99% of the CSS constructs, the remaining 1% can be done in a escape hatch similar to an ecto fragment.

Like Temple - HTML DSL for Elixir and Phoenix but for CSS?

1 Like

I actually wrote my own html DSL: html_writer

but for CSS if there are more people interested we could do something more advanced, think something like Ecto.

2 Likes

Building CSS is really straightforward, parsing isn’t complex but it’s more complex than building it.

I’m just not sure about what would be the syntax and the benefits of having it written in Elixir (while in HTML I can clearly see why it can help).

Taking this example from Temple

    header do
      section class: "container" do
        nav role: "navigation" do
          ul do
            li do: a("Get Started", href: "https://hexdocs.pm/phoenix/overview.html")
          end
        end

        a href: "http://phoenixframework.org/", class: "phx-logo" do
          img src: Routes.static_path(@conn, "/images/phoenix.png"),
              alt: "Phoenix Framework Logo"
        end
      end
    end

CSS is already sort of structured and using the & for instance to style the header & section with a class “container”

@!bg_color white; // these need not be set in a file,  while usually you would
// write it in files, the parser itself can take these as values when started in the
// form of a map where each key is variable name, like %{bg_color: "white", text_color: ...}
@!text_color rgb(75,75,75);

header {
  background-colour: <$bg_color$>;
  color: <$text_color$>;
  width: 100%;
  section.container {
     width: 100%;
     display: flex;
     flex-flow: row no wrap;
     justify-content: space-between;
     //....
  }
}

Are you thinking of something like:

Stylesheet.new()
|> rule("header section.container", width: "100%",  other: "rules", ...)
|> media(type: :print, min: "565px", width: "95%")

?

Just trying to understand what it would look like.

Someone’s working on an erlang to js implementation!

A different syntax only makes sense if it enables new and productive way to refactor the code. Sorry I don’t use CSS heavy enough to make any good suggestion.

1 Like

Parsing CSS is really annoying because test suits assume that you want have the same experience as you do with browsers which is that you can throw a lot of crap at it and it still works :smiley:

But if you want to have a fairly standard compliant parser then I have one. It’s based of the CSS spec, it’s I believe only failing because I have to match test suits:

It doesn’t not yet support the nesting tricks that postcss and sass use.

My intent was and is still to have something akin on styled components available for Elixir. It’s just sometimes hard to find time :smiley:

3 Likes

Nice, thanks for sharing. I had thought about using nimble_parsec but I would need to learn it properly so just started doing by hand with what I know. Your parsec version does look more organised and less code :slight_smile:

Should I change the title? The project itself is cssex but for putting it here on the forum I chose to write it as ExCSS and just saw that you named yours that.

CSSEx is almost fully featured, I’m writing now the custom functions part and some helpers for using them. It’s missing comments which is basic.

At this point specific validations can be added to every part if it was needed or wanted (I might work on adding further validations incrementally, like asserting that if you’re in a [prop=“val”], you’re using valid =, =~, ^=, equality variant, etc). Outside of proper css spec, adding further things like tree-shaking would be almost “trivial” to if there was already something to extract a hierarchy of css from an html page.

I did read parts of the specification for css2 but the difference when parsing CSS from something as cssex or sass , is that the rules are stricter on CSS and in this you depend on further things inside the blocks to output and validate - once you introduce the concatenation & and you’re inside a block everything goes because almost any character can concatenate to the top level chain to produce a valid selector.
So if you want to verify it you need to evaluate it after the fact (and it also depends if any more concatenation elements are inside this selector as well as they would tag into its trail, because it can be arbitrarily nested).

Then outputting that into css, the same with simple nesting like having media queries inside selectors, you can’t generate the css rule in place, as you have to put the previous selector chain inside the media block and the contents of the media block inside this selector. Then if you do @includes, that brings another file in place that can also be inside arbitrary blocks… Then it needs variables and interpolation and these have to be tossed around too, plus keeping the lines & columns information and parsing step.

At that point you need to carry a bag of things as you parse and store them in other format than plain binary, but it would be interesting to see a fuller implementation just with parsec to see how it ends up looking.

I looked at the tests but I didn’t understand how those are supposed to be run. I’m just writing small chunks of relevant cssex and assert on the output, and some errors/warnings. Once I have the functions finished I’ll translate a fairly complex sass stylesheet (a small css framework I usually use) into cssex to do a final bigger test of correctness and others of speed (as it is I’m seeing significant different between dart-sass 0.15s to cssex 0.01s but these weren’t scientific). Maybe I could use those tests to assert on the generated CSS.

The reason why I did a full parser is mostly because if you introduce some of the nesting rules it’s hard to know how to apply them without knowing what structure you have.

I also wanted to try building a proper css parser by the spec and because GitHub - acammack-r7/erlang-css: A library for parsing and serializing CSS failed on some css code I tested it with.

I don’t think anyone is using my library so I don’t mind any name conflict since I haven’t really mentioned it anywhere I believe and it’s kind of a niche until it is really solid and has a lot of examples.

Nice I didn’t know there was a spec for nesting will have to look at that:
https://drafts.csswg.org/css-nesting-1/

1 Like

Neither did I. And I just noticed a pretty significant oversight on mine that thankfully is easily correctable.

Anyway, why introduce a character & to indicate that it’s a plain nest? I mean what else could it be if it’s not a rule (<text><colon><text><semicolon>)?

And I think would be great if they went for nested @media expressions (but not media inside media as that doesn’t make sense, well, it could make sense but would get overall more confusing and easy to create impossible to meet rules)

NOTICE : In a future version of this project, nesting at-rules like @media may be deprecated, as they are not part of the nesting proposal. In a comment, a CSSWG member expressed interest in handling nested @media while handling selector nesting. So deprecating nested at-rules has been temporarily delayed.

But media inside plain selector blocks in some cases it allows you to see the whole element styling in one single place.

I’ll give a look at the draft because if they implement their & then I need to find another valid char for the concatenation x)

It’s pretty useful specially if it’s done to the spec I think.

So I did a bit more reading on the draft.

It’s interesting and I think it covers what nesting needs to be but I’m not sure about using only &. I’m going to use sass/scss as the comparison (but I haven’t went to confirm their spec is exactly as this though so I might be slightly off here):

I always use and thought of & as being specifically concatenation.
This means that

button {
      &.btn-red { background-color: red; }
      &.btn-blue { background-color: blue; }

      span { margin-left: 10px; }
}

Produces:

button.btn-red { ... }
button.btn-blue { ... }
button span { ... }

So & “glues” the selector described afterwards to the parent(s) selector chain(s) up to its opening. This would be used just to describe things where selectors change the styling of the parent.

A non & selector does simple CSS selection when nested. This selector when “inside” the parent(s) selector chain(s). And is used just to facilitate the styling of child elements of the selector up to it.

I think this is less ambiguous. They also suggest adding an additional @nested at-rule to combat the readability issues,

While direct nesting looks nice, it is somewhat fragile. Some valid nesting selectors, like .foo &, are disallowed, and editing the selector in certain ways can make the rule invalid unexpectedly. As well, some people find the nesting challenging to distinguish visually from the surrounding declarations.

because in their proposal you can use &.concatenate and & .not-concatenated. and they’ll have different css meanings while being hard to spot the space between the & and the selector chain. (and my parser right now does the same thing although it’s technically wrong for how I see it used, it’s an easy fix nonetheless and I think I will implement it in this stricter sense too)

The other point about invalid nesting, single trailing & could be taken care of by being stricter where a nesting can be placed - e.g. only at the beginning of a non top level selector, and no spaces between it and the selector - translated to warning about invalid rules automatically when that occurs - which means it’s always a glue me up operation. This would also make IDE’s, parsers and additional tooling life’s easier.

I see they have a GitHub repo to provide feedback and discussion I might take this there to see their thoughts on it.

Yeap, closed with “wont-fix”. Which is funny given it’s a draft for a spec extension.
I sometimes don’t understand how this works. It’s not like there’s no prior art. Sass is from 2006, less is from 2009, they’re the most widely used pre-processors and introduced this idea.

Along comes the opportunity a new spec and instead of using the ideas that prompted the spec, they change them to the point they’re incompatible with what people already showed they preferred, while still introducing complexity (or more) and worse readability.

2 Likes

Totally looking forward to your library so that I can get rid of some more webpack and npm craziness. Just yesterday I tried to install some project that depended on yarn and even after an npm install it gave an amusingly opaque error which I failed to resolved.

I got confused reading through the issue likely since I’m still a novice at css. The example on the SASS site for parent selector, they actually used a space on the dir=rtl line. Maybe it’s meant more as search and replace rather than concatenation?

Thanks! It’s almost feature complete. Another of the reasons I decided to write it was also because I was trying to setup an old nuxt project that used node-sass and couldn’t get libsass to work on ubuntu (maybe for a good reason, but was still pretty annoying trying first to fix that - ultimately went with the new js based sass). If there’s any utilities you would like to see available tell me and I can look into adding them.

I think the real reason why they don’t consider the option (besides having everything lined up and docs written, etc) is because it changes how the parsing has to be done inside a block. Right now in css2 if you’re inside a block and you reach a colon : per the spec you can be sure that you’re in an attribute.

If you are now to add the ability to nest selectors and want to keep that unambiguity, due to the fact that pseudo-selectors & pseudo-classes both use colons, e.g. :hover, you need to somehow signal the line you’re starting inside the block, because otherwise when reaching that colon it would be ambiguous if it was an attribute or a selector. So you “can’t” have a naked nested selector without doing some changes on a parser that was relying on the colon.

So they introduced a similar approach to scss with the ampersand token. But in scss you have & and can have a naked selector too, and it does nesting as well, but has a different meaning from the &.

div {
      &:hover { color: red; }
      button:hover { color: green; }
}
// this results in
div:hover { color: red; }
div button:hover { color: green; }

So the “parent” selector basically appends the selector to the previous chain, while a naked declaration just adds another selector to the chain.

In css3 it will be:

div {
      &:hover { color: red; }
      & button:hover { color: green; }
}

And the difference is that space between & and the selector - and this is were I dislike the proposal because it makes it harder to distinguish between two different outcomes in the css rule. It also means that in a sequence of nested comma-separated selectors, all of them have to be prefixed by &, while otherwise you could write &.concatened, .non-concatenated { ... }. And then there’s another @ rule being introduced (@nest) that is less strict than the &.

Anyway, first world problems for sure but I think overall the scss syntax is way cleaner, and given that the parsers are fairly outnumbered by the people writing css (maybe 50, between browser engines and css pre-post processors?), that a new spec that required a change on the parsers side to instead decide at the end of the declaration if it’s an attribute or a selector wouldn’t be the end of world.

I think I get it now. Looks like the following line of the spec:

To be nest-prefixed, a nesting selector must be the first simple selector in the first compound selector of the selector. If the selector is a list of selectors, every complex selector in the list must be nest-prefixed for the selector as a whole to nest-prefixed.

From their example 4:

.foo {
  color: red;
  .bar { color: blue; }
}
/* Invalid because there’s no nesting selector */

The SASS syntax for nesting is pretty useful for nesting, you hardly have to think about it.

nav {
  ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }
}

I’m pretty confused myself why someone would try to modify syntax that seems to be working from 15 years ago, but there must be some esoteric reason.

Thanks for the explanation.

Yah, I’m considering removing the bundler and just concatenating minified files also. One of my pet peeves is coming back to side projects, cloning them, and finding out they no longer can be built. From my limited experience, it appears that anything involving the NPM ecosystem has maybe a longevity of a year before maintenance is needed for code rot.