Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions lib/code.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
defprotocol Typst.Code do
def encode(t)
end

defimpl Typst.Code, for: Map do
def encode(map) when map_size(map) == 0 do
"(:)"
end

def encode(map) do
"(#{encode_kv(map)})"
end

def encode_kv(map) do
Enum.map_join(map, ", ", fn
{k, v} when is_binary(k) ->
"#{String.Chars.BitString.to_string(k)}: #{Typst.Code.encode(v)}"

{k, v} when is_atom(k) ->
"#{Typst.Code.Atom.encode(k)}: #{Typst.Code.encode(v)}"
end)
end
end

defimpl Typst.Code, for: List do
def encode(list) do
if Keyword.keyword?(list) do
"(#{Typst.Code.Map.encode_kv(list)})"
else
"(#{Enum.map_join(list, ", ", &Typst.Code.encode/1)})"
end
end
end

defimpl Typst.Code, for: Integer do
def encode(int) do
String.Chars.Integer.to_string(int)
end
end

defimpl Typst.Code, for: Float do
def encode(float) do
String.Chars.Float.to_string(float)
end
end

if Code.ensure_loaded?(Decimal) do
defimpl Typst.Code, for: Decimal do
def encode(decimal) do
"decimal(\"#{Decimal.to_string(decimal)}\")"
end
end
end

defimpl Typst.Code, for: BitString do
def encode(str) do
if String.printable?(str) do
encode_printable(str)
else
encode_bytes(str)
end
end

defp encode_printable(str) do
replacements = %{
"\\" => "\\\\",
"\"" => "\\\"",
"\n" => "\\n",
"\t" => "\\t",
"\r" => "\\r"
}

escaped = String.replace(str, Map.keys(replacements), &Map.fetch!(replacements, &1))

"\"#{escaped}\""
end

defp encode_bytes(bytes) do
bytes =
bytes
|> :binary.bin_to_list()
|> Enum.join(", ")

"bytes(#{bytes})"
end
end

defimpl Typst.Code, for: Atom do
def encode(atom) do
Atom.to_string(atom)
end
end

defimpl Typst.Code, for: Tuple do
def encode({:label, label}) when is_atom(label) do
"<#{Atom.to_string(label)}>"
end
end

defimpl Typst.Code, for: Date do
def encode(date) do
kv =
Typst.Code.Map.encode_kv(
year: date.year,
month: date.month,
day: date.day
)

"datetime(#{kv})"
end
end

defimpl Typst.Code, for: Time do
def encode(time) do
kv =
Typst.Code.Map.encode_kv(
hour: time.hour,
minute: time.minute,
second: time.second
)

"datetime(#{kv})"
end
end

defimpl Typst.Code, for: NaiveDateTime do
def encode(naive) do
kv =
Typst.Code.Map.encode_kv(
year: naive.year,
month: naive.month,
day: naive.day,
hour: naive.hour,
minute: naive.minute,
second: naive.second
)

"datetime(#{kv})"
end
end

defimpl Typst.Code, for: DateTime do
def encode(datetime) do
datetime
|> DateTime.to_naive()
|> Typst.Code.NaiveDateTime.encode()
end
end

defimpl Typst.Code, for: Regex do
def encode(regex) do
"regex(`#{regex.source}`.text)"
end
end
104 changes: 104 additions & 0 deletions lib/engine.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
defmodule Typst.Engine do
@behaviour EEx.Engine

@impl EEx.Engine
def init(opts) do
EEx.Engine.init(opts)
end

@impl EEx.Engine
def handle_body(state) do
EEx.Engine.handle_body(state)
end

@impl EEx.Engine
def handle_begin(state) do
EEx.Engine.handle_begin(state)
end

@impl EEx.Engine
def handle_end(state) do
EEx.Engine.handle_end(state)
end

@impl EEx.Engine
def handle_text(state, meta, text) do
EEx.Engine.handle_text(state, meta, text)
end

