Refactor and document output path (#28)
* Updated output path builder to better use yt-dlp variables * Added UI explaining output template options * Fixed outdated reference in dockerfile
This commit is contained in:
@@ -11,7 +11,8 @@ module.exports = {
|
|||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
satoshi: ['Satoshi', 'sans-serif']
|
satoshi: ['Satoshi', 'sans-serif'],
|
||||||
|
...defaultTheme.fontFamily
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
'2xsm': '375px',
|
'2xsm': '375px',
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ WORKDIR /app
|
|||||||
COPY . ./
|
COPY . ./
|
||||||
|
|
||||||
# Needs permissions to be updated AFTER the copy step
|
# Needs permissions to be updated AFTER the copy step
|
||||||
RUN chmod +x ./docker-run.sh
|
RUN chmod +x ./docker-run.dev.sh
|
||||||
|
|
||||||
# Install Elixir deps
|
# Install Elixir deps
|
||||||
RUN mix deps.get
|
RUN mix deps.get
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||||||
schema "media_profiles" do
|
schema "media_profiles" do
|
||||||
field :name, :string
|
field :name, :string
|
||||||
|
|
||||||
field :output_path_template, :string, default: "/{{ uploader }}/{{ title }}/{{ title }}-{{ id }}.{{ ext }}"
|
field :output_path_template, :string, default: "/{{ channel }}/{{ title }}/{{ title }} [{{ id }}].{{ ext }}"
|
||||||
|
|
||||||
field :download_subs, :boolean, default: true
|
field :download_subs, :boolean, default: true
|
||||||
field :download_auto_subs, :boolean, default: true
|
field :download_auto_subs, :boolean, default: true
|
||||||
|
|||||||
@@ -12,50 +12,22 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder do
|
|||||||
|
|
||||||
Translates liquid-style templates into yt-dlp-style templates,
|
Translates liquid-style templates into yt-dlp-style templates,
|
||||||
leaving yt-dlp syntax intact.
|
leaving yt-dlp syntax intact.
|
||||||
|
|
||||||
IDEA: apart from any custom options I've defined, I can support any yt-dlp
|
|
||||||
option by assuming `{{ identifier }}` should transform to `%(identifier)S`.
|
|
||||||
It's not doing anything huge, but it's nicer to type and more approachable IMO.
|
|
||||||
IDEA: set a default for `MediaProfile`'s `output_path_template` field
|
|
||||||
"""
|
"""
|
||||||
def build(template_string) do
|
def build(template_string) do
|
||||||
TemplateParser.parse(template_string, full_yt_dlp_options_map())
|
TemplateParser.parse(template_string, custom_yt_dlp_option_map(), &identifier_fn/2)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp full_yt_dlp_options_map do
|
# The `nil` case simply wraps the identifier in yt-dlp-style syntax. This assumes that
|
||||||
Map.merge(
|
# the identifier is a valid yt-dlp option. The upside is that this gives the user
|
||||||
standard_yt_dlp_option_map(),
|
# access to ALL single-word yt-dlp options in the (imo) more friendly/forgiving liquid-style syntax.
|
||||||
custom_yt_dlp_option_map()
|
#
|
||||||
)
|
# For all "custom" variables, we use the `Map.get/3` function to look up the value in the provided.
|
||||||
end
|
# See `custom_yt_dlp_option_map` for a list of those.
|
||||||
|
defp identifier_fn(identifier, variables) do
|
||||||
defp standard_yt_dlp_option_map do
|
case Map.get(variables, identifier) do
|
||||||
%{
|
nil -> "%(#{identifier})S"
|
||||||
# Uppercase "S" means "safe" - ie: filepath-safe
|
value -> value
|
||||||
"id" => "%(id)S",
|
end
|
||||||
"ext" => "%(ext)S",
|
|
||||||
"title" => "%(title)S",
|
|
||||||
"fulltitle" => "%(fulltitle)S",
|
|
||||||
"uploader" => "%(uploader)S",
|
|
||||||
"creator" => "%(creator)S",
|
|
||||||
"upload_date" => "%(upload_date)S",
|
|
||||||
"release_date" => "%(release_date)S",
|
|
||||||
"duration" => "%(duration)S",
|
|
||||||
# For videos classified as an episode of a series:
|
|
||||||
"series" => "%(series)S",
|
|
||||||
"season" => "%(season)S",
|
|
||||||
"season_number" => "%(season_number)S",
|
|
||||||
"episode" => "%(episode)S",
|
|
||||||
"episode_number" => "%(episode_number)S",
|
|
||||||
"episode_id" => "%(episode_id)S",
|
|
||||||
# For videos classified as music:
|
|
||||||
"track" => "%(track)S",
|
|
||||||
"track_number" => "%(track_number)S",
|
|
||||||
"artist" => "%(artist)S",
|
|
||||||
"album" => "%(album)S",
|
|
||||||
"album_type" => "%(album_type)S",
|
|
||||||
"genre" => "%(genre)S"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp custom_yt_dlp_option_map do
|
defp custom_yt_dlp_option_map do
|
||||||
|
|||||||
@@ -8,28 +8,35 @@ defmodule Pinchflat.RenderedString.Parser do
|
|||||||
use Pinchflat.RenderedString.Base
|
use Pinchflat.RenderedString.Base
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Parses a string into a rendered string, using the provided variables.
|
Parses a string into a rendered string, using the provided variables. Optionally
|
||||||
|
takes a custom fetcher function for handling missing variables.
|
||||||
|
|
||||||
Variable identifiers are surrounded by {{ and }}. The variable keys MUST be strings.
|
Variable identifiers are surrounded by {{ and }}. The variable keys MUST be strings.
|
||||||
If an identifier is not found in the provided variables, it will be removed from the string.
|
If an identifier is not found in the provided variables, it will be removed from the string.
|
||||||
|
|
||||||
|
Returns `{:ok, binary()}` or `{:error, binary()}`.
|
||||||
"""
|
"""
|
||||||
def parse(string, variables) do
|
def parse(string, variables, value_fetch_fn \\ &default_fetcher/2) do
|
||||||
# `do_parse` comes from `RenderedString.Base`
|
# `do_parse` comes from `RenderedString.Base`
|
||||||
case do_parse(string) do
|
case do_parse(string) do
|
||||||
{:ok, parsed, _, _, _, _} ->
|
{:ok, parsed, _, _, _, _} ->
|
||||||
{:ok, build_string(parsed, variables)}
|
{:ok, build_string(parsed, variables, value_fetch_fn)}
|
||||||
|
|
||||||
{:error, message, _, _, _, _} ->
|
{:error, message, _, _, _, _} ->
|
||||||
{:error, message}
|
{:error, message}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_string(parsed, variables) do
|
defp build_string(parsed, variables, value_fetch_fn) do
|
||||||
Enum.reduce(parsed, "", fn element, acc ->
|
Enum.reduce(parsed, "", fn element, acc ->
|
||||||
case element do
|
case element do
|
||||||
{:text, text} -> acc <> text
|
{:text, text} -> acc <> text
|
||||||
{:interpolation, {:identifier, identifier}} -> acc <> to_string(variables[identifier])
|
{:interpolation, {:identifier, identifier}} -> acc <> value_fetch_fn.(identifier, variables)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def default_fetcher(identifier, variables) do
|
||||||
|
Map.get(variables, identifier, "")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ defmodule PinchflatWeb do
|
|||||||
# Core UI components and translation
|
# Core UI components and translation
|
||||||
import PinchflatWeb.Gettext
|
import PinchflatWeb.Gettext
|
||||||
import PinchflatWeb.CoreComponents
|
import PinchflatWeb.CoreComponents
|
||||||
|
import PinchflatWeb.CustomComponents.TextComponents
|
||||||
import PinchflatWeb.CustomComponents.TableComponents
|
import PinchflatWeb.CustomComponents.TableComponents
|
||||||
import PinchflatWeb.CustomComponents.ButtonComponents
|
import PinchflatWeb.CustomComponents.ButtonComponents
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
|
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
|
||||||
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
|
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
|
||||||
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
|
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
|
||||||
|
attr :inputclass, :string, default: ""
|
||||||
|
|
||||||
attr :rest, :global, include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
|
attr :rest, :global, include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
|
||||||
multiple pattern placeholder readonly required rows size step)
|
multiple pattern placeholder readonly required rows size step)
|
||||||
@@ -281,7 +282,15 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
<div phx-feedback-for={@name}>
|
<div phx-feedback-for={@name}>
|
||||||
<label class="flex items-center gap-4 text-sm leading-6">
|
<label class="flex items-center gap-4 text-sm leading-6">
|
||||||
<input type="hidden" name={@name} value="false" />
|
<input type="hidden" name={@name} value="false" />
|
||||||
<input type="checkbox" id={@id} name={@name} value="true" checked={@checked} class="rounded focus:ring-0" {@rest} />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={@id}
|
||||||
|
name={@name}
|
||||||
|
value="true"
|
||||||
|
checked={@checked}
|
||||||
|
class={["rounded focus:ring-0", @inputclass]}
|
||||||
|
{@rest}
|
||||||
|
/>
|
||||||
<%= @label %>
|
<%= @label %>
|
||||||
</label>
|
</label>
|
||||||
<.help :if={@help}><%= @help %></.help>
|
<.help :if={@help}><%= @help %></.help>
|
||||||
@@ -315,7 +324,10 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
<div x-bind:class="enabled && '!bg-primary'" class="block h-8 w-14 rounded-full bg-black"></div>
|
<div x-bind:class="enabled && '!bg-primary'" class="block h-8 w-14 rounded-full bg-black"></div>
|
||||||
<div
|
<div
|
||||||
x-bind:class="enabled && '!right-1 !translate-x-full'"
|
x-bind:class="enabled && '!right-1 !translate-x-full'"
|
||||||
class="absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-white transition"
|
class={[
|
||||||
|
"absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-white transition",
|
||||||
|
@inputclass
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,7 +347,8 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
name={@name}
|
name={@name}
|
||||||
class={[
|
class={[
|
||||||
"relative z-20 w-full appearance-none rounded border border-stroke bg-transparent py-3 pl-5 pr-12 outline-none transition",
|
"relative z-20 w-full appearance-none rounded border border-stroke bg-transparent py-3 pl-5 pr-12 outline-none transition",
|
||||||
"focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input text-black dark:text-white"
|
"focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input text-black dark:text-white",
|
||||||
|
@inputclass
|
||||||
]}
|
]}
|
||||||
multiple={@multiple}
|
multiple={@multiple}
|
||||||
{@rest}
|
{@rest}
|
||||||
@@ -359,6 +372,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
class={[
|
class={[
|
||||||
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
|
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
|
||||||
"min-h-[6rem] phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
|
"min-h-[6rem] phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
|
||||||
|
@inputclass,
|
||||||
@errors == [] && "border-zinc-300 focus:border-zinc-400",
|
@errors == [] && "border-zinc-300 focus:border-zinc-400",
|
||||||
@errors != [] && "border-rose-400 focus:border-rose-400"
|
@errors != [] && "border-rose-400 focus:border-rose-400"
|
||||||
]}
|
]}
|
||||||
@@ -384,6 +398,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent px-5 py-3 font-normal text-black",
|
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent px-5 py-3 font-normal text-black",
|
||||||
"outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter",
|
"outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter",
|
||||||
"dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary",
|
"dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary",
|
||||||
|
@inputclass,
|
||||||
@errors != [] && "border-rose-400 focus:border-rose-400"
|
@errors != [] && "border-rose-400 focus:border-rose-400"
|
||||||
]}
|
]}
|
||||||
{@rest}
|
{@rest}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
defmodule PinchflatWeb.CustomComponents.TextComponents do
|
||||||
|
@moduledoc false
|
||||||
|
use Phoenix.Component
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a code block with the given content.
|
||||||
|
"""
|
||||||
|
slot :inner_block
|
||||||
|
|
||||||
|
def inline_code(assigns) do
|
||||||
|
~H"""
|
||||||
|
<code class="inline-block text-sm font-mono text-gray bg-boxdark rounded-md p-0.5 mx-0.5 text-nowrap">
|
||||||
|
<%= render_slot(@inner_block) %>
|
||||||
|
</code>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a reference link with the given href and content.
|
||||||
|
"""
|
||||||
|
attr :href, :string, required: true
|
||||||
|
slot :inner_block
|
||||||
|
|
||||||
|
def reference_link(assigns) do
|
||||||
|
~H"""
|
||||||
|
<.link href={@href} target="_blank" class="text-blue-500 hover:text-blue-300">
|
||||||
|
<%= render_slot(@inner_block) %>
|
||||||
|
</.link>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,4 +28,21 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
{"360p", "360p"}
|
{"360p", "360p"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def custom_output_template_options do
|
||||||
|
~w(upload_day upload_month upload_year)a
|
||||||
|
end
|
||||||
|
|
||||||
|
def common_output_template_options do
|
||||||
|
~w(
|
||||||
|
id
|
||||||
|
ext
|
||||||
|
title
|
||||||
|
fulltitle
|
||||||
|
uploader
|
||||||
|
channel
|
||||||
|
upload_date
|
||||||
|
duration_string
|
||||||
|
)a
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+9
-5
@@ -6,11 +6,13 @@
|
|||||||
General Options
|
General Options
|
||||||
</h3>
|
</h3>
|
||||||
<.input field={f[:name]} type="text" label="Name" placeholder="New Profile" help="(required)" />
|
<.input field={f[:name]} type="text" label="Name" placeholder="New Profile" help="(required)" />
|
||||||
|
|
||||||
<.input
|
<.input
|
||||||
field={f[:output_path_template]}
|
field={f[:output_path_template]}
|
||||||
type="text"
|
type="text"
|
||||||
|
inputclass="font-mono"
|
||||||
label="Output path template"
|
label="Output path template"
|
||||||
help="TODO: provide docs (required)"
|
help="Must end with .{{ ext }}. See below for more details. I promise the default is good for most cases (required)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 class="mb-4 mt-8 text-2xl text-black dark:text-white">
|
<h3 class="mb-4 mt-8 text-2xl text-black dark:text-white">
|
||||||
@@ -67,7 +69,7 @@
|
|||||||
options={friendly_format_type_options()}
|
options={friendly_format_type_options()}
|
||||||
type="select"
|
type="select"
|
||||||
label="Include Shorts?"
|
label="Include Shorts?"
|
||||||
help="Experimental"
|
help="Experimental. Please report any issues on GitHub"
|
||||||
/>
|
/>
|
||||||
<.input
|
<.input
|
||||||
field={f[:livestream_behaviour]}
|
field={f[:livestream_behaviour]}
|
||||||
@@ -88,7 +90,9 @@
|
|||||||
help="Will grab the closest available resolution if your preferred is not available"
|
help="Will grab the closest available resolution if your preferred is not available"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<:actions>
|
<.button class="mt-15 mb-5 sm:mb-7.5">Save Media profile</.button>
|
||||||
<.button class="mt-15 mb-5 sm:mb-7.5">Save Media profile</.button>
|
|
||||||
</:actions>
|
<div class="rounded-sm dark:bg-meta-4 p-4 md:p-6 mb-5">
|
||||||
|
<.output_template_help />
|
||||||
|
</div>
|
||||||
</.simple_form>
|
</.simple_form>
|
||||||
|
|||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
<%!-- The heex HTML formatter is really struggling with this file - I apologize in advance --%>
|
||||||
|
<aside>
|
||||||
|
<h2 class="text-xl font-bold mb-2">Output Template Syntax</h2>
|
||||||
|
<section class="ml-2 md:ml-4 mb-4 max-w-prose">
|
||||||
|
<p>When generating an output template, you have 3 options for syntax:</p>
|
||||||
|
<ul class="list-disc list-inside ml-2 md:ml-5">
|
||||||
|
<li>
|
||||||
|
Liquid-style:
|
||||||
|
<.inline_code>/{{ channel }}/{{ title }} - {{ id }}.{{ ext }}</.inline_code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code class="text-sm">yt-dlp</code>-style
|
||||||
|
<.reference_link href="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#output-template">
|
||||||
|
<.icon name="hero-arrow-top-right-on-square" class="h-4 w-4" />
|
||||||
|
</.reference_link>:
|
||||||
|
<.inline_code>/%(channel)s/%(duration>%H-%M-%S)s-%(id)s.%(ext)s</.inline_code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Any bare words:
|
||||||
|
<.inline_code>/videos/1080p/{{ id }}.{{ ext }}</.inline_code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="my-2">
|
||||||
|
Apart from custom aliases, the liquid-style syntax maps 1:1 to the <code class="text-sm">yt-dlp</code>-style syntax behind-the-scenes. This means that
|
||||||
|
<em>any</em>
|
||||||
|
single-word <code class="text-sm">yt-dlp</code>
|
||||||
|
option can be used as liquid-style and it's automatically made filepath-safe. For example, the
|
||||||
|
<.inline_code>{{ duration }}</.inline_code>
|
||||||
|
option is translated to
|
||||||
|
<.inline_code>%(duration)S</.inline_code>
|
||||||
|
</p>
|
||||||
|
<p class="my-2">
|
||||||
|
<strong>Major 🔑:</strong>
|
||||||
|
these syntaxes can be mixed and matched freely! I prefer to use liquid-style and bare words
|
||||||
|
but I'll include <code class="text-sm">yt-dlp</code>-style when I need more control. For example:
|
||||||
|
<.inline_code>/1080p/{{ channel }}/{{ title }}-(%(subtitles.en.-1.ext)s).{{ ext }}</.inline_code>
|
||||||
|
</p>
|
||||||
|
<p class="my-2">
|
||||||
|
<strong>NOTE:</strong>
|
||||||
|
Your template <em>must</em>
|
||||||
|
end with an extension option (<.inline_code>.{{ ext }}</.inline_code>
|
||||||
|
or
|
||||||
|
<.inline_code>.%(ext)S</.inline_code>).
|
||||||
|
Downloading won't work as expected without it.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-bold mb-2">Template Options</h2>
|
||||||
|
<section class="ml-2 md:ml-4 mb-4 max-w-prose">
|
||||||
|
<p>
|
||||||
|
Any single-word <code class="text-sm">yt-dlp</code>
|
||||||
|
option
|
||||||
|
<.reference_link href="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#output-template">
|
||||||
|
<.icon name="hero-arrow-top-right-on-square" class="h-4 w-4" />
|
||||||
|
</.reference_link>
|
||||||
|
can be used with the curly braced liquid-style syntax.
|
||||||
|
This is just a list of the most common options as well as some custom aliases
|
||||||
|
</p>
|
||||||
|
<h3 class="text-lg font-bold mb-2">Custom Aliases</h3>
|
||||||
|
<ul class="list-disc list-inside ml-2 md:ml-5">
|
||||||
|
<li :for={opt <- custom_output_template_options()}>
|
||||||
|
<.inline_code>{{ <%= opt %> }}</.inline_code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h3 class="text-lg font-bold mb-2">Common Options</h3>
|
||||||
|
<ul class="list-disc list-inside ml-2 md:ml-5">
|
||||||
|
<li :for={opt <- common_output_template_options()}>
|
||||||
|
<.inline_code>{{ <%= opt %> }}</.inline_code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
@@ -3,7 +3,7 @@ defmodule Pinchflat.RenderedString.ParserTest do
|
|||||||
|
|
||||||
alias Pinchflat.RenderedString.Parser
|
alias Pinchflat.RenderedString.Parser
|
||||||
|
|
||||||
describe "parse/2" do
|
describe "parse/3" do
|
||||||
test "it returns the rendered string when the string is valid" do
|
test "it returns the rendered string when the string is valid" do
|
||||||
assert {:ok, "bar"} = Parser.parse("{{ foo }}", %{"foo" => "bar"})
|
assert {:ok, "bar"} = Parser.parse("{{ foo }}", %{"foo" => "bar"})
|
||||||
end
|
end
|
||||||
@@ -36,5 +36,13 @@ defmodule Pinchflat.RenderedString.ParserTest do
|
|||||||
test "it returns an error when the string is invalid" do
|
test "it returns an error when the string is invalid" do
|
||||||
assert {:error, "expected end of string"} = Parser.parse("{{ 1-1 }", %{})
|
assert {:error, "expected end of string"} = Parser.parse("{{ 1-1 }", %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it supports a custom fetcher function" do
|
||||||
|
custom_fetcher = fn _, _ ->
|
||||||
|
"quux"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert {:ok, "quux"} = Parser.parse("{{ foo }}", %{}, custom_fetcher)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user