Help with Macro for DSL

Hi.

I’m relatively new to Elixir and meta programming in particular.
For a small project of mine I thought hat a HTML DSL would be a good exercise.
But I’m stuck.

My DSL should use the Eml HTML library to generate the actual HTML.
I have the following simple macro:

  defmacro tag(name, attrs, do: inner) do
    IO.puts "tag(name): #{inspect(name)}"
    IO.puts "tag(attrs): #{inspect(attrs)}"
    IO.puts "tag(inner): #{inspect(inner)}"
    ast = quote do
      %Eml.Element{tag: unquote(name), attrs: Enum.into(unquote(attrs), %{}), content: unquote(inner)}
    end
    IO.puts "ast: #{Macro.to_string(ast)}"
    ast
  end

On top of this I have generated macros for the html tags like:

    defmacro unquote(tag)(attrs, do: inner) do
      tag = unquote(tag)
      quote do: tag(unquote(tag), unquote(attrs), do: unquote(inner))
    end

This is pretty much taken from Chris McCord’s book “Metaprogramming Elixir”.

So far so good.
The problem here is generating the Eml.Element type from a nested block when it contains 2 or more macros.

For a simple code like this:

html do
  span id: "1", do: "MySpan1"
  span id: "2", do: "MySpan2"
end

the dump of ‘inner’ from within the macro gives me this:

tag(name): :html
tag(attrs): [foo: "bar"]
tag(inner): {:__block__, [line: 60], [{:span, [line: 58], [[id: "1", do: "MySpan"]]}, {:span, [line: 59], [[id: "2", do: "MySpan2"]]}]}
ast: %Eml.Element{tag: :html, attrs: Enum.into([foo: "bar"], %{}), content: (
  span(id: "1", do: "MySpan")
  span(id: "2", do: "MySpan2")
)}
tag(name): :span
tag(attrs): [id: "1"]
tag(inner): "MySpan"
ast: %E{tag: :span, attrs: Enum.into([id: "1"], %{}), content: "MySpan"}
tag(name): :span
tag(attrs): [id: "2"]
tag(inner): "MySpan2"
ast: %E{tag: :span, attrs: Enum.into([id: "2"], %{}), content: "MySpan2"}
"#html<%{foo: \"bar\"} #span<%{id: \"2\"} \"MySpan2\">>"

The problem here is the:

%Eml.Element{tag: :html, attrs: Enum.into([foo: "bar"], %{}), content: (
  span(id: "1", do: "MySpan")
  span(id: "2", do: "MySpan2")
)}

It seems that only the last of the nested spans is taken for the content, the first one is discarded.

Is there a way this can be fixed?

Regards,
Manfred

Right now, you’re getting Elixir semantics when there are two elements in a block: the last expression evaluated in the block is the return value.

To get the “append every expression’s result to a buffer” behavior you’re looking for, you’ll need to transform the supplied AST into code that does exactly that.

For instance, the relevant code in Xain starts with Xain.join_lines. join_lines specifically targets AST nodes tagged with :__block__ and transforms them into code that prepends to xain_buffer:

Thanks for your reply.
Yeah, I guess some parsing of the AST is required to do this.

Manfred