// Serialize MoonBlox AST types into MoonBit source code expressions.
// Used at build time to pre-parse markdown and generate code that
// constructs the AST directly, skipping runtime parsing.
///|
/// Escape a string for embedding in a MoonBit string literal.
/// Handles \\, \", \n, \t, \r.
pub fn escape_string(s : String) -> String {
let sb = StringBuilder::new()
for i = 0; i < s.length(); i = i + 1 {
let c = s.get_char(i).unwrap_or(' ')
if c == '\\' {
sb.write_string("\\\\")
} else if c == '"' {
sb.write_string("\\\"")
} else if c == '\n' {
sb.write_string("\\n")
} else if c == '\t' {
sb.write_string("\\t")
} else if c == '\r' {
sb.write_string("\\r")
} else {
sb.write_char(c)
}
}
sb.to_string()
}
///|
/// Generate indentation string of 2*n spaces.
pub fn indent_str(n : Int) -> String {
let sb = StringBuilder::new()
for i = 0; i < n; i = i + 1 {
sb.write_string(" ")
}
sb.to_string()
}
///|
/// Serialize a double value as a MoonBit double literal.
/// Ensures whole numbers get `.0` suffix.
pub fn double_to_string(n : Double) -> String {
let s = n.to_string()
if s.contains(".") || s.contains("e") || s.contains("E") {
s
} else {
s + ".0"
}
}
///|
/// Sort an array of strings (insertion sort for deterministic output).
fn sort_strings(arr : Array[String]) -> Array[String] {
let n = arr.length()
if n <= 1 {
return arr
}
for i = 1; i < n; i = i + 1 {
let key = arr[i]
let mut j = i - 1
while j >= 0 && arr[j] > key {
arr[j + 1] = arr[j]
j = j - 1
}
arr[j + 1] = key
}
arr
}
///|
/// Serialize a Value to a MoonBit expression.
pub fn serialize_value(v : @ast.Value, indent : Int) -> String {
match v {
@ast.Value::Str(s) => "@ast.Value::Str(\"" + escape_string(s) + "\")"
@ast.Value::Number(n) => "@ast.Value::Number(" + double_to_string(n) + ")"
@ast.Value::Bool(b) =>
if b {
"@ast.Value::Bool(true)"
} else {
"@ast.Value::Bool(false)"
}
@ast.Value::Var(name) => "@ast.Value::Var(\"" + escape_string(name) + "\")"
@ast.Value::Array(arr) => serialize_value_array(arr, indent)
@ast.Value::Record(m) => serialize_value_record(m, indent)
@ast.Value::Funcall(name, args) =>
"@ast.Value::Funcall(\"" +
escape_string(name) +
"\", " +
serialize_value_map_inline(args) +
")"
}
}
///|
fn serialize_value_array(arr : Array[@ast.Value], indent : Int) -> String {
if arr.length() == 0 {
return "@ast.Value::Array([])"
}
let sb = StringBuilder::new()
sb.write_string("@ast.Value::Array([\n")
for i = 0; i < arr.length(); i = i + 1 {
sb.write_string(indent_str(indent + 1))
sb.write_string(serialize_value(arr[i], indent + 1))
sb.write_string(",\n")
}
sb.write_string(indent_str(indent))
sb.write_string("])")
sb.to_string()
}
///|
fn serialize_value_record(m : Map[String, @ast.Value], indent : Int) -> String {
let keys = sort_strings(m.keys().to_array())
if keys.length() == 0 {
return "@ast.Value::Record(Map([]))"
}
let sb = StringBuilder::new()
sb.write_string("@ast.Value::Record(Map([\n")
for i = 0; i < keys.length(); i = i + 1 {
let k = keys[i]
sb.write_string(indent_str(indent + 1))
sb.write_string("(\"")
sb.write_string(escape_string(k))
sb.write_string("\", ")
sb.write_string(serialize_value(m[k], indent + 1))
sb.write_string(")")
sb.write_string(",\n")
}
sb.write_string(indent_str(indent))
sb.write_string("]))")
sb.to_string()
}
///|
/// Serialize a Map[String, Value] to a compact inline expression.
/// Used for Funcall args where inline is preferred.
fn serialize_value_map_inline(m : Map[String, @ast.Value]) -> String {
let keys = sort_strings(m.keys().to_array())
if keys.length() == 0 {
return "Map([])"
}
let sb = StringBuilder::new()
sb.write_string("Map([")
for i = 0; i < keys.length(); i = i + 1 {
let k = keys[i]
sb.write_string("(\"")
sb.write_string(escape_string(k))
sb.write_string("\", ")
sb.write_string(serialize_value_simple(m[k]))
sb.write_string(")")
if i < keys.length() - 1 {
sb.write_string(", ")
}
}
sb.write_string("])")
sb.to_string()
}
///|
/// Serialize a Value in compact form (no newlines). Used for inline map values.
fn serialize_value_simple(v : @ast.Value) -> String {
match v {
@ast.Value::Str(s) => "@ast.Value::Str(\"" + escape_string(s) + "\")"
@ast.Value::Number(n) => "@ast.Value::Number(" + double_to_string(n) + ")"
@ast.Value::Bool(b) =>
if b {
"@ast.Value::Bool(true)"
} else {
"@ast.Value::Bool(false)"
}
@ast.Value::Var(name) => "@ast.Value::Var(\"" + escape_string(name) + "\")"
@ast.Value::Array(arr) =>
if arr.length() == 0 {
"@ast.Value::Array([])"
} else {
let sb = StringBuilder::new()
sb.write_string("@ast.Value::Array([")
for i = 0; i < arr.length(); i = i + 1 {
sb.write_string(serialize_value_simple(arr[i]))
if i < arr.length() - 1 {
sb.write_string(", ")
}
}
sb.write_string("])")
sb.to_string()
}
@ast.Value::Record(m) => serialize_record_simple(m)
@ast.Value::Funcall(name, args) =>
"@ast.Value::Funcall(\"" +
escape_string(name) +
"\", " +
serialize_value_map_inline(args) +
")"
}
}
///|
fn serialize_record_simple(m : Map[String, @ast.Value]) -> String {
let keys = sort_strings(m.keys().to_array())
if keys.length() == 0 {
return "@ast.Value::Record(Map([]))"
}
let sb = StringBuilder::new()
sb.write_string("@ast.Value::Record(Map([")
for i = 0; i < keys.length(); i = i + 1 {
let k = keys[i]
sb.write_string("(\"")
sb.write_string(escape_string(k))
sb.write_string("\", ")
sb.write_string(serialize_value_simple(m[k]))
sb.write_string(")")
if i < keys.length() - 1 {
sb.write_string(", ")
}
}
sb.write_string("]))")
sb.to_string()
}
///|
/// Serialize a Map[String, Value] to a MoonBit expression.
pub fn serialize_value_map(
m : Map[String, @ast.Value],
_indent : Int,
) -> String {
serialize_value_map_inline(m)
}
///|
/// Serialize a Map[String, String] to a MoonBit expression.
pub fn serialize_string_map(m : Map[String, String], _indent : Int) -> String {
let keys = sort_strings(m.keys().to_array())
if keys.length() == 0 {
return "Map([])"
}
let sb = StringBuilder::new()
sb.write_string("Map([")
for i = 0; i < keys.length(); i = i + 1 {
let k = keys[i]
sb.write_string("(\"")
sb.write_string(escape_string(k))
sb.write_string("\", \"")
sb.write_string(escape_string(m[k]))
sb.write_string("\")")
if i < keys.length() - 1 {
sb.write_string(", ")
}
}
sb.write_string("])")
sb.to_string()
}
///|
/// Serialize a Span
pub fn serialize_span(span : @markdown.Span) -> String {
"@markdown.Span::{ from: " +
span.from.to_string() +
", to: " +
span.to.to_string() +
" }"
}
///|
/// Serialize a TableAlign
pub fn serialize_align(align : @markdown.TableAlign) -> String {
match align {
@markdown.TableAlign::Left => "@markdown.TableAlign::Left"
@markdown.TableAlign::Center => "@markdown.TableAlign::Center"
@markdown.TableAlign::Right => "@markdown.TableAlign::Right"
@markdown.TableAlign::None => "@markdown.TableAlign::None"
}
}
///|
/// Serialize an EmphasisMarker
pub fn serialize_emphasis(m : @markdown.EmphasisMarker) -> String {
match m {
@markdown.EmphasisMarker::Asterisk => "@markdown.EmphasisMarker::Asterisk"
@markdown.EmphasisMarker::Underscore =>
"@markdown.EmphasisMarker::Underscore"
}
}
///|
/// Serialize a HardBreakStyle
pub fn serialize_hardbreak(s : @markdown.HardBreakStyle) -> String {
match s {
@markdown.HardBreakStyle::TwoSpaces => "@markdown.HardBreakStyle::TwoSpaces"
@markdown.HardBreakStyle::Backslash => "@markdown.HardBreakStyle::Backslash"
}
}
///|
/// Serialize @markdown.Inline with given indent.
fn serialize_md_inline(inline : @markdown.Inline, _indent : Int) -> String {
match inline {
@markdown.Inline::Text(content~, span~) =>
"@markdown.Inline::Text(content=\"" +
escape_string(content) +
"\", span=" +
serialize_span(span) +
")"
@markdown.Inline::SoftBreak(span~) =>
"@markdown.Inline::SoftBreak(span=" + serialize_span(span) + ")"
@markdown.Inline::HardBreak(style~, span~) =>
"@markdown.Inline::HardBreak(style=" +
serialize_hardbreak(style) +
", span=" +
serialize_span(span) +
")"
@markdown.Inline::Emphasis(marker~, children~, span~) =>
"@markdown.Inline::Emphasis(marker=" +
serialize_emphasis(marker) +
", children=" +
serialize_md_inlines_simple(children) +
", span=" +
serialize_span(span) +
")"
@markdown.Inline::Strong(marker~, children~, span~) =>
"@markdown.Inline::Strong(marker=" +
serialize_emphasis(marker) +
", children=" +
serialize_md_inlines_simple(children) +
", span=" +
serialize_span(span) +
")"
@markdown.Inline::Strikethrough(children~, span~) =>
"@markdown.Inline::Strikethrough(children=" +
serialize_md_inlines_simple(children) +
", span=" +
serialize_span(span) +
")"
@markdown.Inline::Code(content~, backtick_count~, span~) =>
"@markdown.Inline::Code(content=\"" +
escape_string(content) +
"\", backtick_count=" +
backtick_count.to_string() +
", span=" +
serialize_span(span) +
")"
@markdown.Inline::WikiLink(target~, label~, fragment~, span~) =>
"@markdown.Inline::WikiLink(target=\"" +
escape_string(target) +
"\", label=\"" +
escape_string(label) +
"\", fragment=\"" +
escape_string(fragment) +
"\", span=" +
serialize_span(span) +
")"
@markdown.Inline::Link(children~, url~, title~, span~) =>
"@markdown.Inline::Link(children=" +
serialize_md_inlines_simple(children) +
", url=\"" +
escape_string(url) +
"\", title=\"" +
escape_string(title) +
"\", span=" +
serialize_span(span) +
")"
@markdown.Inline::RefLink(children~, label~, span~) =>
"@markdown.Inline::RefLink(children=" +
serialize_md_inlines_simple(children) +
", label=\"" +
escape_string(label) +
"\", span=" +
serialize_span(span) +
")"
@markdown.Inline::Autolink(url~, is_email~, span~) =>
"@markdown.Inline::Autolink(url=\"" +
escape_string(url) +
"\", is_email=" +
(if is_email { "true" } else { "false" }) +
", span=" +
serialize_span(span) +
")"
@markdown.Inline::Image(alt~, url~, title~, span~) =>
"@markdown.Inline::Image(alt=\"" +
escape_string(alt) +
"\", url=\"" +
escape_string(url) +
"\", title=\"" +
escape_string(title) +
"\", span=" +
serialize_span(span) +
")"
@markdown.Inline::RefImage(alt~, label~, span~) =>
"@markdown.Inline::RefImage(alt=\"" +
escape_string(alt) +
"\", label=\"" +
escape_string(label) +
"\", span=" +
serialize_span(span) +
")"
@markdown.Inline::HtmlInline(html~, span~) =>
"@markdown.Inline::HtmlInline(html=\"" +
escape_string(html) +
"\", span=" +
serialize_span(span) +
")"
@markdown.Inline::FootnoteReference(label~, span~) =>
"@markdown.Inline::FootnoteReference(label=\"" +
escape_string(label) +
"\", span=" +
serialize_span(span) +
")"
}
}
///|
/// Serialize @markdown.Inline array in compact form (no newlines).
fn serialize_md_inlines_simple(inlines : Array[@markdown.Inline]) -> String {
if inlines.length() == 0 {
return "[]"
}
let sb = StringBuilder::new()
sb.write_string("[")
for i = 0; i < inlines.length(); i = i + 1 {
sb.write_string(serialize_md_inline(inlines[i], 0))
if i < inlines.length() - 1 {
sb.write_string(", ")
}
}
sb.write_string("]")
sb.to_string()
}
///|
/// Serialize an array of @markdown.Inline with given indent.
pub fn serialize_md_inlines(
inlines : Array[@markdown.Inline],
_indent : Int,
) -> String {
serialize_md_inlines_simple(inlines)
}
///|
/// Serialize @ast.Inline with given indent.
pub fn serialize_inline(inline : @ast.Inline, indent : Int) -> String {
match inline {
@ast.Inline::Markdown(md) =>
"@ast.Inline::Markdown(" + serialize_md_inline(md, indent) + ")"
@ast.Inline::Component(name, args) =>
"@ast.Inline::Component(\"" +
escape_string(name) +
"\", " +
serialize_value_map_inline(args) +
")"
}
}
///|
/// Serialize an array of @ast.Inline with given indent.
pub fn serialize_inlines(inlines : Array[@ast.Inline], indent : Int) -> String {
if inlines.length() == 0 {
return "[]"
}
let sb = StringBuilder::new()
sb.write_string("[\n")
for i = 0; i < inlines.length(); i = i + 1 {
sb.write_string(indent_str(indent + 1))
sb.write_string(serialize_inline(inlines[i], indent + 1))
sb.write_string(",\n")
}
sb.write_string(indent_str(indent))
sb.write_string("]")
sb.to_string()
}
///|
/// Serialize a BlockTableCell
pub fn serialize_cell(cell : @ast.BlockTableCell, indent : Int) -> String {
let sb = StringBuilder::new()
sb.write_string("@ast.BlockTableCell::{ children: ")
sb.write_string(serialize_inlines(cell.children, indent))
sb.write_string(" }")
sb.to_string()
}
///|
/// Serialize a ListItem
pub fn serialize_list_item(item : @ast.ListItem, indent : Int) -> String {
let sb = StringBuilder::new()
sb.write_string("@ast.ListItem::{ children: ")
sb.write_string(serialize_blocks(item.children, indent))
sb.write_string(" }")
sb.to_string()
}
///|
/// Serialize an array of @ast.Block with given indent.
pub fn serialize_blocks(blocks : Array[@ast.Block], indent : Int) -> String {
if blocks.length() == 0 {
return "[]"
}
let sb = StringBuilder::new()
sb.write_string("[\n")
for i = 0; i < blocks.length(); i = i + 1 {
sb.write_string(indent_str(indent + 1))
sb.write_string(serialize_block(blocks[i], indent + 1))
sb.write_string(",\n")
}
sb.write_string(indent_str(indent))
sb.write_string("]")
sb.to_string()
}
///|
/// Serialize a single Block with given indent.
pub fn serialize_block(block : @ast.Block, indent : Int) -> String {
match block {
@ast.Block::Paragraph(p) => {
let sb = StringBuilder::new()
sb.write_string("@ast.Block::Paragraph(@ast.BlockParagraph::{\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("children: ")
sb.write_string(serialize_inlines(p.children, indent + 1))
sb.write_string(",\n")
sb.write_string(indent_str(indent))
sb.write_string("})")
sb.to_string()
}
@ast.Block::Heading(h) => {
let sb = StringBuilder::new()
sb.write_string("@ast.Block::Heading(@ast.BlockHeading::{\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("level: ")
sb.write_string(h.level.to_string())
sb.write_string(",\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("children: ")
sb.write_string(serialize_inlines(h.children, indent + 1))
sb.write_string(",\n")
sb.write_string(indent_str(indent))
sb.write_string("})")
sb.to_string()
}
@ast.Block::CodeBlock(c) => {
let info_str = match c.info {
Some(info) => "Some(\"" + escape_string(info) + "\")"
None => "None"
}
let sb = StringBuilder::new()
sb.write_string("@ast.Block::CodeBlock(@ast.BlockCode::{\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("info: ")
sb.write_string(info_str)
sb.write_string(",\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("content: \"")
sb.write_string(escape_string(c.content))
sb.write_string("\",\n")
sb.write_string(indent_str(indent))
sb.write_string("})")
sb.to_string()
}
@ast.Block::ContainerComponent(cc) => {
let sb = StringBuilder::new()
sb.write_string("@ast.Block::ContainerComponent(@ast.BlockContainer::{\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("name: \"")
sb.write_string(escape_string(cc.name))
sb.write_string("\",\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("args: ")
sb.write_string(serialize_value_map_inline(cc.args))
sb.write_string(",\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("children: ")
sb.write_string(serialize_blocks(cc.children, indent + 1))
sb.write_string(",\n")
sb.write_string(indent_str(indent))
sb.write_string("})")
sb.to_string()
}
@ast.Block::BlockQuote(blocks) =>
"@ast.Block::BlockQuote(" + serialize_blocks(blocks, indent) + ")"
@ast.Block::List(l) => {
let sb = StringBuilder::new()
sb.write_string("@ast.Block::List(@ast.BlockList::{\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("ordered: ")
sb.write_string(if l.ordered { "true" } else { "false" })
sb.write_string(",\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("tight: ")
sb.write_string(if l.tight { "true" } else { "false" })
sb.write_string(",\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("items: [\n")
for i = 0; i < l.items.length(); i = i + 1 {
sb.write_string(indent_str(indent + 2))
sb.write_string(serialize_list_item(l.items[i], indent + 2))
sb.write_string(",\n")
}
sb.write_string(indent_str(indent + 1))
sb.write_string("],\n")
sb.write_string(indent_str(indent))
sb.write_string("})")
sb.to_string()
}
@ast.Block::Table(t) => {
let sb = StringBuilder::new()
sb.write_string("@ast.Block::Table(@ast.BlockTable::{\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("header: [\n")
for i = 0; i < t.header.length(); i = i + 1 {
sb.write_string(indent_str(indent + 2))
sb.write_string(serialize_cell(t.header[i], indent + 2))
sb.write_string(",\n")
}
sb.write_string(indent_str(indent + 1))
sb.write_string("],\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("alignments: [\n")
for i = 0; i < t.alignments.length(); i = i + 1 {
sb.write_string(indent_str(indent + 2))
sb.write_string(serialize_align(t.alignments[i]))
sb.write_string(",\n")
}
sb.write_string(indent_str(indent + 1))
sb.write_string("],\n")
sb.write_string(indent_str(indent + 1))
sb.write_string("rows: [\n")
for i = 0; i < t.rows.length(); i = i + 1 {
let row = t.rows[i]
sb.write_string(indent_str(indent + 2))
sb.write_string("[\n")
for j = 0; j < row.length(); j = j + 1 {
sb.write_string(indent_str(indent + 3))
sb.write_string(serialize_cell(row[j], indent + 3))
sb.write_string(",\n")
}
sb.write_string(indent_str(indent + 2))
sb.write_string("],\n")
}
sb.write_string(indent_str(indent + 1))
sb.write_string("],\n")
sb.write_string(indent_str(indent))
sb.write_string("})")
sb.to_string()
}
@ast.Block::ThematicBreak => "@ast.Block::ThematicBreak"
}
}
///|
/// Serialize a Document to a MoonBit expression string.
/// Returns code like:
/// @ast.Document::{ children: [...], meta: Map([("title", "Hello")]) }
pub fn serialize_document(doc : @ast.Document) -> String {
let sb = StringBuilder::new()
sb.write_string("@ast.Document::{\n")
sb.write_string(indent_str(1))
sb.write_string("children: ")
sb.write_string(serialize_blocks(doc.children, 1))
sb.write_string(",\n")
sb.write_string(indent_str(1))
sb.write_string("meta: ")
sb.write_string(serialize_string_map(doc.meta, 1))
sb.write_string(",\n")
sb.write_string("}")
sb.to_string()
}