Skip to content
Merged
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
273 changes: 153 additions & 120 deletions CodeGen.roc
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,86 @@ module [generate]
import Parser exposing [Node]

generate : List { name : Str, nodes : List Node } -> Str
generate = \templates ->
generate = |templates|

functions =
List.map templates render_template
|> Str.joinWith "\n\n"
List.map(templates, render_template)
|> Str.join_with("\n\n")

names =
templates
|> List.map \template -> " $(template.name),"
|> Str.joinWith "\n"
|> List.map(|template| " ${template.name},")
|> Str.join_with("\n")

imports =
templates
|> List.walk (Set.empty {}) \acc, template -> Set.union acc (module_imports template)
|> Set.toList
|> List.map \module_import -> "import $(module_import)"
|> Str.joinWith "\n"
|> List.walk(Set.empty({}), |acc, template| Set.union(acc, module_imports(template)))
|> Set.to_list
|> List.map(|module_import| "import ${module_import}")
|> Str.join_with("\n")

# If we included escapeHtml in every module, templates without interpolations would
# trigger a warning when running roc check for escapeHtml not being used.
requires_escape_html = List.any templates \{ nodes } ->
contains_interpolation nodes
requires_escape_html = List.any(
templates,
|{ nodes }|
contains_interpolation(nodes),
)

"""
## Generated by RTL https://github.com/isaacvando/rtl
module [
$(names)
${names}
]

$(imports)
${imports}

$(functions)
$(if requires_escape_html then escape_html else "")
${functions}
${if requires_escape_html then escape_html else ""}
"""

module_imports = \template ->
module_imports = |template|
template.nodes
|> List.walk (Set.empty {}) \acc, n ->
when n is
ModuleImport m -> Set.insert acc m
_ -> acc
|> List.walk(
Set.empty({}),
|acc, n|
when n is
ModuleImport(m) -> Set.insert(acc, m)
_ -> acc,
)

contains_interpolation : List Node -> Bool
contains_interpolation = \nodes ->
List.any nodes \node ->
when node is
Interpolation _ -> Bool.true
Conditional { true_branch, false_branch } ->
contains_interpolation true_branch || contains_interpolation false_branch

Sequence { body } -> contains_interpolation body
WhenIs { cases } ->
List.any cases \case ->
contains_interpolation case.branch

Text _ | RawInterpolation _ | ModuleImport _ -> Bool.false
contains_interpolation = |nodes|
List.any(
nodes,
|node|
when node is
Interpolation(_) -> Bool.true
Conditional({ true_branch, false_branch }) ->
contains_interpolation(true_branch) or contains_interpolation(false_branch)

Sequence({ body }) -> contains_interpolation(body)
WhenIs({ cases }) ->
List.any(
cases,
|case|
contains_interpolation(case.branch),
)

Text(_) | RawInterpolation(_) | ModuleImport(_) -> Bool.false,
)

escape_html =
"""

escape_html : Str -> Str
escape_html = \\input ->
input
|> Str.replaceEach "&" "&"
|> Str.replaceEach "<" "&lt;"
|> Str.replaceEach ">" "&gt;"
|> Str.replaceEach "\\"" "&quot;"
|> Str.replaceEach "'" "&#39;"
|> Str.replace_each "&" "&amp;"
|> Str.replace_each "<" "&lt;"
|> Str.replace_each ">" "&gt;"
|> Str.replace_each "\\"" "&quot;"
|> Str.replace_each "'" "&#39;"
"""

