From 6a137c5638648fb5e7b9a13c46c86fe7c7fdaa95 Mon Sep 17 00:00:00 2001 From: Isaac Van Doren <69181572+isaacvando@users.noreply.github.com> Date: Thu, 21 Mar 2024 18:26:57 -0500 Subject: [PATCH 1/3] add placeholder for template --- template/index.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 template/index.md diff --git a/template/index.md b/template/index.md new file mode 100644 index 0000000..7e2a579 --- /dev/null +++ b/template/index.md @@ -0,0 +1 @@ +# Template Engine From 25604c12c661fe9e1591afd4ac392fc85b87f703 Mon Sep 17 00:00:00 2001 From: Isaac Van Doren <69181572+isaacvando@users.noreply.github.com> Date: Wed, 27 Mar 2024 21:18:22 -0400 Subject: [PATCH 2/3] add template draft --- template/.gitignore | 5 + template/CodeGen.roc | 127 +++++++++++ template/LICENSE.md | 17 ++ template/Parser.roc | 382 ++++++++++++++++++++++++++++++++++ template/README.md | 98 +++++++++ template/compile.roc | 72 +++++++ template/example/Pages.roc | 67 ++++++ template/example/blogPost.rtl | 14 ++ template/example/home.rtl | 19 ++ template/example/server.roc | 60 ++++++ template/index.md | 36 +++- 11 files changed, 896 insertions(+), 1 deletion(-) create mode 100644 template/.gitignore create mode 100644 template/CodeGen.roc create mode 100644 template/LICENSE.md create mode 100644 template/Parser.roc create mode 100644 template/README.md create mode 100644 template/compile.roc create mode 100644 template/example/Pages.roc create mode 100644 template/example/blogPost.rtl create mode 100644 template/example/home.rtl create mode 100644 template/example/server.roc diff --git a/template/.gitignore b/template/.gitignore new file mode 100644 index 0000000..77a422c --- /dev/null +++ b/template/.gitignore @@ -0,0 +1,5 @@ +server +engine +output +compile +output.roc diff --git a/template/CodeGen.roc b/template/CodeGen.roc new file mode 100644 index 0000000..4385b5d --- /dev/null +++ b/template/CodeGen.roc @@ -0,0 +1,127 @@ +interface CodeGen + exposes [generate] + imports [Parser.{ Node }] + +generate : List { name : Str, nodes : List Node } -> Str +generate = \templates -> + functions = + List.map templates renderTemplate + |> Str.joinWith "\n\n" + names = + List.map templates .name + |> Str.joinWith ",\n" + |> indent + |> indent + + """ + interface Pages + exposes [ + $(names) + ] + imports [] + + $(functions) + + escapeHtml : Str -> Str + escapeHtml = \\input -> + input + |> Str.replaceEach "&" "&" + |> Str.replaceEach "<" "<" + |> Str.replaceEach ">" ">" + |> Str.replaceEach "\\"" """ + |> Str.replaceEach "'" "'" + + """ + +# \"" + +RenderNode : [ + Text Str, + Conditional { condition : Str, trueBranch : List RenderNode, falseBranch : List RenderNode }, + Sequence { item : Str, list : Str, body : List RenderNode }, +] + +renderTemplate : { name : Str, nodes : List Node } -> Str +renderTemplate = \{ name, nodes } -> + body = + condense nodes + |> renderNodes + + """ + $(name) = \\model -> + $(body) + """ + +renderNodes : List RenderNode -> Str +renderNodes = \nodes -> + when List.map nodes toStr is + [] -> "\"\"" |> indent + [elem] -> elem + blocks -> + list = blocks |> Str.joinWith ",\n" + """ + [ + $(list) + ] + |> Str.joinWith "" + """ + |> indent + +toStr = \node -> + block = + when node is + Text t -> + """ + \""" + $(t) + \""" + """ + + Conditional { condition, trueBranch, falseBranch } -> + """ + if $(condition) then + $(renderNodes trueBranch) + else + $(renderNodes falseBranch) + """ + + Sequence { item, list, body } -> + """ + List.map $(list) \\$(item) -> + $(renderNodes body) + |> Str.joinWith "" + """ + indent block + +condense : List Node -> List RenderNode +condense = \nodes -> + List.map nodes \node -> + when node is + RawInterpolation i -> Text "\$($(i))" + Interpolation i -> Text "\$($(i) |> escapeHtml)" + 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 } + Conditional { condition, trueBranch, falseBranch } -> + Conditional { + condition, + trueBranch: condense trueBranch, + falseBranch: condense falseBranch, + } + |> 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 + +indent : Str -> Str +indent = \input -> + Str.split input "\n" + |> List.map \str -> + Str.concat " " str + |> Str.joinWith "\n" diff --git a/template/LICENSE.md b/template/LICENSE.md new file mode 100644 index 0000000..5493086 --- /dev/null +++ b/template/LICENSE.md @@ -0,0 +1,17 @@ +Copyright (c) 2024 Isaac Van Doren and contributors + +The Universal Permissive License (UPL), Version 1.0 + +Subject to the condition set forth below, permission is hereby granted to any person obtaining a copy of this software, associated documentation and/or data (collectively the “Software”), free of charge and under any and all copyright rights in the Software, and any and all patent rights owned or freely licensable by each licensor hereunder covering either (i) the unmodified Software as contributed to or provided by such licensor, or (ii) the Larger Works (as defined below), to deal in both + +(a) the Software, and + +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is included with the Software (each a “Larger Work” to which the Software is contributed by such licensors), + +without restriction, including without limitation the rights to copy, create derivative works of, display, perform, and distribute the Software and make, use, sell, offer for sale, import, export, have made, and have sold the Software and the Larger Work(s), and to sublicense the foregoing rights on either these or other terms. + +This license is subject to the following condition: + +The above copyright notice and either this complete permission notice or at a minimum a reference to the UPL must be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/template/Parser.roc b/template/Parser.roc new file mode 100644 index 0000000..b6e9940 --- /dev/null +++ b/template/Parser.roc @@ -0,0 +1,382 @@ +interface Parser + exposes [parse, Node] + imports [] + +Node : [ + Text Str, + Interpolation Str, + RawInterpolation Str, + Conditional { condition : Str, trueBranch : List Node, falseBranch : List Node }, + Sequence { item : Str, list : Str, body : List Node }, +] + +parse : Str -> List Node +parse = \input -> + when Str.toUtf8 input |> (many node) is + Match { input: [], val } -> combineTextNodes val + Match _ -> crash "There is a bug! Not all input was consumed." + NoMatch -> crash "There is a bug! The parser failed." + +combineTextNodes : List Node -> List Node +combineTextNodes = \nodes -> + List.walk nodes [] \state, elem -> + when (state, elem) is + ([.. as rest, Text t1], Text t2) -> + List.append rest (Text (Str.concat t1 t2)) + + (_, Conditional { condition, trueBranch, falseBranch }) -> + List.append state (Conditional { condition, trueBranch: combineTextNodes trueBranch, falseBranch: combineTextNodes falseBranch }) + + (_, Sequence { item, list, body }) -> + List.append state (Sequence { item, list, body: combineTextNodes body }) + + _ -> List.append state elem + +Parser a : List U8 -> [Match { input : List U8, val : a }, NoMatch] + +# Parsers + +node = + oneOf [ + rawInterpolation, + interpolation, + conditionalElse, + conditionalIf, + sequence, + text, + ] + +interpolation : Parser Node +interpolation = + manyUntil anyByte (string "}}") + |> startWith (string "{{") + |> map \bytes -> + unsafeFromUtf8 bytes + |> Str.trim + |> Interpolation + +rawInterpolation : Parser Node +rawInterpolation = + manyUntil anyByte (string "}}}") + |> startWith (string "{{{") + |> map \bytes -> + unsafeFromUtf8 bytes + |> Str.trim + |> RawInterpolation + +conditionalIf = + condition <- manyUntil anyByte (string " |}") + |> startWith (string "{|if ") + |> leftoverTagSpace + |> andThen + + endIf = + string "{|endif|}" + |> startWith (optional (string "\n")) + |> leftoverTagSpace + + trueBranch <- manyUntil node endIf + |> andThen + + val = Conditional { + condition: unsafeFromUtf8 condition, + trueBranch, + falseBranch: [], + } + + \input -> Match { input, val } + +conditionalElse = + condition <- manyUntil anyByte (string " |}") + |> startWith (string "{|if ") + |> leftoverTagSpace + |> andThen + + elseSep = + string "{|else|}" + |> startWith (optional (string "\n")) + |> leftoverTagSpace + + endIf = + string "{|endif|}" + |> startWith (optional (string "\n")) + |> leftoverTagSpace + + trueBranch <- manyUntil node elseSep + |> andThen + + falseBranch <- manyUntil node endIf + |> andThen + + val = Conditional { + condition: unsafeFromUtf8 condition, + trueBranch, + falseBranch, + } + + \input -> Match { input, val } + +sequence : Parser Node +sequence = + item <- manyUntil anyByte (string " : ") + |> startWith (string "{|list ") + |> andThen + + list <- + manyUntil anyByte (string " |}") + |> leftoverTagSpace + |> andThen + + endList = + string "{|endlist|}" + |> leftoverTagSpace + + body <- manyUntil node endList + |> andThen + + val = Sequence { + item: unsafeFromUtf8 item, + list: unsafeFromUtf8 list, + body: body, + } + + \input -> Match { input, val } + +leftoverTagSpace : Parser a -> Parser a +leftoverTagSpace = \parser -> + parser + |> endWith (optional (string "\n")) + |> endWith (many hSpace) + +text : Parser Node +text = + anyByte + |> map \byte -> + unsafeFromUtf8 [byte] + |> Text + +hSpace : Parser Str +hSpace = + oneOf [string " ", string "\t"] + +string : Str -> Parser Str +string = \str -> + \input -> + bytes = Str.toUtf8 str + if List.startsWith input bytes then + Match { input: List.dropFirst input (List.len bytes), val: str } + else + NoMatch + +anyByte : Parser U8 +anyByte = \input -> + when input is + [first, .. as rest] -> Match { input: rest, val: first } + _ -> NoMatch + +# Combinators + +startWith : Parser a, Parser * -> Parser a +startWith = \parser, start -> + andThen start \_ -> + parser + +endWith : Parser a, Parser * -> Parser a +endWith = \parser, end -> + andThen parser \m -> + end |> map \_ -> m + +oneOf : List (Parser a) -> Parser a +oneOf = \options -> + when options is + [] -> \_ -> NoMatch + [first, .. as rest] -> + \input -> + when first input is + Match m -> Match m + NoMatch -> (oneOf rest) input + +many : Parser a -> Parser (List a) +many = \parser -> + help = \input, items -> + when parser input is + NoMatch -> Match { input: input, val: items } + Match m -> help m.input (List.append items m.val) + + \input -> help input [] + +optional = \parser -> + \input -> + when parser input is + NoMatch -> Match { input, val: {} } + Match m -> Match { input: m.input, val: {} } + +manyUntil : Parser a, Parser * -> Parser (List a) +manyUntil = \parser, end -> + help = \input, items -> + when end input is + Match state -> Match { input: state.input, val: items } + NoMatch -> + when parser input is + NoMatch -> NoMatch + Match m -> help m.input (List.append items m.val) + + \input -> help input [] + +andThen : Parser a, (a -> Parser b) -> Parser b +andThen = \parser, mapper -> + \input -> + when parser input is + NoMatch -> NoMatch + Match m -> (mapper m.val) m.input + +map : Parser a, (a -> b) -> Parser b +map = \parser, mapper -> + \in -> + when parser in is + Match { input, val } -> Match { input, val: mapper val } + NoMatch -> NoMatch + +unsafeFromUtf8 = \bytes -> + when Str.fromUtf8 bytes is + Ok s -> s + Err _ -> + crash "I was unable to convert these bytes into a string: $(Inspect.toStr bytes)" + +# Tests + +expect + result = parse "foo" + result == [Text "foo"] + +expect + result = parse "

