///|
/// Command Line Interface for moon-typst compiler and multi-target renderer
fn print_help() -> Unit {
let help_text =
#|moon-typst: Typst-style markup parser, layout engine, and multi-format renderer in MoonBit
#|
#|Usage:
#| typst-render [OPTIONS]
#|
#|Options:
#| -o, --output Specify output file path (default: input.svg, input.pdf, or input.html)
#| -p, --pdf Render to standard PDF 1.4 binary document
#| --svg Render to vector SVG graphics (default)
#| --html Render to semantic HTML5 document
#| --no-color Disable ANSI color escapes in diagnostic messages
#| -h, --help Print help information
#| -v, --version Print version information
#|
#|Examples:
#| typst-render paper.typ -p -o paper.pdf
#| typst-render report.typ --html -o report.html
#| typst-render diagram.typ --svg -o diagram.svg
println(help_text)
}
///|
fn print_version() -> Unit {
println(
"moon-typst v0.2.1 (MoonBit Typst-style Markup Compiler & Multi-Target Renderer)",
)
}
///|
pub enum OutputFormat {
Svg
Pdf
Html
} derive(Eq, Debug)
///|
fn main {
let args = @env.args()
if args.length() <= 1 {
print_help()
return
}
let mut input_path = ""
let mut output_path = ""
let mut format = OutputFormat::Svg
let mut format_explicit = false
let mut use_color = true
let mut i = 1
while i < args.length() {
let arg = args[i]
if arg == "-h" || arg == "--help" {
print_help()
return
}
if arg == "-v" || arg == "--version" {
print_version()
return
}
if arg == "--no-color" {
use_color = false
i = i + 1
continue
}
if arg == "-p" || arg == "--pdf" {
format = OutputFormat::Pdf
format_explicit = true
i = i + 1
continue
}
if arg == "--html" {
format = OutputFormat::Html
format_explicit = true
i = i + 1
continue
}
if arg == "--svg" {
format = OutputFormat::Svg
format_explicit = true
i = i + 1
continue
}
if arg == "-o" || arg == "--output" {
if i + 1 < args.length() {
output_path = args[i + 1]
i = i + 2
continue
} else {
println("Error: Missing argument for output option (-o / --output)")
return
}
}
if input_path.length() == 0 && !arg.has_prefix("-") {
input_path = arg
i = i + 1
continue
}
i = i + 1
}
if input_path.length() == 0 {
println("Error: No input .typ file specified.")
print_help()
return
}
if !@fs.path_exists(input_path) {
let reporter = @diag.DiagnosticReporter::new(input_path, "")
reporter.add(
@diag.Diagnostic::error(
"Input file not found: " + input_path,
@diag.Span::empty(),
).with_hint("Please check the file path and ensure it exists."),
)
println(reporter.render_all(use_color))
return
}
// Auto-detect format from output extension if format not explicitly specified
if !format_explicit && output_path.length() > 0 {
if output_path.has_suffix(".pdf") {
format = OutputFormat::Pdf
} else if output_path.has_suffix(".html") || output_path.has_suffix(".htm") {
format = OutputFormat::Html
} else if output_path.has_suffix(".svg") {
format = OutputFormat::Svg
}
}
// Derive default output path if not provided
let target_output = if output_path.length() > 0 {
output_path
} else {
let stem = if input_path.has_suffix(".typ") {
input_path[0:input_path.length() - 4].to_owned()
} else {
input_path
}
match format {
OutputFormat::Pdf => stem + ".pdf"
OutputFormat::Html => stem + ".html"
OutputFormat::Svg => stem + ".svg"
}
}
// Read input file
let content = @fs.read_file_to_string(input_path) catch {
@fs.IOError(err) => {
let reporter = @diag.DiagnosticReporter::new(input_path, "")
reporter.add(
@diag.Diagnostic::error(
"Failed to read file: " + err,
@diag.Span::empty(),
),
)
println(reporter.render_all(use_color))
return
}
}
let reporter = @diag.DiagnosticReporter::new(input_path, content)
let base_style = @core.StyleContext::default()
let evaluator = @eval.Evaluator::new(base_style, reporter)
println("[moon-typst] Compiling: \{input_path}")
let doc = @parser.parse_typst(content)
println(
"[moon-typst] Abstract Syntax Tree: \{doc.blocks.length()} root blocks",
)
match format {
OutputFormat::Html => {
println("[moon-typst] Target: Semantic HTML5 document")
let html_content = @render.render_html(doc)
try {
@fs.write_string_to_file(target_output, html_content)
println("[moon-typst] Successfully wrote: \{target_output}")
} catch {
@fs.IOError(err) =>
println("[moon-typst] Error writing \{target_output}: \{err}")
}
}
OutputFormat::Pdf => {
println("[moon-typst] Target: Adobe PDF 1.4 Binary Document")
println("[moon-typst] Executing layout engine & font metrics...")
let engine = @layout.LayoutEngine::new(evaluator.style)
let page = engine.layout_doc(doc)
println(
"[moon-typst] Layout computed: \{page.boxes.length()} boxes (Page size: \{page.width}x\{page.height} pt)",
)
println(
"[moon-typst] Serializing PDF xref tables, streams, and font dicts...",
)
let pdf_content = @render.render_pdf(page)
try {
@fs.write_string_to_file(target_output, pdf_content)
println(
"[moon-typst] Successfully generated: \{target_output} (\{@unicode.to_utf8_bytes(pdf_content).length()} bytes)",
)
} catch {
@fs.IOError(err) =>
println("[moon-typst] Error writing \{target_output}: \{err}")
}
}
OutputFormat::Svg => {
println("[moon-typst] Target: Vector SVG Graphics")
println("[moon-typst] Executing layout engine & box geometry...")
let engine = @layout.LayoutEngine::new(evaluator.style)
let page = engine.layout_doc(doc)
println(
"[moon-typst] Layout computed: \{page.boxes.length()} boxes (ViewBox: 0 0 \{page.width} \{page.height})",
)
println(
"[moon-typst] Serializing vector paths, text nodes, and styles...",
)
let svg_content = @render.render_svg(page)
try {
@fs.write_string_to_file(target_output, svg_content)
println(
"[moon-typst] Successfully generated: \{target_output} (\{@unicode.to_utf8_bytes(svg_content).length()} bytes)",
)
} catch {
@fs.IOError(err) =>
println("[moon-typst] Error writing \{target_output}: \{err}")
}
}
}
if reporter.diagnostics.length() > 0 {
println("")
println(reporter.render_all(use_color))
}
}