@impl EEx.Engine
def handle_expr(state, "=", ast) do
ast = traverse(ast)
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
var = Macro.var(:"arg#{vars_count}", __MODULE__)

ast =
quote do
unquote(var) = Typst.Code.encode(unquote(ast))
end

segment =
quote do
unquote(var) :: binary
end

%{state | dynamic: [ast | dynamic], binary: [segment | binary], vars_count: vars_count + 1}
end

def handle_expr(state, "|", ast) do
ast = traverse(ast)
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
var = Macro.var(:"arg#{vars_count}", __MODULE__)

ast =
quote do
unquote(var) = Typst.Markup.encode(unquote(ast))
end

segment =
quote do
unquote(var) :: binary
end

%{state | dynamic: [ast | dynamic], binary: [segment | binary], vars_count: vars_count + 1}
end

def handle_expr(state, marker, expr) do
expr = traverse(expr)
EEx.Engine.handle_expr(state, marker, expr)
end

# Assigns Traversal
# There is `EEx.Engine.handle_assign/1`, but it doesn't raise, but only warn.
#
defp traverse(expr) do
Macro.prewalk(expr, &handle_assign/1)
end

defp handle_assign({:@, meta, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do
quote line: meta[:line] || 0 do
Typst.Engine.fetch_assign!(var!(assigns), unquote(name))
end
end

defp handle_assign(arg), do: arg

@doc false
def fetch_assign!(assigns, key) do
case Access.fetch(assigns, key) do
{:ok, val} ->
val

:error ->
raise ArgumentError, """
assign @#{key} not available in template.

Please make sure all proper assigns have been set. If this
is a child template, ensure assigns are given explicitly by
the parent template as they are not automatically forwarded.

Available assigns: #{inspect(Enum.map(assigns, &elem(&1, 0)))}
"""
end
end
end
64 changes: 0 additions & 64 deletions lib/format.ex

This file was deleted.

49 changes: 26 additions & 23 deletions lib/format/table.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
defmodule Typst.Format.Table do
import Typst.Format

@moduledoc """
Creates a typst [`#table()`](https://typst.app/docs/reference/model/table) and implements the `String.Chars` protocol for easy EEx interpolation.
To build more complex tables you can use the structs under this module like `Typst.Format.Table.Hline`
Expand Down Expand Up @@ -45,28 +43,33 @@ defmodule Typst.Format.Table do
:rows
]

defimpl String.Chars do
def to_string(%Typst.Format.Table{} = table) do
[
"#table(",
[
if_set(table.columns, "columns: #{table.columns}"),
if_set(table.rows, "rows: #{table.rows}"),
if_set(table.gutter, "gutter: #{table.gutter}"),
if_set(table.column_gutter, "column-gutter: #{table.column_gutter}"),
if_set(table.row_gutter, "row-gutter: #{table.row_gutter}"),
if_set(table.fill, "fill: #{table.fill}"),
if_set(table.align, "align: #{table.align}"),
if_set(table.stroke, "stroke: #{table.stroke}"),
if_set(table.inset, "inset: #{table.inset}")
]
|> Enum.reject(fn item -> item == [] end)
|> Enum.intersperse(", ")
|> maybe_append_separator(),
Typst.Format.recurse(table.content),
")"
defimpl Typst.Markup do
def encode(%Typst.Format.Table{} = table) do
option_fields = [
:columns,
:gutter,
:row_gutter,
:column_gutter,
:fill,
:align,
:stroke,
:inset,
:rows
]
|> IO.iodata_to_binary()

kv =
for k <- option_fields,
v <- Map.fetch!(table, k),
v do
{k, v}
end
|> Typst.Code.Map.encode_kv()
|> then(fn
"" -> ""
str -> str <> ", "
end)

"#table(#{kv}..#{Typst.Code.encode(table.content)})"
end
end

Expand Down
Loading