{{name}}

" + result == [Text "

", Interpolation "name", Text "

"] + +expect + result = parse "{{foo}bar}}" + result == [Interpolation "foo}bar"] + +expect + result = parse "{{{raw val}}}" + result == [RawInterpolation "raw val"] + +expect + result = parse "{{{ foo : 10 } |> \\x -> Num.toStr x.foo}}" + result == [Interpolation "{ foo : 10 } |> \\x -> Num.toStr x.foo"] + +expect + result = parse "{{func arg1 arg2 |> func2 arg2}}" + result == [Interpolation "func arg1 arg2 |> func2 arg2"] + +expect + result = parse "{|if x > y |}foo{|endif|}" + result == [Conditional { condition: "x > y", trueBranch: [Text "foo"], falseBranch: [] }] + +expect + result = parse + """ + {|if x > y |} + foo + {|endif|} + """ + result == [Conditional { condition: "x > y", trueBranch: [Text "foo"], falseBranch: [] }] + +expect + result = parse + """ + {|if model.field |} + Hello + {|else|} + goodbye + {|endif|} + """ + result + == [ + Conditional { + condition: "model.field", + trueBranch: [Text "Hello"], + falseBranch: [Text "goodbye"], + }, + ] + +expect + result = parse + """ + {|if model.someField |} + {|if Bool.false |} + bar + {|endif|} + {|endif|} + """ + result + == [ + Conditional { + condition: "model.someField", + trueBranch: [ + Conditional { condition: "Bool.false", trueBranch: [Text "bar"], falseBranch: [] }, + ], + falseBranch: [], + }, + ] + +expect + result = parse + """ + foo + bar + {{model.baz}} + foo + """ + result == [Text "foo\nbar\n", Interpolation "model.baz", Text "\nfoo"] + +expect + result = parse + """ +

