mnussbaumer / cssex

An Elixir based and opinionated way to write CSS
MIT License
20 stars 0 forks source link

Pretty printing CSS? #58

Closed John-Goff closed 1 month ago

John-Goff commented 1 month ago

Hello! Thanks for the great library, however I think I'm probably not using it for its intended purpose :)

I'm trying to create the most drop-dead simple CMS you can imagine, with support only for what I need and nothing else. Unfortunately, one of the requirements is being able to add custom CSS without a redeploy. That's fine and working, however currently there's no indications of whether or not your CSS is valid, as well as mangling the formatting every time I save and load it to the database. I've found CSSEx to do quite well at the parsing so far, however it always spits out a minified CSS binary. For now I've been manually adding newlines and spaces based on a few simple String.replace/3 calls, but I was wondering if it'd be in scope to add a config option or argument to the function to output as either minified or pretty printed?

Thanks in advance, and thanks again for the library!

mnussbaumer commented 1 month ago

Hi @John-Goff. So the pretty printing you mean for the final CSS after it has been assembled right? That shouldn't be difficult, from the top of my head I can't see any issue, since there's only one level for each selector in the final CSS. Not sure though when I would have the time to check it out - specially because it would probably make more sense to implement as a pluggable system at the end with two backends available by default (compact & pretty printed).

One other possible solution, if you don't want to try a PR, would be to add some js lib that does pretty printing, I imagine there should be some that you can extract from somewhere and include as a static js lib in the project for that part in the meanwhile. Nonetheless when I have time I will look into it.

Regarding validation of CSS, currently as you noticed it doesn't really validate the final CSS output - I personally rely on whatever build chain I'm using in the project to validate the CSS, in my case esbuild at build/deployment time - it would definitively be interesting to create a proper validation system inbuilt in CSSEx for that - since right now it requires an additional tool if to be run dynamically on the running server - but I doubt it would be any time soon.

The easiest way for you to solve that right now would be with any external validator that can be ran as a command line utility and accepts input from stdin (or even from a file in last case) and call it with the result CSS.

Solving that at the CSSEx level requires having a CSS engine that validates according to the CSS 3.0 spec - which I'm not sure is possible to derive programatically from a source so if not it means writing down all combinations of properties & possible values. It would be great if the browser just exposed a validate_stylesheet wouldn't it...

John-Goff commented 1 month ago

Let me give you an example so it's a bit clearer hopefully :smile:

Right now, I have an Ecto schema called Page, which has a field :contents and another called :css. The requirement is simply to be able to write HTML in :contents, CSS in :css, and have them stored minified in a TEXT column in sqlite and pretty-printed for editing by admins. I don't necessarily care about "correctness" of HTML or CSS, e.g. making sure the tags you use or the CSS properties you use are valid, more about making sure you didn't e.g. forget a closing tag or a semicolon in your CSS. Right now, the HTML requirement is solved by Floki and the CSS is solved by CSSEx.

I have a format_css! function and a format_html! function to wrap these behaviours, the HTML one is quite simple thanks to Floki:

  def format_html!(html, mode \\ :pretty) do
    pretty_print? = mode == :pretty
    [{_elem, _attrs, parsed_html}] = html |> Floki.parse_fragment!() |> Floki.find("body")
    Floki.raw_html(parsed_html, encode: false, pretty: pretty_print?)
  end

CSSEx.Parser.parse/1 handles the validations that I need to do just fine:

CSSEx.Parser.parse("""
main {
  dummy: property
}
""")
# => {:ok, %CSSEx.Parser{...}, "main{dummy:property}\n"}

CSSEx.Parser.parse("""
main {
  dummy: property
""")
# => {:error, %CSSEx.Parser{...}}

CSSEx.Parser.parse("""
main {              
  dummy: property  
  another: issue    
}              
""")            
# => {:error, %CSSEx.Parser{...}}

CSSEx.Parser.parse("""
main {
  dummy: property;
  all: good;
}
""")
# => {:ok, %CSSEx.Parser{...}, "main{all:good;dummy:property}\n"}

So then the format_css! function is also simple, but slightly less so since I do need to pretty-print it myself:

  def format_css!(css, mode \\ :pretty)

  def format_css!(css, :minified) do
    {:ok, _parsed, minified_css} = CSSEx.Parser.parse(css)
    minified_css
  end

  def format_css!(css, :pretty) do
    css
    |> format_css!(:minified)
    |> String.replace(~r/(\S);(\S)/, "\\1;\n  \\2")
    |> String.replace(~r/(\S){(\S)/, "\\1 {\n  \\2")
    |> String.replace(~r/(\S)}(\S)/, "\\1;\n}\n\n\\2")
    |> String.replace(~r/:(.*;)/, ": \\1")
  end

But since CSSEx.Parser.parse returns the binary as the third element in the tuple, there's no eqivalent to Floki's raw_html/2 function which takes a parsed representation of HTML and turns it into a binary. I would perhaps prefer to see that approach taken with CSSEx, but I'm not sure how large of a change that would be so I'm reluctant to propose it. Given the API of that function, perhaps a field on the struct? So you could do something like

{:ok, _parser, minified} = CSSEx.Parser.parse(%CSSEx.Parser{output_css: :minified}, initial_css)
{:ok, _parser, pretty} = CSSEx.Parser.parse(%CSSEx.Parser{output_css: :pretty}, initial_css)

My function relies on the CSS being minified first which is why I call format_css!(:minified) and then do the string replace on the resulting CSS, and I haven't tested it on anything but the examples I had to hand, but so far this is working fine for me. I'm just wondering if it's interesting to anyone else to have this behaviour be a bit more generalized, and if so, if this library is the right place to do it 😄

mnussbaumer commented 1 month ago

Hi again @John-Goff ,

thanks for the follow up. Yes, the pretty printing option shouldn't be really that difficult to add, since everything related to outputting the final css is done in https://github.com/mnussbaumer/cssex/blob/master/lib/helpers/output.ex . So it's a matter of me working on adding an option that gets passed to the ouput and doing it properly there.

Ok, the other point about validation was that I didn't understand if you were also asking for proper CSS validation on top of simple syntax validation.

mnussbaumer commented 1 month ago

@John-Goff can you try this: https://github.com/mnussbaumer/cssex/pull/59

You need to call it right now as:

CSSEx.Parser.parse(nil, """
main {
  dummy: property;
  all: good;
}
""", nil, pretty_print?: true)

I'll probably add a simpler form of just calling parse("""content"""", pretty_print: true) but want to double check this covers what you would need. I have checked with a complex project and seems to work fine - there can be a couple situations where the output will have two empty newlines instead of one in some situations.

I still need to add tests and change documentation so it will take a few more days