///|
async fn main {
let cmd = @argparse.Command(
"tim",
about="Compile an .alt file to HTML.",
flags=[
@argparse.FlagArg(
"watch",
short='w',
long="watch",
about="Watch the input file and recompile on changes.",
),
],
positionals=[
@argparse.PositionArg(
"input",
about="Path to the .alt file.",
num_args=@argparse.ValueRange::single(),
),
],
)
let matches = cmd.parse() catch { err => return println(err) }
guard matches.values.get("input") is Some([input_path]) else {
return println(cmd.render_help())
}
if !input_path.has_suffix(".alt") {
return println("error: input path must end with .alt")
}
let output_path = output_path_for(input_path)
if matches.flags.get("watch") is Some(true) {
watch_and_compile(input_path, output_path)
} else {
compile_file(input_path, output_path)
}
}
///|
fn compile_file(input_path : String, output_path : String) -> Unit {
guard read_input(input_path) is Some(source) &&
render_html(input_path, source) is Some(html) &&
write_output(output_path, html) else {
return
}
println("output: \{output_path}")
}
///|
async fn watch_and_compile(input_path : String, output_path : String) -> Unit {
let input_name = input_filename(input_path)
let watcher = @async_fs.Watcher(watch_directory(input_path), ignored_paths=path => {
path != input_name
}) catch {
error => return println("error: unable to watch \{input_path}: \{error}")
}
defer watcher.close()
compile_file(input_path, output_path)
println("watching: \{input_path}")
for ;; {
watcher.wait_any() catch {
error => return println("error: stopped watching \{input_path}: \{error}")
}
compile_file(input_path, output_path)
}
}
///|
fn output_path_for(input_path : String) -> String {
let end = input_path.length() - 4
input_path.unsafe_substring(start=0, end~) + ".html"
}
///|
fn render_html(input_path : String, source : String) -> String? {
let options = Some({
..@backend_html.HtmlDocumentOptions::default(),
extensions: [html_include_resolver(input_dir(input_path))],
})
try @backend_html.to_html_document(source, options) catch {
error => {
println(error.render())
None
}
} noraise {
html => Some(html)
}
}
///|
fn html_include_resolver(base_dir : String) -> @backend_html.ElementResolver {
compiler => {
guard compiler.call.tag == "html.include" else { return None }
let path = include_path(compiler.call, compiler.context)
let resolved = resolve_include_path(base_dir, path)
let content = read_include(resolved, compiler.call.span)
let raw = @sexp.SexpNode::RawHtml(content)
Some(@backend_html.ElementOutput::body([raw]))
}
}
///|
fn include_path(
call : @backend_html.ElementCall,
context : @backend_html.CompileContext,
) -> String raise @core.Diagnostic {
for attr in call.attrs {
if attr.name != "src" {
continue
}
guard attr.value is Some(value) else {
raise attr.span.diagnostic(
@core.DiagnosticPhase::Type,
"html.include src requires a value",
)
}
return value.to_scalar(context.env)
}
guard !call.children.is_empty() else {
raise call.span.diagnostic(
@core.DiagnosticPhase::Type,
"html.include requires a path",
)
}
@core.nodes_to_value(call.children).to_scalar(context.env)
}
///|
fn read_include(
path : String,
span : @core.SourceSpan,
) -> String raise @core.Diagnostic {
@fs.read_file_to_string(path) catch {
@fs.IOError(msg) =>
raise span.diagnostic(
@core.DiagnosticPhase::ContentModel,
describe_io_error("including", path, msg),
)
}
}
///|
fn resolve_include_path(base_dir : String, path : String) -> String {
if path.has_prefix("/") || base_dir.is_empty() {
return path
}
base_dir + path
}
///|
fn input_dir(path : String) -> String {
let chars = path.to_array()
let mut index = chars.length()
while index > 0 {
index -= 1
if is_path_separator(chars[index]) {
return path.unsafe_substring(start=0, end=index + 1)
}
}
""
}
///|
fn input_filename(path : String) -> String {
let dir = input_dir(path)
path.unsafe_substring(start=dir.length(), end=path.length())
}
///|
fn watch_directory(path : String) -> String {
let dir = input_dir(path)
guard !dir.is_empty() else { dir }
"."
}
///|
fn is_path_separator(ch : Char) -> Bool {
ch == '/' || ch == '\\'
}
///|
fn read_input(path : String) -> String? {
Some(@fs.read_file_to_string(path)) catch {
@fs.IOError(msg) => {
println(describe_io_error("reading", path, msg))
None
}
}
}
///|
fn write_output(path : String, content : String) -> Bool {
try {
@fs.write_string_to_file(path, content)
true
} catch {
@fs.IOError(msg) => {
println(describe_io_error("writing", path, msg))
false
}
}
}