+ {|if foo |} + bar + {|endif|} +

+ """ + result + == [ + Text "

\n ", + Conditional { condition: "foo", trueBranch: [Text "bar\n "], falseBranch: [] }, + Text "

", + ] + +expect + result = parse + """ +
{|if model.username == "isaac" |}Hello{|endif|}
+ """ + result + == + [ + Text "
", + Conditional { condition: "model.username == \"isaac\"", trueBranch: [Text "Hello"], falseBranch: [] }, + Text "
", + ] + +expect + result = parse + """ + {|list user : users |} +

Hello {{user}}!

+ {|endlist|} + """ + + result + == + [ + Sequence { + item: "user", + list: "users", + body: [ + Text "

Hello ", + Interpolation "user", + Text "!

\n", + ], + }, + ] diff --git a/template/README.md b/template/README.md new file mode 100644 index 0000000..e796760 --- /dev/null +++ b/template/README.md @@ -0,0 +1,98 @@ +# roc-template +An HTML template language for Roc. + +## Example + +First write a template like `hello.rtl`: +``` +

Hello, {{model.name}}!

+ + + +{|if model.isSubscribed |} +Subscription +{|else|} +Sign up +{|endif|} +``` +Then run `compile.roc` in the directory containing `hello.rtl`. Now call the generated function: +```roc +Pages.hello { + name: "Isaac", + numbers: [1, 2, 3], + isSubscribed: Bool.true, + } +``` +and get your HTML! +```html +

Hello, Isaac!

+ + + +Subscription +``` + + +## Usage +Running `compile.roc` in a directory containg `.rtl` (Roc Template Language) templates will generate a file called `Pages.roc` which will expose a normal roc function for each `.rtl` with the same name. Each function accepts a single argument called `model` which can be any type, but will normally be a record. + +roc-template supports inserting values, conditionally including content, and expanding over lists. Interpolations, conditionals, and lists all accept arbitrary single-line Roc expressions, so there is no need to learn a new language outside of the template specific features. + +The generated file, `Pages.roc` becomes a normal part of your Roc project, so you get type checking right out of the box, for free. + +### Inserting Values + +To interpolate a value into the document, use double curly brackets: +``` +{{ model.firstName }} +``` +The value between the brackets must be a `Str`, so conversions may be necessary: +``` +{{ 2 |> Num.toStr }} +``` +HTML in the interplated string will be escaped to prevent security issues like XSS. + +### Lists +Generate a list of values by specifying a pattern for a list element, and the list to be expanded over. +``` +{|list paragraph : model.paragraphs |} +

{{ paragraph }}

+{|endlist|} +``` + +The pattern can be any normal Roc pattern, so things like this are also valid: +``` +{|list (x,y) : [(1,2),(3,4)] |} +

X: {{ x |> Num.toStr }}, Y: {{ y |> Num.toStr }}

+{|endlist|} +``` + +### Conditionals +Conditionally include content like this: +``` +{|if model.x < model.y |} +Conditional content here +{|endif|} +``` +Or with an else block: +``` +{|if model.x < model.y |} +Conditional content here +{|else|} +Other content +{|endif|} +``` + +### Raw Interpolation +If it is necessary to insert content into the document without escaping HTML, use triple brackets. +``` +{{{ model.dynamicHtml }}} +``` diff --git a/template/compile.roc b/template/compile.roc new file mode 100644 index 0000000..df62e89 --- /dev/null +++ b/template/compile.roc @@ -0,0 +1,72 @@ +app "compile" + packages { + pf: "https://github.com/roc-lang/basic-cli/releases/download/0.8.1/x8URkvfyi9I0QhmVG98roKBUs_AZRkLFwFJVJ3942YA.tar.br", + } + imports [ + pf.Stdout, + pf.Task.{ Task }, + pf.Path.{ Path }, + pf.File, + pf.Dir, + Parser, + CodeGen, + ] + provides [main] to pf + +main = + paths <- Dir.list (Path.fromStr ".") + |> Task.onErr \e -> + {} <- Stdout.line "Error listing directories: $(Inspect.toStr e)" |> Task.await + Task.err 1 + |> Task.map keepTemplates + |> Task.await + + templates <- taskAll paths \path -> + File.readUtf8 path + |> Task.map \template -> + { path, template } + |> Task.onErr \e -> + {} <- Stdout.line "There was an error reading the templates: $(Inspect.toStr e)" |> Task.await + Task.err 1 + |> Task.await + + {} <- File.writeUtf8 (Path.fromStr "Pages.roc") (compile templates) + |> Task.onErr \e -> + {} <- Stdout.line "Error writing file: $(Inspect.toStr e)" |> Task.await + Task.err 1 + |> Task.await + + Stdout.line "Generated Pages.roc" + +taskAll : List a, (a -> Task b err) -> Task (List b) err +taskAll = \items, task -> + Task.loop { vals: [], rest: items } \{ vals, rest } -> + when rest is + [] -> Done vals |> Task.ok + [item, .. as remaining] -> + Task.map (task item) \val -> + Step { vals: List.append vals val, rest: remaining } + +keepTemplates : List Path -> List Path +keepTemplates = \paths -> + List.keepIf paths \p -> + Path.display p + |> Str.endsWith extension + +compile : List { path : Path, template : Str } -> Str +compile = \templates -> + templates + |> List.map \{ path, template } -> + { name: extractFunctionName path, nodes: Parser.parse template } + |> CodeGen.generate + +extractFunctionName : Path -> Str +extractFunctionName = \path -> + display = Path.display path + when Str.split display "/" is + [.., filename] if Str.endsWith filename extension -> + Str.replaceLast filename extension "" + + _ -> crash "Error: $(display) is not a valid template path" + +extension = ".rtl" diff --git a/template/example/Pages.roc b/template/example/Pages.roc new file mode 100644 index 0000000..6c7452a --- /dev/null +++ b/template/example/Pages.roc @@ -0,0 +1,67 @@ +interface Pages + exposes [ + blogPost, + home + ] + imports [] + +blogPost = \model -> + """ + + + + + $(model.post.title |> escapeHtml) - Roc Template Example + + +
+

