Added basic parser for hydrating template strings
This commit is contained in:
parent
05371cdac6
commit
7dcc893297
5 changed files with 122 additions and 1 deletions
52
lib/pinchflat/rendered_string/base.ex
Normal file
52
lib/pinchflat/rendered_string/base.ex
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
40
test/pinchflat/rendered_string/parser_test.exs
Normal file
40
test/pinchflat/rendered_string/parser_test.exs
Normal 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
|
||||||
Loading…
Reference in a new issue