///|
/// Best-effort whole-document formatter used by the `.mbtv` language server.
pub fn format_document(
source : String,
filename? : String = "Component.mbtv",
) -> String? {
let doc = @compiler.parse_sfc(source, filename~) catch { _ => return None }
let template_nodes = @compiler.parse_template(doc.template) catch {
_ =>
return Some(
format_blocks(doc.blocks, normalize_raw_template_content(doc.template)),
)
}
Some(
format_blocks(
doc.blocks,
render_template_block(doc.template, template_nodes),
),
)
}
///|
/// Convert a successful format into one full-document text edit.
pub fn formatting_edit(
source : String,
filename? : String = "Component.mbtv",
) -> ToolingTextEdit? {
match format_document(source, filename~) {
Some(formatted) =>
if formatted == source {
None
} else {
Some({ range: full_document_range(source), new_text: formatted })
}
None => None
}
}
///|
/// Re-render all top-level blocks with stable spacing between them.
fn format_blocks(
blocks : Array[@compiler.SfcBlock],
rendered_template : String,
) -> String {
let rendered : Array[String] = []
for block in blocks {
rendered.push(
match block.kind {
@compiler_common.Template =>
render_named_block("template", block.attrs, rendered_template)
@compiler_common.Style =>
render_named_block(
"style",
block.attrs,
normalize_raw_block_content(block.content),
)
@compiler_common.ScriptSetup | @compiler_common.ScriptExtern =>
render_named_block(
"script",
block.attrs,
normalize_raw_block_content(block.content),
)
},
)
}
rendered.join("\n\n") + "\n"
}
///|
/// Render one top-level SFC block using its original attribute set.
fn render_named_block(
tag : String,
attrs : Array[@compiler.BlockAttr],
content : String,
) -> String {
let sb = StringBuilder::new()
sb.write_char('<')
sb.write_string(tag)
for attr in attrs {
sb.write_char(' ')
sb.write_string(attr.name)
match attr.value {
Some(value) => {
sb.write_string("=\"")
sb.write_string(escape_block_attr_value(value))
sb.write_char('"')
}
None => ()
}
}
sb.write_string(">\n")
if content.length() > 0 {
sb.write_string(content)
sb.write_char('\n')
}
sb.write_string("")
sb.write_string(tag)
sb.write_char('>')
sb.to_string()
}
///|
/// Normalize script/style block bodies without trying to rewrite MoonBit or CSS syntax.
fn normalize_raw_block_content(content : String) -> String {
let lines = content.split("\n").map(fn(line) { line.to_string() }).collect()
let normalized : Array[String] = []
for line in lines {
normalized.push(line.trim_end(chars=" \t\r").to_string())
}
let mut start = 0
let mut end_ = normalized.length()
while start < end_ && normalized[start].trim().length() == 0 {
start += 1
}
while end_ > start && normalized[end_ - 1].trim().length() == 0 {
end_ -= 1
}
let trimmed : Array[String] = []
for index in start..` block when the AST can be rendered safely.
fn render_template_block(
raw_template : String,
nodes : Array[@compiler.TemplateNode],
) -> String {
if raw_template.contains("