Merge pull request #3920 from roc-lang/static-site-gen

Static site generator example
This commit is contained in:
Brian Carroll 2022-09-01 07:12:12 +01:00 committed by GitHub
commit bfc00a4b94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1037 additions and 97 deletions

View File

@ -20,7 +20,7 @@ mod cli_run {
use roc_test_utils::assert_multiline_str_eq;
use serial_test::serial;
use std::iter;
use std::path::{Path, PathBuf};
use std::path::Path;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
@ -66,7 +66,7 @@ mod cli_run {
filename: &'a str,
executable_filename: &'a str,
stdin: &'a [&'a str],
input_file: Option<&'a str>,
input_paths: &'a [&'a str],
expected_ending: &'a str,
use_valgrind: bool,
}
@ -93,24 +93,16 @@ mod cli_run {
file: &'a Path,
args: I,
stdin: &[&str],
opt_input_file: Option<PathBuf>,
app_args: &[String],
) -> Out {
let compile_out = if let Some(input_file) = opt_input_file {
run_roc(
// converting these all to String avoids lifetime issues
args.into_iter().map(|arg| arg.to_string()).chain([
file.to_str().unwrap().to_string(),
"--".to_string(),
input_file.to_str().unwrap().to_string(),
]),
stdin,
)
} else {
run_roc(
args.into_iter().chain(iter::once(file.to_str().unwrap())),
stdin,
)
};
let compile_out = run_roc(
// converting these all to String avoids lifetime issues
args.into_iter()
.map(|arg| arg.to_string())
.chain([file.to_str().unwrap().to_string(), "--".to_string()])
.chain(app_args.iter().cloned()),
stdin,
);
// If there is any stderr, it should be reporting the runtime and that's it!
if !(compile_out.stderr.is_empty()
@ -132,7 +124,7 @@ mod cli_run {
stdin: &[&str],
executable_filename: &str,
flags: &[&str],
opt_input_file: Option<PathBuf>,
app_args: &[String],
expected_ending: &str,
use_valgrind: bool,
) {
@ -154,24 +146,17 @@ mod cli_run {
let out = match cli_mode {
CliMode::RocBuild => {
run_roc_on(file, iter::once(CMD_BUILD).chain(flags.clone()), &[], None);
run_roc_on(file, iter::once(CMD_BUILD).chain(flags.clone()), &[], &[]);
if use_valgrind && ALLOW_VALGRIND {
let (valgrind_out, raw_xml) = if let Some(ref input_file) = opt_input_file {
run_with_valgrind(
stdin.iter().copied(),
&[
file.with_file_name(executable_filename).to_str().unwrap(),
input_file.clone().to_str().unwrap(),
],
)
} else {
run_with_valgrind(
stdin.iter().copied(),
&[file.with_file_name(executable_filename).to_str().unwrap()],
)
};
let mut valgrind_args = vec![file
.with_file_name(executable_filename)
.to_str()
.unwrap()
.to_string()];
valgrind_args.extend(app_args.iter().cloned());
let (valgrind_out, raw_xml) =
run_with_valgrind(stdin.iter().copied(), &valgrind_args);
if valgrind_out.status.success() {
let memory_errors = extract_valgrind_errors(&raw_xml).unwrap_or_else(|err| {
panic!("failed to parse the `valgrind` xml output. Error was:\n\n{:?}\n\nvalgrind xml was: \"{}\"\n\nvalgrind stdout was: \"{}\"\n\nvalgrind stderr was: \"{}\"", err, raw_xml, valgrind_out.stdout, valgrind_out.stderr);
@ -207,26 +192,20 @@ mod cli_run {
}
valgrind_out
} else if let Some(ref input_file) = opt_input_file {
run_cmd(
file.with_file_name(executable_filename).to_str().unwrap(),
stdin.iter().copied(),
&[input_file.to_str().unwrap()],
)
} else {
run_cmd(
file.with_file_name(executable_filename).to_str().unwrap(),
stdin.iter().copied(),
&[],
app_args,
)
}
}
CliMode::Roc => run_roc_on(file, flags.clone(), stdin, opt_input_file.clone()),
CliMode::Roc => run_roc_on(file, flags.clone(), stdin, app_args),
CliMode::RocRun => run_roc_on(
file,
iter::once(CMD_RUN).chain(flags.clone()),
stdin,
opt_input_file.clone(),
app_args,
),
};
@ -247,10 +226,10 @@ mod cli_run {
stdin: &[&str],
executable_filename: &str,
flags: &[&str],
input_file: Option<PathBuf>,
input_paths: &[&str],
expected_ending: &str,
) {
assert_eq!(input_file, None, "Wasm does not support input files");
assert!(input_paths.is_empty(), "Wasm does not support input files");
let mut flags = flags.to_vec();
flags.push(concatcp!(TARGET_FLAG, "=wasm32"));
@ -296,12 +275,17 @@ mod cli_run {
let example = $example;
let file_name = example_file(dir_name, example.filename);
let mut app_args: Vec<String> = vec![];
for file in example.input_paths {
app_args.push(example_file(dir_name, file).to_str().unwrap().to_string());
}
match example.executable_filename {
"form" | "hello-gui" | "breakout" | "ruby" => {
// Since these require things the build system often doesn't have
// (e.g. GUIs open a window, Ruby needs ruby installed, WASM needs a browser)
// we do `roc build` on them but don't run them.
run_roc_on(&file_name, [CMD_BUILD, OPTIMIZE_FLAG], &[], None);
run_roc_on(&file_name, [CMD_BUILD, OPTIMIZE_FLAG], &[], &[]);
return;
}
"rocLovesSwift" => {
@ -324,7 +308,7 @@ mod cli_run {
example.stdin,
example.executable_filename,
&[],
example.input_file.and_then(|file| Some(example_file(dir_name, file))),
&app_args,
example.expected_ending,
example.use_valgrind,
);
@ -337,7 +321,7 @@ mod cli_run {
example.stdin,
example.executable_filename,
&[OPTIMIZE_FLAG],
example.input_file.and_then(|file| Some(example_file(dir_name, file))),
&app_args,
example.expected_ending,
example.use_valgrind,
);
@ -350,7 +334,7 @@ mod cli_run {
example.stdin,
example.executable_filename,
&[LINKER_FLAG, "legacy"],
example.input_file.and_then(|file| Some(example_file(dir_name, file))),
&app_args,
example.expected_ending,
example.use_valgrind,
);
@ -387,7 +371,7 @@ mod cli_run {
filename: "main.roc",
executable_filename: "helloWorld",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending:"Hello, World!\n",
use_valgrind: true,
},
@ -395,7 +379,7 @@ mod cli_run {
filename: "main.roc",
executable_filename: "rocLovesPlatforms",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending:"Which platform am I running on now?\n",
use_valgrind: true,
},
@ -406,7 +390,7 @@ mod cli_run {
// filename: "rocLovesC.roc",
// executable_filename: "rocLovesC",
// stdin: &[],
// input_file: None,
// input_paths: &[],
// expected_ending:"Roc <3 C!\n",
// use_valgrind: true,
// },
@ -414,7 +398,7 @@ mod cli_run {
filename: "rocLovesRust.roc",
executable_filename: "rocLovesRust",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending:"Roc <3 Rust!\n",
use_valgrind: true,
},
@ -422,7 +406,7 @@ mod cli_run {
filename: "rocLovesSwift.roc",
executable_filename: "rocLovesSwift",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending:"Roc <3 Swift!\n",
use_valgrind: true,
},
@ -430,7 +414,7 @@ mod cli_run {
filename: "rocLovesWebAssembly.roc",
executable_filename: "rocLovesWebAssembly",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending:"Roc <3 Web Assembly!\n",
use_valgrind: true,
},
@ -438,7 +422,7 @@ mod cli_run {
filename: "rocLovesZig.roc",
executable_filename: "rocLovesZig",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending:"Roc <3 Zig!\n",
use_valgrind: true,
},
@ -446,7 +430,7 @@ mod cli_run {
filename: "main.roc",
executable_filename: "libhello",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending:"",
use_valgrind: true,
},
@ -454,7 +438,7 @@ mod cli_run {
filename: "fibonacci.roc",
executable_filename: "fibonacci",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending:"55\n",
use_valgrind: true,
},
@ -462,7 +446,7 @@ mod cli_run {
filename: "Hello.roc",
executable_filename: "hello-gui",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending: "",
use_valgrind: false,
},
@ -470,7 +454,7 @@ mod cli_run {
filename: "breakout.roc",
executable_filename: "breakout",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending: "",
use_valgrind: false,
},
@ -478,7 +462,7 @@ mod cli_run {
filename: "quicksort.roc",
executable_filename: "quicksort",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending: "[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2]\n",
use_valgrind: true,
},
@ -486,7 +470,7 @@ mod cli_run {
// filename: "Quicksort.roc",
// executable_filename: "quicksort",
// stdin: &[],
// input_file: None,
// input_paths: &[],
// expected_ending: "[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2]\n",
// use_valgrind: true,
// },
@ -494,7 +478,7 @@ mod cli_run {
filename: "effects.roc",
executable_filename: "effects",
stdin: &["hi there!"],
input_file: None,
input_paths: &[],
expected_ending: "hi there!\nIt is known\n",
use_valgrind: true,
},
@ -502,7 +486,7 @@ mod cli_run {
// filename: "Main.roc",
// executable_filename: "tea-example",
// stdin: &[],
// input_file: None,
// input_paths: &[],
// expected_ending: "",
// use_valgrind: true,
// },
@ -510,7 +494,7 @@ mod cli_run {
filename: "form.roc",
executable_filename: "form",
stdin: &["Giovanni\n", "Giorgio\n"],
input_file: None,
input_paths: &[],
expected_ending: "Hi, Giovanni Giorgio! 👋\n",
use_valgrind: false,
},
@ -518,7 +502,7 @@ mod cli_run {
filename: "tui.roc",
executable_filename: "tui",
stdin: &["foo\n"], // NOTE: adding more lines leads to memory leaks
input_file: None,
input_paths: &[],
expected_ending: "Hello Worldfoo!\n",
use_valgrind: true,
},
@ -526,7 +510,7 @@ mod cli_run {
// filename: "Main.roc",
// executable_filename: "custom-malloc-example",
// stdin: &[],
// input_file: None,
// input_paths: &[],
// expected_ending: "ms!\nThe list was small!\n",
// use_valgrind: true,
// },
@ -534,7 +518,7 @@ mod cli_run {
// filename: "Main.roc",
// executable_filename: "task-example",
// stdin: &[],
// input_file: None,
// input_paths: &[],
// expected_ending: "successfully wrote to file\n",
// use_valgrind: true,
// },
@ -543,11 +527,21 @@ mod cli_run {
filename: "False.roc",
executable_filename: "false",
stdin: &[],
input_file: Some("examples/hello.false"),
input_paths: &["examples/hello.false"],
expected_ending:"Hello, World!\n",
use_valgrind: false,
}
},
static_site_gen: "static-site-gen" => {
Example {
filename: "static-site.roc",
executable_filename: "static-site",
stdin: &[],
input_paths: &["input", "output"],
expected_ending: "hello.txt -> hello.html\n",
use_valgrind: false,
}
},
}
macro_rules! benchmarks {
@ -572,6 +566,11 @@ mod cli_run {
let mut ran_without_optimizations = false;
let mut app_args: Vec<String> = vec![];
for file in benchmark.input_paths {
app_args.push(examples_dir("benchmarks").join(file).to_str().unwrap().to_string());
}
BENCHMARKS_BUILD_PLATFORM.call_once( || {
// Check with and without optimizations
check_output_with_stdin(
@ -579,7 +578,7 @@ mod cli_run {
benchmark.stdin,
benchmark.executable_filename,
&[],
benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))),
&app_args,
benchmark.expected_ending,
benchmark.use_valgrind,
);
@ -597,7 +596,7 @@ mod cli_run {
benchmark.stdin,
benchmark.executable_filename,
&[PRECOMPILED_HOST],
benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))),
&app_args,
benchmark.expected_ending,
benchmark.use_valgrind,
);
@ -608,7 +607,7 @@ mod cli_run {
benchmark.stdin,
benchmark.executable_filename,
&[PRECOMPILED_HOST, OPTIMIZE_FLAG],
benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))),
&app_args,
benchmark.expected_ending,
benchmark.use_valgrind,
);
@ -641,7 +640,7 @@ mod cli_run {
benchmark.stdin,
benchmark.executable_filename,
&[],
benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))),
benchmark.input_paths.iter().map(|file| examples_dir("benchmarks").join(file)),
benchmark.expected_ending,
);
@ -650,7 +649,7 @@ mod cli_run {
benchmark.stdin,
benchmark.executable_filename,
&[OPTIMIZE_FLAG],
benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))),
benchmark.input_paths.iter().map(|file| examples_dir("benchmarks").join(file)),
benchmark.expected_ending,
);
}
@ -682,7 +681,7 @@ mod cli_run {
benchmark.stdin,
benchmark.executable_filename,
[concatcp!(TARGET_FLAG, "=x86_32")],
benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))),
benchmark.input_paths.iter().map(|file| Some(examples_dir("benchmarks").join(file))),
benchmark.expected_ending,
benchmark.use_valgrind,
);
@ -692,7 +691,7 @@ mod cli_run {
benchmark.stdin,
benchmark.executable_filename,
[concatcp!(TARGET_FLAG, "=x86_32"), OPTIMIZE_FLAG],
benchmark.input_file.and_then(|file| Some(examples_dir("benchmarks").join(file))),
benchmark.input_paths.iter().map(|file| Some(examples_dir("benchmarks").join(file))),
benchmark.expected_ending,
benchmark.use_valgrind,
);
@ -721,7 +720,7 @@ mod cli_run {
filename: "NQueens.roc",
executable_filename: "nqueens",
stdin: &["6"],
input_file: None,
input_paths: &[],
expected_ending: "4\n",
use_valgrind: true,
},
@ -729,7 +728,7 @@ mod cli_run {
filename: "CFold.roc",
executable_filename: "cfold",
stdin: &["3"],
input_file: None,
input_paths: &[],
expected_ending: "11 & 11\n",
use_valgrind: true,
},
@ -737,7 +736,7 @@ mod cli_run {
filename: "Deriv.roc",
executable_filename: "deriv",
stdin: &["2"],
input_file: None,
input_paths: &[],
expected_ending: "1 count: 6\n2 count: 22\n",
use_valgrind: true,
},
@ -745,7 +744,7 @@ mod cli_run {
filename: "RBTreeCk.roc",
executable_filename: "rbtree-ck",
stdin: &["100"],
input_file: None,
input_paths: &[],
expected_ending: "10\n",
use_valgrind: true,
},
@ -753,7 +752,7 @@ mod cli_run {
filename: "RBTreeInsert.roc",
executable_filename: "rbtree-insert",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending: "Node Black 0 {} Empty Empty\n",
use_valgrind: true,
},
@ -761,7 +760,7 @@ mod cli_run {
// filename: "RBTreeDel.roc",
// executable_filename: "rbtree-del",
// stdin: &["420"],
// input_file: None,
// input_paths: &[],
// expected_ending: "30\n",
// use_valgrind: true,
// },
@ -769,7 +768,7 @@ mod cli_run {
filename: "TestAStar.roc",
executable_filename: "test-astar",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending: "True\n",
use_valgrind: false,
},
@ -777,7 +776,7 @@ mod cli_run {
filename: "TestBase64.roc",
executable_filename: "test-base64",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending: "encoded: SGVsbG8gV29ybGQ=\ndecoded: Hello World\n",
use_valgrind: true,
},
@ -785,7 +784,7 @@ mod cli_run {
filename: "Closure.roc",
executable_filename: "closure",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending: "",
use_valgrind: false,
},
@ -793,7 +792,7 @@ mod cli_run {
filename: "Issue2279.roc",
executable_filename: "issue2279",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending: "Hello, world!\n",
use_valgrind: true,
},
@ -801,7 +800,7 @@ mod cli_run {
filename: "QuicksortApp.roc",
executable_filename: "quicksortapp",
stdin: &[],
input_file: None,
input_paths: &[],
expected_ending: "todo put the correct quicksort answer here",
use_valgrind: true,
},
@ -900,7 +899,7 @@ mod cli_run {
&[],
"multi-dep-str",
&[],
None,
&[],
"I am Dep2.str2\n",
true,
);
@ -914,7 +913,7 @@ mod cli_run {
&[],
"multi-dep-str",
&[OPTIMIZE_FLAG],
None,
&[],
"I am Dep2.str2\n",
true,
);
@ -928,7 +927,7 @@ mod cli_run {
&[],
"multi-dep-thunk",
&[],
None,
&[],
"I am Dep2.value2\n",
true,
);
@ -942,7 +941,7 @@ mod cli_run {
&[],
"multi-dep-thunk",
&[OPTIMIZE_FLAG],
None,
&[],
"I am Dep2.value2\n",
true,
);

View File

@ -164,7 +164,7 @@ where
pub fn run_cmd<'a, I: IntoIterator<Item = &'a str>>(
cmd_name: &str,
stdin_vals: I,
args: &[&str],
args: &[String],
) -> Out {
let mut cmd = Command::new(cmd_name);
@ -202,7 +202,7 @@ pub fn run_cmd<'a, I: IntoIterator<Item = &'a str>>(
pub fn run_with_valgrind<'a, I: IntoIterator<Item = &'a str>>(
stdin_vals: I,
args: &[&str],
args: &[String],
) -> (Out, String) {
//TODO: figure out if there is a better way to get the valgrind executable.
let mut cmd = Command::new("valgrind");

View File

@ -3726,6 +3726,10 @@ fn expose_function_to_host_help_c_abi_v2<'a, 'ctx, 'env>(
// Drop the return pointer the other way, if the C function returns by pointer but Roc
// doesn't
(RocReturn::Return, CCReturn::ByPointer) => (&params[1..], &param_types[..]),
(RocReturn::ByPointer, CCReturn::ByPointer) => {
// Both return by pointer but Roc puts it at the end and C puts it at the beginning
(&params[1..], &param_types[..param_types.len() - 1])
}
_ => (&params[..], &param_types[..]),
};
@ -3807,8 +3811,20 @@ fn expose_function_to_host_help_c_abi_v2<'a, 'ctx, 'env>(
},
CCReturn::ByPointer => {
let out_ptr = c_function.get_nth_param(0).unwrap().into_pointer_value();
env.builder.build_store(out_ptr, value);
match roc_return {
RocReturn::Return => {
env.builder.build_store(out_ptr, value);
}
RocReturn::ByPointer => {
// TODO: ideally, in this case, we should pass the C return pointer directly
// into the call_roc_function rather than forcing an extra alloca, load, and
// store!
let value = env
.builder
.build_load(value.into_pointer_value(), "load_roc_result");
env.builder.build_store(out_ptr, value);
}
}
env.builder.build_return(None);
}
CCReturn::Void => {

2
examples/static-site-gen/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/output
/static-site

View File

@ -0,0 +1,14 @@
# Static site generator
This is an example of how you might build a static site generator using Roc.
It searches for files in the `input` directory, transforms the contents to HTML
using a Roc function, and writes the result into the corresponding file path in
the `output` directory.
To run, `cd` into this directory and run this in your terminal:
```bash
roc run app.roc input/ output/
```
Eventually this example could be expanded to support Markdown parsing, database connections at build time, etc.

View File

@ -0,0 +1,3 @@
Hello, World!
I am a plain text content file.
I make a very interesting web page, I'm sure you'll agree.

View File

@ -0,0 +1,37 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "arrayvec"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]]
name = "host"
version = "0.0.1"
dependencies = [
"libc",
"roc_std",
]
[[package]]
name = "libc"
version = "0.2.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
[[package]]
name = "roc_std"
version = "0.0.1"
dependencies = [
"arrayvec",
"static_assertions",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"

View File

@ -0,0 +1,23 @@
[package]
name = "host"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2021"
links = "app"
[lib]
name = "host"
path = "src/lib.rs"
crate-type = ["staticlib", "rlib"]
[[bin]]
name = "host"
path = "src/main.rs"
[dependencies]
roc_std = { path = "../../../crates/roc_std" }
libc = "0.2"
[workspace]

View File

@ -0,0 +1,353 @@
interface Html
exposes [
Node,
Attribute,
render,
renderWithoutDocType,
element,
text,
attribute,
html,
base,
head,
link,
meta,
style,
title,
body,
address,
article,
aside,
footer,
header,
h1,
h2,
h3,
h4,
h5,
h6,
main,
nav,
section,
blockquote,
dd,
div,
dl,
dt,
figcaption,
figure,
hr,
li,
menu,
ol,
p,
pre,
ul,
a,
abbr,
b,
bdi,
bdo,
br,
cite,
code,
data,
dfn,
em,
i,
kbd,
mark,
q,
rp,
rt,
ruby,
s,
samp,
small,
span,
strong,
sub,
sup,
time,
u,
var,
wbr,
area,
audio,
img,
map,
track,
video,
embed,
iframe,
object,
picture,
portal,
source,
svg,
math,
canvas,
noscript,
script,
del,
ins,
caption,
col,
colgroup,
table,
tbody,
td,
tfoot,
th,
thead,
tr,
button,
datalist,
fieldset,
form,
input,
label,
legend,
meter,
optgroup,
option,
output,
progress,
select,
textarea,
details,
dialog,
summary,
slot,
template,
]
imports [Html.Attributes]
Node : [Text Str, Element Str Nat (List Attribute) (List Node)]
Attribute : Html.Attributes.Attribute
attribute : Str -> (Str -> Attribute)
attribute = Html.Attributes.attribute
text : Str -> Node
text = Text
## Define a non-standard HTML Element
##
## You can use this to add elements that are not already supported.
## For example, you could bring back the obsolete <blink> element,
## and add some 90's nostalgia to your web page!
##
## blink : List Attribute, List Node -> Node
## blink = element "blink"
##
## html = blink [] [ text "This text is blinking!" ]
##
element : Str -> (List Attribute, List Node -> Node)
element = \tagName ->
\attrs, children ->
# While building the node tree, calculate the size of Str it will render to
withTag = 2 * (3 + Str.countUtf8Bytes tagName)
withAttrs = List.walk attrs withTag \acc, Attribute name val ->
acc + Str.countUtf8Bytes name + Str.countUtf8Bytes val + 4
totalSize = List.walk children withAttrs \acc, child ->
acc + nodeSize child
Element tagName totalSize attrs children
# internal helper
nodeSize : Node -> Nat
nodeSize = \node ->
when node is
Text content ->
Str.countUtf8Bytes content
Element _ size _ _ ->
size
## Render a Node to an HTML string
##
## The output has no whitespace between nodes, to make it small.
## This is intended for generating full HTML documents, so it
## automatically adds `<!DOCTYPE html>` to the start of the string.
## See also `renderWithoutDocType`.
render : Node -> Str
render = \node ->
buffer = Str.reserve "<!DOCTYPE html>" (nodeSize node)
renderHelp buffer node
## Render a Node to a string, without a DOCTYPE tag
renderWithoutDocType : Node -> Str
renderWithoutDocType = \node ->
buffer = Str.reserve "" (nodeSize node)
renderHelp buffer node
# internal helper
renderHelp : Str, Node -> Str
renderHelp = \buffer, node ->
when node is
Text content ->
Str.concat buffer content
Element tagName _ attrs children ->
withTagName = "\(buffer)<\(tagName)"
withAttrs =
if List.isEmpty attrs then
withTagName
else
List.walk attrs "\(withTagName) " renderAttr
withTag = Str.concat withAttrs ">"
withChildren = List.walk children withTag renderHelp
"\(withChildren)</\(tagName)>"
# internal helper
renderAttr : Str, Attribute -> Str
renderAttr = \buffer, Attribute key val ->
"\(buffer) \(key)=\"\(val)\""
# Main root
html = element "html"
# Document metadata
base = element "base"
head = element "head"
link = element "link"
meta = element "meta"
style = element "style"
title = element "title"
# Sectioning root
body = element "body"
# Content sectioning
address = element "address"
article = element "article"
aside = element "aside"
footer = element "footer"
header = element "header"
h1 = element "h1"
h2 = element "h2"
h3 = element "h3"
h4 = element "h4"
h5 = element "h5"
h6 = element "h6"
main = element "main"
nav = element "nav"
section = element "section"
# Text content
blockquote = element "blockquote"
dd = element "dd"
div = element "div"
dl = element "dl"
dt = element "dt"
figcaption = element "figcaption"
figure = element "figure"
hr = element "hr"
li = element "li"
menu = element "menu"
ol = element "ol"
p = element "p"
pre = element "pre"
ul = element "ul"
# Inline text semantics
a = element "a"
abbr = element "abbr"
b = element "b"
bdi = element "bdi"
bdo = element "bdo"
br = element "br"
cite = element "cite"
code = element "code"
data = element "data"
dfn = element "dfn"
em = element "em"
i = element "i"
kbd = element "kbd"
mark = element "mark"
q = element "q"
rp = element "rp"
rt = element "rt"
ruby = element "ruby"
s = element "s"
samp = element "samp"
small = element "small"
span = element "span"
strong = element "strong"
sub = element "sub"
sup = element "sup"
time = element "time"
u = element "u"
var = element "var"
wbr = element "wbr"
# Image and multimedia
area = element "area"
audio = element "audio"
img = element "img"
map = element "map"
track = element "track"
video = element "video"
# Embedded content
embed = element "embed"
iframe = element "iframe"
object = element "object"
picture = element "picture"
portal = element "portal"
source = element "source"
# SVG and MathML
svg = element "svg"
math = element "math"
# Scripting
canvas = element "canvas"
noscript = element "noscript"
script = element "script"
# Demarcating edits
del = element "del"
ins = element "ins"
# Table content
caption = element "caption"
col = element "col"
colgroup = element "colgroup"
table = element "table"
tbody = element "tbody"
td = element "td"
tfoot = element "tfoot"
th = element "th"
thead = element "thead"
tr = element "tr"
# Forms
button = element "button"
datalist = element "datalist"
fieldset = element "fieldset"
form = element "form"
input = element "input"
label = element "label"
legend = element "legend"
meter = element "meter"
optgroup = element "optgroup"
option = element "option"
output = element "output"
progress = element "progress"
select = element "select"
textarea = element "textarea"
# Interactive elements
details = element "details"
dialog = element "dialog"
summary = element "summary"
# Web Components
slot = element "slot"
template = element "template"

View File

@ -0,0 +1,277 @@
interface Html.Attributes
exposes [
Attribute,
attribute,
accept,
acceptCharset,
accesskey,
action,
align,
allow,
alt,
async,
autocapitalize,
autocomplete,
autofocus,
autoplay,
background,
bgcolor,
border,
buffered,
capture,
challenge,
charset,
checked,
cite,
class,
code,
codebase,
color,
cols,
colspan,
content,
contenteditable,
contextmenu,
controls,
coords,
crossorigin,
csp,
data,
dataAttr,
datetime,
decoding,
default,
defer,
dir,
dirname,
disabled,
download,
draggable,
enctype,
enterkeyhint,
for,
form,
formaction,
formenctype,
formmethod,
formnovalidate,
formtarget,
headers,
height,
hidden,
high,
href,
hreflang,
httpEquiv,
icon,
id,
importance,
integrity,
intrinsicsize,
inputmode,
ismap,
itemprop,
keytype,
kind,
label,
lang,
language,
loading,
list,
loop,
low,
manifest,
max,
maxlength,
minlength,
media,
method,
min,
multiple,
muted,
name,
novalidate,
open,
optimum,
pattern,
ping,
placeholder,
poster,
preload,
radiogroup,
readonly,
referrerpolicy,
rel,
required,
reversed,
role,
rows,
rowspan,
sandbox,
scope,
scoped,
selected,
shape,
size,
sizes,
slot,
span,
spellcheck,
src,
srcdoc,
srclang,
srcset,
start,
step,
style,
summary,
tabindex,
target,
title,
translate,
type,
usemap,
value,
width,
wrap,
]
imports []
Attribute : [Attribute Str Str]
attribute : Str -> (Str -> Attribute)
attribute = \attrName ->
\attrValue -> Attribute attrName attrValue
accept = attribute "accept"
acceptCharset = attribute "accept-charset"
accesskey = attribute "accesskey"
action = attribute "action"
align = attribute "align"
allow = attribute "allow"
alt = attribute "alt"
async = attribute "async"
autocapitalize = attribute "autocapitalize"
autocomplete = attribute "autocomplete"
autofocus = attribute "autofocus"
autoplay = attribute "autoplay"
background = attribute "background"
bgcolor = attribute "bgcolor"
border = attribute "border"
buffered = attribute "buffered"
capture = attribute "capture"
challenge = attribute "challenge"
charset = attribute "charset"
checked = attribute "checked"
cite = attribute "cite"
class = attribute "class"
code = attribute "code"
codebase = attribute "codebase"
color = attribute "color"
cols = attribute "cols"
colspan = attribute "colspan"
content = attribute "content"
contenteditable = attribute "contenteditable"
contextmenu = attribute "contextmenu"
controls = attribute "controls"
coords = attribute "coords"
crossorigin = attribute "crossorigin"
csp = attribute "csp"
data = attribute "data"
dataAttr = \dataName, dataVal -> Attribute "data-\(dataName)" dataVal
datetime = attribute "datetime"
decoding = attribute "decoding"
default = attribute "default"
defer = attribute "defer"
dir = attribute "dir"
dirname = attribute "dirname"
disabled = attribute "disabled"
download = attribute "download"
draggable = attribute "draggable"
enctype = attribute "enctype"
enterkeyhint = attribute "enterkeyhint"
for = attribute "for"
form = attribute "form"
formaction = attribute "formaction"
formenctype = attribute "formenctype"
formmethod = attribute "formmethod"
formnovalidate = attribute "formnovalidate"
formtarget = attribute "formtarget"
headers = attribute "headers"
height = attribute "height"
hidden = attribute "hidden"
high = attribute "high"
href = attribute "href"
hreflang = attribute "hreflang"
httpEquiv = attribute "http-equiv"
icon = attribute "icon"
id = attribute "id"
importance = attribute "importance"
integrity = attribute "integrity"
intrinsicsize = attribute "intrinsicsize"
inputmode = attribute "inputmode"
ismap = attribute "ismap"
itemprop = attribute "itemprop"
keytype = attribute "keytype"
kind = attribute "kind"
label = attribute "label"
lang = attribute "lang"
language = attribute "language"
loading = attribute "loading"
list = attribute "list"
loop = attribute "loop"
low = attribute "low"
manifest = attribute "manifest"
max = attribute "max"
maxlength = attribute "maxlength"
minlength = attribute "minlength"
media = attribute "media"
method = attribute "method"
min = attribute "min"
multiple = attribute "multiple"
muted = attribute "muted"
name = attribute "name"
novalidate = attribute "novalidate"
open = attribute "open"
optimum = attribute "optimum"
pattern = attribute "pattern"
ping = attribute "ping"
placeholder = attribute "placeholder"
poster = attribute "poster"
preload = attribute "preload"
radiogroup = attribute "radiogroup"
readonly = attribute "readonly"
referrerpolicy = attribute "referrerpolicy"
rel = attribute "rel"
required = attribute "required"
reversed = attribute "reversed"
role = attribute "role"
rows = attribute "rows"
rowspan = attribute "rowspan"
sandbox = attribute "sandbox"
scope = attribute "scope"
scoped = attribute "scoped"
selected = attribute "selected"
shape = attribute "shape"
size = attribute "size"
sizes = attribute "sizes"
slot = attribute "slot"
span = attribute "span"
spellcheck = attribute "spellcheck"
src = attribute "src"
srcdoc = attribute "srcdoc"
srclang = attribute "srclang"
srcset = attribute "srcset"
start = attribute "start"
step = attribute "step"
style = attribute "style"
summary = attribute "summary"
tabindex = attribute "tabindex"
target = attribute "target"
title = attribute "title"
translate = attribute "translate"
type = attribute "type"
usemap = attribute "usemap"
value = attribute "value"
width = attribute "width"
wrap = attribute "wrap"

View File

@ -0,0 +1,4 @@
fn main() {
println!("cargo:rustc-link-lib=dylib=app");
println!("cargo:rustc-link-search=.");
}

View File

@ -0,0 +1,3 @@
extern int rust_main();
int main() { return rust_main(); }

View File

@ -0,0 +1,9 @@
platform "static-site-gen"
requires {} { transformFileContent : List U8 -> Result (List U8) Str }
exposes []
packages {}
imports []
provides [transformFileContentForHost]
transformFileContentForHost : List U8 -> Result (List U8) Str
transformFileContentForHost = \list -> transformFileContent list

View File

@ -0,0 +1,171 @@
use core::ffi::c_void;
use libc;
use roc_std::{RocList, RocResult, RocStr};
use std::env;
use std::ffi::CStr;
use std::fs;
use std::os::raw::c_char;
use std::path::{Path, PathBuf};
extern "C" {
#[link_name = "roc__transformFileContentForHost_1_exposed"]
fn roc_transformFileContentForHost(content: RocList<u8>) -> RocResult<RocList<u8>, RocStr>;
}
#[no_mangle]
pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
libc::malloc(size)
}
#[no_mangle]
pub unsafe extern "C" fn roc_realloc(
c_ptr: *mut c_void,
new_size: usize,
_old_size: usize,
_alignment: u32,
) -> *mut c_void {
libc::realloc(c_ptr, new_size)
}
#[no_mangle]
pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
libc::free(c_ptr)
}
#[no_mangle]
pub extern "C" fn rust_main() -> i32 {
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
eprintln!("Usage: {} path/to/input/dir path/to/output/dir", args[0]);
return 1;
}
match run(&args[1], &args[2]) {
Err(e) => {
eprintln!("{}", e);
1
}
Ok(()) => 0,
}
}
#[no_mangle]
pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) {
match tag_id {
0 => {
let slice = CStr::from_ptr(c_ptr as *const c_char);
let string = slice.to_str().unwrap();
eprintln!("Roc hit a panic: {}", string);
std::process::exit(1);
}
_ => todo!(),
}
}
#[no_mangle]
pub unsafe extern "C" fn roc_memcpy(
dest: *mut c_void,
src: *const c_void,
bytes: usize,
) -> *mut c_void {
libc::memcpy(dest, src, bytes)
}
#[no_mangle]
pub unsafe extern "C" fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut c_void {
libc::memset(dst, c, n)
}
fn run(input_dirname: &str, output_dirname: &str) -> Result<(), String> {
let input_dir = PathBuf::from(input_dirname)
.canonicalize()
.map_err(|e| format!("{}: {}", input_dirname, e))?;
let output_dir = {
let dir = PathBuf::from(output_dirname);
if !dir.exists() {
fs::create_dir(&dir).unwrap();
}
dir.canonicalize()
.map_err(|e| format!("{}: {}", output_dirname, e))?
};
let mut input_files: Vec<PathBuf> = vec![];
find_files(&input_dir, &mut input_files)
.map_err(|e| format!("Error finding input files: {}", e))?;
println!("Processing {} input files...", input_files.len());
let mut had_errors = false;
// TODO: process the files asynchronously
for input_file in input_files {
match process_file(&input_dir, &output_dir, &input_file) {
Ok(()) => {}
Err(e) => {
eprintln!("{}", e);
had_errors = true;
}
}
}
if had_errors {
Err("Could not process all files".into())
} else {
Ok(())
}
}
fn process_file(input_dir: &Path, output_dir: &Path, input_file: &Path) -> Result<(), String> {
let rust_content = match fs::read(input_file) {
Ok(bytes) => bytes,
Err(e) => {
return Err(format!(
"Error reading {}: {}",
input_file.to_str().unwrap_or("an input file"),
e
));
}
};
let roc_content = RocList::from_iter(rust_content);
let roc_result = unsafe { roc_transformFileContentForHost(roc_content) };
match Result::from(roc_result) {
Ok(roc_output_bytes) => {
let input_relpath = input_file
.strip_prefix(input_dir)
.map_err(|e| e.to_string())?
.to_path_buf();
let mut output_relpath = input_relpath.clone();
output_relpath.set_extension("html");
let output_file = output_dir.join(&output_relpath);
let rust_output_bytes = Vec::from_iter(roc_output_bytes.into_iter());
fs::write(&output_file, &rust_output_bytes).map_err(|e| format!("{}", e))?;
println!(
"{} -> {}",
input_relpath.display(),
output_relpath.display()
);
Ok(())
}
Err(roc_error_str) => Err(format!(
"Error transforming {}: {}",
input_file.to_str().unwrap_or("an input file"),
roc_error_str.as_str()
)),
}
}
fn find_files(dir: &Path, file_paths: &mut Vec<PathBuf>) -> std::io::Result<()> {
for entry in fs::read_dir(dir)? {
let pathbuf = entry?.path();
if pathbuf.is_dir() {
find_files(&pathbuf, file_paths)?;
} else {
file_paths.push(pathbuf);
}
}
Ok(())
}

View File

@ -0,0 +1,3 @@
fn main() {
std::process::exit(host::rust_main());
}

View File

@ -0,0 +1,26 @@
app "static-site"
packages { pf: "platform/main.roc" }
imports [pf.Html.{ html, head, body, div, text }]
provides [transformFileContent] to pf
transformFileContent : List U8 -> Result (List U8) Str
transformFileContent = \content ->
when Str.fromUtf8 content is
Err _ -> Err "Invalid UTF-8"
Ok contentStr ->
contentStr
|> view
|> Html.render
|> Str.toUtf8
|> Ok
view : Str -> Html.Node
view = \content ->
html [] [
head [] [],
body [] [
div [] [
text content,
],
],
]