Added basic parser for hydrating template strings

This commit is contained in:
Kieran Eglin 2024-01-20 18:54:17 -08:00
parent 05371cdac6
commit 7dcc893297
No known key found for this signature in database
GPG key ID: 193984967FCF432D
5 changed files with 122 additions and 1 deletions

View file

@ -0,0 +1,52 @@
defmodule Pinchflat.RenderedString.Base do
@moduledoc """
A base module for parsing rendered strings, designed as a macro to be used
in other modules.
NOTE: if the needs here get any more complicated, look into using a Liquid
template parser. No need to reinvent the wheel any more than I already have.
NOTE: this is effectively tested by the `Pinchflat.RenderedString.Parser`'s tests
"""
defmacro __using__(_opts) do
quote location: :keep do
import NimbleParsec
opening_tag = string("{{")
closing_tag = string("}}")
optional_whitespaces = ascii_string(~c[ \t\n\r], min: 0)
# Capture everything up to the opening object
text =
lookahead_not(opening_tag)
# ... as long as it's a character
|> utf8_char([])
# ... and there's at least one character
|> times(min: 1)
# ... and then convert it to a string
|> reduce({List, :to_string, []})
# ... finally bag it and tag it
|> unwrap_and_tag(:text)
identifier =
utf8_string([?a..?z, ?A..?Z, ?_, ?0..?9], min: 1)
|> reduce({Enum, :join, []})
|> unwrap_and_tag(:identifier)
defparsecp(:expression, identifier)
# when spotting interpolation, ignore the opening tag and any whitespace
interpolation =
ignore(concat(opening_tag, optional_whitespaces))
# ... then parse the expression (identifier)
|> parsec(:expression)
# ... then ignore any whitespace and the closing tags after the expression
|> ignore(concat(optional_whitespaces, closing_tag))
# ... once again we bag it and tag it
|> unwrap_and_tag(:interpolation)
defparsec(:do_parse, choice([interpolation, text]) |> repeat() |> eos())
end
end
end

View file

@ -1,5 +1,34 @@
defmodule Pinchflat.Downloader.RenderedString.Parser do defmodule Pinchflat.RenderedString.Parser do
@moduledoc """ @moduledoc """
Parses liquid-ish-style strings into a rendered string Parses liquid-ish-style strings into a rendered string
Used for turning filepath templates into real filepaths
""" """
use Pinchflat.RenderedString.Base
@doc """
Parses a string into a rendered string, using the provided variables.
The variable keys MUST be strings.
"""
def parse(string, variables) do
# `do_parse` comes from `RenderedString.Base`
case do_parse(string) do
{:ok, parsed, _, _, _, _} ->
{:ok, build_string(parsed, variables)}
{:error, message, _, _, _, _} ->
{:error, message}
end
end
defp build_string(parsed, variables) do
Enum.reduce(parsed, "", fn element, acc ->
case element do
{:text, text} -> acc <> text
{:interpolation, {:identifier, identifier}} -> acc <> to_string(variables[identifier])
end
end)
end
end end

View file

@ -0,0 +1,40 @@
defmodule Pinchflat.RenderedString.ParserTest do
use ExUnit.Case, async: true
alias Pinchflat.RenderedString.Parser
describe "parse/2" do
test "it returns the rendered string when the string is valid" do
assert {:ok, "bar"} = Parser.parse("{{ foo }}", %{"foo" => "bar"})
end
test "it works with filepath-like strings" do
assert {:ok, "bar/baz"} =
Parser.parse("{{ foo }}/{{ bar }}", %{"foo" => "bar", "bar" => "baz"})
end
test "it works when mixing text and variables" do
assert {:ok, "bar text baz"} =
Parser.parse("{{ foo }} text {{ bar }}", %{"foo" => "bar", "bar" => "baz"})
end
test "it removes the placeholder but doesn't blow up when the variable isn't provided" do
assert {:ok, ""} = Parser.parse("{{ foo }}", %{})
end
test "it accepts any number of spaces between open and closing tags" do
assert {:ok, "bar"} = Parser.parse("{{foo}}", %{"foo" => "bar"})
assert {:ok, "bar"} = Parser.parse("{{ foo}}", %{"foo" => "bar"})
assert {:ok, "bar"} = Parser.parse("{{foo }}", %{"foo" => "bar"})
assert {:ok, "bar"} = Parser.parse("{{ foo }}", %{"foo" => "bar"})
end
test "it doesn't interpret single braces as variables" do
assert {:ok, "{foo}"} = Parser.parse("{foo}", %{})
end
test "it returns an error when the string is invalid" do
assert {:error, "expected end of string"} = Parser.parse("{{ 1-1 }", %{})
end
end
end