RenderNode : [
Expand All @@ -81,134 +93,155 @@ RenderNode : [
]

render_template : { name : Str, nodes : List Node } -> Str
render_template = \{ name, nodes } ->
render_template = |{ name, nodes }|
body =
condense nodes
condense(nodes)
|> render_nodes

# We check if the model was used in the template so that we can ignore the parameter
# if it was not used to prevent an unused field warning from showing up.
"""
$(name) = \\$(if is_model_used_in_list nodes then "" else "_")model ->
$(body)
${name} = \\${if is_model_used_in_list(nodes) then "" else "_"}model ->
${body}
"""

render_nodes : List RenderNode -> Str
render_nodes = \nodes ->
when List.map nodes to_str is
render_nodes = |nodes|
when List.map(nodes, to_str) is
[] -> "\"\"" |> indent
[elem] -> elem
blocks ->
list = blocks |> Str.joinWith ",\n"
list = blocks |> Str.join_with(",\n")
"""
[
$(list)
${list}
]
|> Str.joinWith ""
|> Str.join_with ""
"""
|> indent

to_str = \node ->
to_str = |node|
block =
when node is
Text t ->
Text(t) ->
"""
\"""
$(t)
${t}
\"""
"""

Conditional { condition, true_branch, false_branch } ->
Conditional({ condition, true_branch, false_branch }) ->
"""
if $(condition) then
$(render_nodes true_branch)
if ${condition} then
${render_nodes(true_branch)}
else
$(render_nodes false_branch)
${render_nodes(false_branch)}
"""

Sequence { item, list, body } ->
Sequence({ item, list, body }) ->
"""
List.map $(list) \\$(item) ->
$(render_nodes body)
|> Str.joinWith ""
List.map ${list} \\${item} ->
${render_nodes(body)}
|> Str.join_with ""
"""

WhenIs { expression, cases } ->
WhenIs({ expression, cases }) ->
branches =
List.map cases \{ pattern, branch } ->
"""
$(pattern) ->
$(render_nodes branch)
"""
|> Str.joinWith "\n"
List.map(
cases,
|{ pattern, branch }|
"""
${pattern} ->
${render_nodes(branch)}
""",
)
|> Str.join_with("\n")
|> indent
"""
when $(expression) is
$(branches)
when ${expression} is
${branches}

"""
indent block
indent(block)

condense : List Node -> List RenderNode
condense = \nodes ->
List.map nodes \node ->
when node is
RawInterpolation i -> Text "\$($(i))"
Interpolation i -> Text "\$($(i) |> escape_html)"
Text t ->
# Escape Roc string interpolations from the template
escaped = Str.replaceEach t "$" "\\$"
Text escaped

Sequence { item, list, body } -> Sequence { item, list, body: condense body }
ModuleImport _ -> Text ""
Conditional { condition, true_branch, false_branch } ->
Conditional {
condition,
true_branch: condense true_branch,
false_branch: condense false_branch,
}

WhenIs { expression, cases } ->
WhenIs {
expression,
cases: List.map cases \{ pattern, branch } ->
{ pattern, branch: condense branch },
}
|> List.walk [] \state, elem ->
when (state, elem) is
([.. as rest, Text x], Text y) ->
combined = Str.concat x y |> Text
rest |> List.append combined

_ -> List.append state elem

is_model_used_in_list = \nodes ->
List.any nodes is_model_used_in_node
condense = |nodes|
List.map(
nodes,
|node|
when node is
RawInterpolation(i) -> Text("\$(${i})")
Interpolation(i) -> Text("\$(${i} |> escape_html)")
Text(t) ->
# Escape Roc string interpolations from the template
escaped = Str.replace_each(t, "$", "\\$")
Text(escaped)

Sequence({ item, list, body }) -> Sequence({ item, list, body: condense(body) })
ModuleImport(_) -> Text("")
Conditional({ condition, true_branch, false_branch }) ->
Conditional(
{
condition,
true_branch: condense(true_branch),
false_branch: condense(false_branch),
},
)

WhenIs({ expression, cases }) ->
WhenIs(
{
expression,
cases: List.map(
cases,
|{ pattern, branch }|
{ pattern, branch: condense(branch) },
),
},
),
)
|> List.walk(
[],
|state, elem|
when (state, elem) is
([.. as rest, Text(x)], Text(y)) ->
combined = Str.concat(x, y) |> Text
rest |> List.append(combined)

_ -> List.append(state, elem),
)

is_model_used_in_list = |nodes|
List.any(nodes, is_model_used_in_node)

# We can't determine with full certainty if the model was used without parsing the Roc code that
# is used on the template, which we don't do. This is a heuristic that just checks if any of the spots
# that could reference the model contain "model". So a string literal that contains "model" could create
# a false positive, but this isn't a big deal.
is_model_used_in_node = \node ->
contains_model = \str ->
Str.contains str "model"
is_model_used_in_node = |node|
contains_model = |str|
Str.contains(str, "model")
when node is
Interpolation i | RawInterpolation i -> contains_model i
Conditional { condition, true_branch, false_branch } ->
contains_model condition || is_model_used_in_list true_branch || is_model_used_in_list false_branch
Interpolation(i) | RawInterpolation(i) -> contains_model(i)
Conditional({ condition, true_branch, false_branch }) ->
contains_model(condition) or is_model_used_in_list(true_branch) or is_model_used_in_list(false_branch)

Sequence { list, body } -> contains_model list || is_model_used_in_list body
WhenIs { expression, cases } ->
contains_model expression
|| List.any cases \case ->
is_model_used_in_list case.branch
Sequence({ list, body }) -> contains_model(list) or is_model_used_in_list(body)
WhenIs({ expression, cases }) ->
contains_model(expression)
or List.any(
cases,
|case|
is_model_used_in_list(case.branch),
)

Text _ | ModuleImport _ -> Bool.false
Text(_) | ModuleImport(_) -> Bool.false

indent : Str -> Str
indent = \input ->
Str.splitOn input "\n"
|> List.map \str ->
Str.concat " " str
|> Str.joinWith "\n"
indent = |input|
Str.split_on(input, "\n")
|> List.map(
|str|
Str.concat(" ", str),
)
|> Str.join_with("\n")
Loading