egze

egze

Help to parse a template with NimbleParsec

I’m trying to create a template where you would enter this:

ID: {{ my_func($.project.id, "arg") }}, NAME:  {{ $.project.id }}

or

{{ my_func($.project.id, "arg") }}, NAME:  {{ $.project.id }}

I want to write a parser with NimbleParsec that gives me something like this:

{:ok, ["ID: ", [expr: [function: [my_func: ["$.project.id", "arg"]]]], ", NAME: ", [expr: "$.project.id"]]}

Basically, I need to identify opening and closing tags {{ and }}, and then inside them detect if there is a function call. If yes, parse the function call too for the name and arguments.
Function calls can also be nested my_other_func(my_func($.project.id, "arg")) and also sometimes there is no function call, but only a value.

The output doesn’t need to be exactly like mine, I just need to be able to tell things apart.

This is my first ever serious usage on NimbleParsec, and I’m not really sure how to approach it.

I get stuck on things like:

  • how to collect a string, until I detect a starting tag. But also how to make it optional.
  • how do I capture things between open and close tags, and then tag it as :expr.
  • how do I repeat it, until I reach the end of the string.

And I haven’t even got to parsing nested functions.

Can anyone be so kind and write down how do you start to tackle the problem?

Marked As Solved

fuelen

fuelen

Hello @edze

That’s a good exercise for my evening :slight_smile:
Here is my parser:

defmodule TemplateEngine do
  defmodule Parser do
    import NimbleParsec

    optional_whitespaces = ascii_string(~c[ \t\n\r], min: 0)

    text =
      times(
        lookahead_not(string("{{"))
        |> utf8_char([]),
        min: 1
      )
      |> reduce({List, :to_string, []})
      |> unwrap_and_tag(:text)

    string_literal =
      ascii_char([?"])
      |> ignore()
      |> repeat(
        lookahead_not(ascii_char([?"]))
        |> choice([
          ~S(\") |> string() |> replace(?"),
          utf8_char([])
        ])
      )
      |> ignore(ascii_char([?"]))
      |> reduce({List, :to_string, []})
      |> unwrap_and_tag(:string_literal)

    variable =
      string("$")
      |> times(string(".") |> utf8_string([?a..?z, ?A..?Z, ?_, ?0..?9], min: 1), min: 1)
      |> reduce({Enum, :join, []})
      |> unwrap_and_tag(:variable)

    function_call =
      utf8_string([?a..?z, ?A..?Z, ?_, ?0..?9], min: 1)
      |> unwrap_and_tag(:name)
      |> ignore(string("("))
      |> tag(
        repeat(
          parsec(:expression)
          |> ignore(optional(string(",") |> ignore(optional_whitespaces)))
        ),
        :args
      )
      |> ignore(string(")"))
      |> tag(:function_call)

    defparsecp(
      :expression,
      choice([
        variable,
        function_call,
        string_literal
      ])
    )

    interpolation =
      ignore(
        string("{{")
        |> concat(optional_whitespaces)
      )
      |> parsec(:expression)
      |> ignore(
        optional_whitespaces
        |> string("}}")
      )
      |> unwrap_and_tag(:interpolation)

    defparsec(:parse, repeat(choice([interpolation, text])) |> eos())
  end

  def test do
    template = ~s|ID: {{ my_func($.project.id, "arg") }}, NAME:  {{ $.project.id }} {{ my_other_func(my_func($.project.id, "arg"))}}|
    __MODULE__.Parser.parse(template)
  end
end

and result:

iex> TemplateEngine.test()
{:ok,
 [
   text: "ID: ",
   interpolation: {:function_call,
    [name: "my_func", args: [variable: "$.project.id", string_literal: "arg"]]},
   text: ", NAME:  ",
   interpolation: {:variable, "$.project.id"},
   text: " ",
   interpolation: {:function_call,
    [
      name: "my_other_func",
      args: [
        function_call: [
          name: "my_func",
          args: [variable: "$.project.id", string_literal: "arg"]
        ]
      ]
    ]}
 ], "", %{}, {1, 0}, 114}

12
Post #2

Also Liked

fuelen

fuelen

Yes, I’ve started from left to right. While working on parser, I find it easier not to limit anything on the right side. Like, put eos() only when parser is ready. When working on function(...) put expectation for closing ) only when parsing arguments is ready.
Also, I have this helper for inspecting errors:

def inspect_error(result, input) do
  print_lines = fn
    [] -> :noop
    lines -> IO.puts([IO.ANSI.yellow(), Enum.intersperse(lines, "\n")])
  end

  case result do
    {:error, reason, _rest, _context, {line, offset}, byte_offset} ->
      {lines_with_error, lines_after_error} = input |> String.split("\n") |> Enum.split(line)

      {:ok, terminal_width} = :io.columns()

      cursor_position = byte_offset - offset
      {lines_before_error, [line_to_split]} = lines_with_error |> Enum.split(-1)
      chunks = line_to_split |> String.codepoints() |> Enum.chunk_every(terminal_width)
      number_of_chunk_with_error = div(cursor_position, terminal_width) + 1
      cursor_position_in_chunk = cursor_position |> rem(terminal_width)
      {chunks_with_error, chunks_without_error} = Enum.split(chunks, number_of_chunk_with_error)

      print_lines.(lines_before_error)
      print_lines.(chunks_with_error)
      IO.puts([IO.ANSI.red(), List.duplicate(" ", cursor_position_in_chunk), "^", reason])
      print_lines.(chunks_without_error)
      print_lines.(lines_after_error)

    _ ->
      :no_error
  end
end

usage:

template
|> __MODULE__.Parser.parse()
|> tap(&inspect_error(&1, template))

error messages are not so good as they could be, and writing a parser with good error messages is another art. But this helper allows to visually find the place where something goes wrong. I mean, try to remove " from the template near the "arg" and inspect the error.

fuelen

fuelen

I’m not sure if nimble parsec is the best tool for autocomplete on possibly invalid templates. I think you need a lexer (leex) and then using list of tokens and position of a cursor try to analyze what you can suggest for autocomplete

Where Next?

Popular in Questions Top

_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New

We're in Beta

About us Mission Statement