DanielXMoore / Civet

A TypeScript superset that favors more types and less typing
https://civet.dev
MIT License
1.36k stars 29 forks source link

Indentation based jsx syntax #20

Closed lorefnon closed 1 year ago

lorefnon commented 1 year ago

Hello, thanks a lot for creating Civet - This looks great. I spent some time playing around with it yesterday and loved the DX.

Since this language is quite early, I was wondering if we could eliminate the need for closing xml tags in jsx, and adopt an indentation based syntax similar to imba. I think this will make it more coherent with the rest of the language as well.

Imba is tied to its own dom/rendering impl. where as I am trying to use civet with solidjs which has its own jsx preprocessor.

edemaine commented 1 year ago

I agree 100% (also a Solid fan here!). We had some discussions about this in the Discord, and I was planning to take a stab at implementing it (as Daniel doesn't use JSX himself).

I think the right way to do this is to support both syntaxes, via a parser flag in the first line (the existing "civet ..." directive). That way we can still offer CoffeeScript compatibility. For example:

@STRd6 I haven't looked at how JSX is handled, but I assume there's significant support in the lexer. Will it be feasible to support both modes?

STRd6 commented 1 year ago

It should be possible to support both, and it may even be possible to support optional closing tags by default. With the caching implemented in the parser there is much less cost to distant lookaheads. Since optional closing tags is a superset of explicit closing tags then it could make a very compatible default that people could opt into by usage. Perhaps the only necessary directive would be "civet closeJSX" to opt into mandatory explicit JSX closing.

@edemaine take a look at the NestedBlockStatements rule for an example of how the parser currently handles nesting. You can add a production to JSXElement that uses nesting and inserts a synthetic closing tag (similar to how it checks for mismatched tags). Let me know if you have any other questions, thanks!

lorefnon commented 1 year ago

So the indentation based syntax in imba has a few more nuances other than optional closing tag. I am not suggesting we adopt them all, but one thing of interest is that because strings need to be explicitly quoted, we can avoid the need to wrap expressions in braces as in JSX.

This enables us to write code like:

<div.tiles> for tile,i in game.tiles
    <Tile data=game nr=i @click=game.place(i)> tile

which is more succinct compared to:

<div.tiles>{for tile,i in game.tiles
    <Tile data={game} nr={i} @click={game.place(i)}> {tile}}
STRd6 commented 1 year ago

@lorefnon I agree about removing (or optional) braces for attribute values but for element content non-braced characters would collide with text children.

lorefnon commented 1 year ago

Yes, unless text children in indented jsx mode have to be always quoted eg.

<div>"Foo"

Text content within jsx is increasingly uncommon in most apps that support i18n

STRd6 commented 1 year ago

@lorefnon I'm definitely open to exploring it. I don't use JSX too much myself but I do see the value in Civet providing a much more concise, mostly compatible JSX that improves the common cases and supports best practices (i18n). Worst case we can add a directive like "civet jsxCompat" for people migrating from existing JSX codebases.

<div> "hi" if i > 2
---
<div>{i > 2?"hi": void 0}</div>

So far seems like it would be 👍

edemaine commented 1 year ago

It'd be helpful to pull apart the various features of imba to see what makes sense here.

Indentation instead of explicitly closing tags

I think we all agree this is good, especially if we can also still support closing tags.

.className

I'm a big fan of Pug so I like the idea of div.foo as shorthand for <div class="foo">. However, I don't think it's a good idea in JSX context, because <div.foo> already has a meaning. For example, Motion One for Solid uses <Motion.div> to dereference the div property of the Motion object.

Maybe <div .foo> could work? But this looks less like a CSS selector...

Children are JS expressions, not text

I'm a little more hesitant on this one, as it makes the notation not feel like HTML anymore. JSX has such clear semantics that braces mean JS content, whether they're in attributes or children. JSX also has the nice feature that you can copy/paste HTML and you're generally good (especially with Solid).

On the other hand, I can see preferring this. Maybe it makes sense to offer both options via a civet flag?

Also, I don't think I understand how imba actually treats children. They're not just JS expressions. There's also the css thing (it's clearly not a function), and multiple tag children can be "returned" without having to wrap in <>...</> as you do in React or Solid. I'm not sure how this would be achieved in Civet. An option for implicitly wrapping multiple consecutive tags into a fragment would be really convenient (can't tell you how many times I've forgotten to do this in CoffeeScript).

lorefnon commented 1 year ago

Yes, I wasn't suggesting adopting all parts of imba template syntax. It is very feature rich, has evolved over quite a long time, and some of it (eg. scoped css, slots etc.) are closely tied to their implementation and will not translate well to arbitrary jsx providers.