$(model.post.title |> escapeHtml)

+

$(model.post.text |> escapeHtml)

+ Home +
+ + + + """ + +home = \model -> + [ + """ + + + + + Roc Template Example Blog + + +
+

Posts

+ +
+ + + + """ + ] + |> Str.joinWith "" + +escapeHtml : Str -> Str +escapeHtml = \input -> + input + |> Str.replaceEach "&" "&" + |> Str.replaceEach "<" "<" + |> Str.replaceEach ">" ">" + |> Str.replaceEach "\"" """ + |> Str.replaceEach "'" "'" diff --git a/template/example/blogPost.rtl b/template/example/blogPost.rtl new file mode 100644 index 0000000..cf402df --- /dev/null +++ b/template/example/blogPost.rtl @@ -0,0 +1,14 @@ + + + + + {{ model.post.title }} - Roc Template Example + + +
+

{{ model.post.title }}

+

{{ model.post.text }}

+ Home +
+ + diff --git a/template/example/home.rtl b/template/example/home.rtl new file mode 100644 index 0000000..e871648 --- /dev/null +++ b/template/example/home.rtl @@ -0,0 +1,19 @@ + + + + + Roc Template Example Blog + + +
+

Posts

+ +
+ + diff --git a/template/example/server.roc b/template/example/server.roc new file mode 100644 index 0000000..27bcacc --- /dev/null +++ b/template/example/server.roc @@ -0,0 +1,60 @@ +app "server" + packages { pf: "https://github.com/roc-lang/basic-webserver/releases/download/0.3.0/gJOTXTeR3CD4zCbRqK7olo4edxQvW5u3xGL-8SSxDcY.tar.br" } + imports [ + pf.Task.{ Task }, + pf.Http.{ Request, Response }, + Pages, + ] + provides [main] to pf + +main = \req -> + when Str.split req.url "/" |> List.dropFirst 1 is + ["posts", slug] -> + maybePost = List.findFirst posts \post -> + post.slug == slug + when maybePost is + Err _ -> notFound + Ok post -> + Pages.blogPost { + post, + } + |> success + + [""] -> + Pages.home { + posts, + } + |> success + + _ -> notFound + +notFound = Task.ok { + status: 404, + headers: [], + body: [], +} + +success = \body -> + Task.ok { + status: 200, + headers: [Http.header "Content-Type" "text/html"], + body: body |> Str.toUtf8, + } + +posts = [ + { + title: "How to write a template engine in Roc", + slug: "template-engine", + text: "Roc's type inference shines through here. It makes it easy to write a template language with compile time errors while having the same feel as dynamic languages.", + }, + { + title: "My story: thinking of blog ideas for this example", + slug: "my-story", + text: "Just one more after this one...", + }, + { + title: "The last blog post", + slug: "fin", + text: "Three seems like enough for this example", + }, +] diff --git a/template/index.md b/template/index.md index 7e2a579..58a3bf6 100644 --- a/template/index.md +++ b/template/index.md @@ -1 +1,35 @@ -# Template Engine +# Template + +## Design +- When the user runs `compile.roc`, we search for templates ending in `.rtl` (Roc Template Language) in the current directory and load them. +- We then parse each template into a list of nodes, each node being unstructured text, a conditional, an HTML-escaped interpolation, a raw interpolation, or a list. Parsing never fails; if the user makes a syntax error while trying to use one of the template languages features, they will get out plain text instead. +- We then take the parsed templates and generate a file called `Pages.roc` that contains a function corresponding to each template file. +- Each function accepts a single argument called `model`. Normally it is a record, and fields on it are accessed in the template like this `Hello, {{model.name}}!`, although it could be another type. + +### Type Inference +- One of my goals with this template language is to get compile time errors. I want to emphasize in the chapter how great it is that Roc has principal decidable type inference, and that it is completely necessary for us to get compile time errors and nice editor support with this kind of approach. +- This approach could be used easily in dynamic languages like JS or Python, but then the compile time errors are lost. If we wanted to use the same approach in a language like Java, with compile time errors, we would have to do real type inference on the template to determine the types in order to include them in the function for the template. + +### Parsing +- I wrote my own parser combinators for this to avoid pulling in another dependency. They do not include errors right now because the whole parsing step never fails. Because at least one other chapter will need similar parsing capabilities, I think it would be good to develop a parser in one chapter and use it in the others. +- Right now the generated HTML will contain some whitespace weirdness due to the presence of the extra syntax in the templates. This does not impact the way the HTML is rendered (unless using `pre`), but it would be nice to have it fixed eventually. I haven't thought about it a ton, but it might be a bit challenging to handle properly in all cases, and I think it would probably be a distraction from the point of the chapter. + +### When-Is +I would like to include a syntax for when expressions also: +``` +{|when x |} +{|is Err NotFound |} + Error +{|is Ok val |} + {{ val }} +{|endwhen|} +``` +I have not implemented this yet, but I think it should be included if there is enough space. It is probably a fairly uncommmon feature and is necessary for using ADTs nicely in templates. + +## Example +To try the example, run `roc ../compile.roc && roc server.roc` in `/example`. + +## Other options considered +- Originally, I wanted the functions to take a destructured record (`page = \{name, email} ->`) so that fields could be accessed directly in the template without having to prefix them with `model.`. To do this we would have to identify each field being used in the template. This should be doable, but I don't think it is worth increasing the scope of the chapter to do it. +- I have a couple of usages of `crash` in the code right now. These could be removed, but I am a bit torn because I don't like presenting errors that won't actually happen. +- We could have a `.roc` file for each template and then pull them all into a single module which the user imports. This would avoid name conflicts and extra long files, but I don't think it is necessary or worth the increased scope. From 0a8018c2bc19f2a9538f5613345c433de8e443b3 Mon Sep 17 00:00:00 2001 From: Isaac Van Doren <69181572+isaacvando@users.noreply.github.com> Date: Sat, 30 Mar 2024 14:03:09 -0500 Subject: [PATCH 3/3] clean ups --- template/.gitignore | 5 ----- template/LICENSE.md | 17 ----------------- 2 files changed, 22 deletions(-) delete mode 100644 template/.gitignore delete mode 100644 template/LICENSE.md diff --git a/template/.gitignore b/template/.gitignore deleted file mode 100644 index 77a422c..0000000 --- a/template/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -server -engine -output -compile -output.roc diff --git a/template/LICENSE.md b/template/LICENSE.md deleted file mode 100644 index 5493086..0000000 --- a/template/LICENSE.md +++ /dev/null @@ -1,17 +0,0 @@ -Copyright (c) 2024 Isaac Van Doren and contributors - -The Universal Permissive License (UPL), Version 1.0 - -Subject to the condition set forth below, permission is hereby granted to any person obtaining a copy of this software, associated documentation and/or data (collectively the “Software”), free of charge and under any and all copyright rights in the Software, and any and all patent rights owned or freely licensable by each licensor hereunder covering either (i) the unmodified Software as contributed to or provided by such licensor, or (ii) the Larger Works (as defined below), to deal in both - -(a) the Software, and - -(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is included with the Software (each a “Larger Work” to which the Software is contributed by such licensors), - -without restriction, including without limitation the rights to copy, create derivative works of, display, perform, and distribute the Software and make, use, sell, offer for sale, import, export, have made, and have sold the Software and the Larger Work(s), and to sublicense the foregoing rights on either these or other terms. - -This license is subject to the following condition: - -The above copyright notice and either this complete permission notice or at a minimum a reference to the UPL must be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file