I was primarily highlighting the expression support because needing to wrap code in { } looks a bit weird in an indented language which otherwise doesn't use braces as block delimiter.

HerringtonDarkholme commented 1 year ago

Do we really need JSX in Civet? Civet is expressive enough for markup I think. Previously we also have https://github.com/mauricemach/coffeekup

edemaine commented 1 year ago

Do we really need JSX in Civet?

Yes. There are many frameworks that work much better with JSX. Big examples are React (the most popular front-end framework), Preact, and Solid. JSX is also used in many other smaller projects, for example my own SVG Tiler (JSX is simply the best way to express programmatic SVG).

Omitting JSX would be limiting Civet to a much smaller audience. JSX is also a key feature of CoffeeScript, so it makes sense for compatibility as well.

lorefnon commented 1 year ago

Just to add to the above, unlike React where jsx always transforms to a plain function call, frameworks like solid, qwik have different and more involved compile time support for jsx - if we don't support jsx, to be able to consume these libraries in a civet we will have to do one of the following:

  1. Use a less performant or less type-safe representation
  2. Expose a custom dsl that emulates these transformations and keeps track of updates

But if we support jsx, civet can just rely on whatever jsx specific tooling these libraries maintain

edemaine commented 1 year ago

Since optional closing tags is a superset of explicit closing tags then it could make a very compatible default that people could opt into by usage.

I don't think this is right, because entering JSX normally (in CoffeeScript) means "ignore all indentation until we re-enter code mode via {". In testing #25, I finally found an example where this actually matters:

return
  <div>
foo bar
  </div>

In CoffeeScript, this is a <div> element with some text. If we allow <div> to become <div/> automatically when it has nothing intended inside it, then we end up turning foo bar into the function call foo(bar) instead of treating it like text.

So I think a toggle may be useful, for backward compatibility. But hopefully this kind of use is pretty rare (text is normally intended within the tag) so we can have indentation JSX on by default.

edemaine commented 1 year ago

Actually, the </div> in that example forces it to parse correctly... I was testing wrong. So maybe there isn't actually a bad case?

edemaine commented 1 year ago

Indentation-based JSX is now live in 0.4.28! (Currently, co-existing with explicitly closed tags.)

Examples (from solidjs.com):

https://playground.solidjs.com/

function Counter()
  [count, setCount] := createSignal 1
  increment := -> setCount count() + 1
  <button type="button" onClick={increment}>
    {count()}

https://www.solidjs.com/tutorial/introduction_jsx

return
  <>
    <div>
      Hello {name}!
    {svg}

https://www.solidjs.com/tutorial/flow_for

<For each={cats()}>{(cat, i) =>
  <li>
    <a target="_blank" href={`https://www.youtube.com/watch?v=${cat.id}`}>
      {i() + 1}: {cat.name}
}

With the other imba suggestions I could see supporting (with a compiler flag):

<For each={cats()}>
  (cat, i) =>
    <li>
      <a target="_blank" href={`https://www.youtube.com/watch?v=${cat.id}`}>
        `${i() + 1}: ${cat.name}`

Maybe I'll try that next.

STRd6 commented 1 year ago

It might also be worth exploring optional braces for attributes:

<For each=cats()>
  (cat, i) =>
    <li>
      <a target="_blank" href=`https://www.youtube.com/watch?v=${cat.id}`>
        `${i() + 1}: ${cat.name}`
lorefnon commented 1 year ago

Great! Thanks a lot for working on this.

edemaine commented 1 year ago

For those tracking this GitHub issue:

It might also be worth exploring optional braces for attributes:

Now supported in Civet v0.5.0! Any attribute values with no "unwrapped" whitespace should now work without braces. For example: cats()?.names[i] or (=> console.log 'hello world').

Also added is computed property names like [propName()]=propValue(). (This compiles to {...{[propName()]: propValue()}}.)

And previously added is the shorthand {foo} for foo={foo}. This works more generally with any braced object literal, including getters and setters.

STRd6 commented 1 year ago

Resolved in: https://github.com/DanielXMoore/Civet/pull/25

lorefnon commented 1 year ago

@STRd6 @edemaine Are we still open to a language level flag to skip braces for embedded code (basically https://github.com/DanielXMoore/Civet/issues/20#issuecomment-1341487100) ?

STRd6 commented 1 year ago

@lorefnon I'm open to it, at least as an optional flag. It might make sense to open a new issue about only that.

edemaine commented 1 year ago

I also plan to work on it, but I'm doing lower-hanging JSX fruit first. 🙂