use textwrap::{Options, wrap};
use tree_sitter::{Node, Parser};
use crate::Config;
use crate::restoration::format_node;
use crate::types::*;
pub(crate) fn format_template_only(html: &str, nodes: &[AskamaNode], config: &Config) -> String {
let mut result = Vec::new();
let mut indent_size = 0;
for token in tokenize(html) {
if let Some(idx) = extract_placeholder_index(token) {
if let Some(node) = nodes.get(idx) {
let (pre, post) = node.indent_delta();
indent_size = (indent_size + pre).max(0);
result.push(format!(
"{}{}",
" ".repeat(
(indent_size * config.indent_size as i32)
.try_into()
.unwrap()
),
format_node(node, indent_size.try_into().unwrap(), config)
));
indent_size = (indent_size + post).max(0);
}
} else if !token.trim().is_empty() {
let indent = "";
let wrapped = wrap_text_with_indent(token.trim(), indent, config.max_line_length);
result.extend(wrapped);
}
}
result.join("\n")
}
fn tokenize(input: &str) -> Vec<&str> {
let mut tokens = Vec::new();
let mut pos = 0;
while pos < input.len() {
if let Some(start) = input[pos..].find(ASKAMA_TOKEN) {
if start > 0 {
tokens.push(&input[pos..pos + start]);
pos += start;
}
if let Some(end) = input[pos..].find(ASKAMA_END_TOKEN) {
tokens.push(&input[pos..pos + end + ASKAMA_END_TOKEN.len()]);
pos += end + ASKAMA_END_TOKEN.len();
} else {
break;
}
} else {
tokens.push(&input[pos..]);
break;
}
}
tokens
}
pub(crate) fn extract_placeholder_index(token: &str) -> Option<usize> {
for prefix in &[ASKAMA_CTRL_TOKEN, ASKAMA_EXPR_TOKEN, ASKAMA_COMMENT_TOKEN] {
if let Some(rest) = token.strip_prefix(prefix)
&& let Some(idx_str) = rest.strip_suffix(ASKAMA_END_TOKEN)
{
return idx_str.parse().ok();
}
}
None
}
pub(crate) fn format_template_with_html(
html_parser: &mut Parser,
html: &str,
nodes: &[AskamaNode],
config: &Config,
) -> Result<String, Box<dyn std::error::Error>> {
let tree = html_parser.parse(html, None).ok_or("HTML parse failed")?;
let node = &tree.root_node();
let source = html.as_bytes();
let mut result = String::new();
let mut current_indent = 0;
format_html_with_indent(
node,
source,
nodes,
config,
&mut current_indent,
&mut result,
)?;
Ok(result.trim_end().to_string())
}
pub(crate) fn wrap_text_with_indent(text: &str, indent: &str, max_length: usize) -> Vec<String> {
let available_width = max_length.saturating_sub(indent.len());
let options = Options::new(available_width);
wrap(text, &options)
.into_iter()
.map(|line| format!("{}{}", indent, line))
.collect()
}
fn format_html_with_indent(
node: &Node,
source: &[u8],
nodes: &[AskamaNode],
config: &Config,
indent: &mut i32,
result: &mut String,
) -> Result<(), Box<dyn std::error::Error>> {
match node.kind() {
"document" | "fragment" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
format_html_with_indent(&child, source, nodes, config, indent, result)?;
}
}
"element" => {
format_element(node, source, nodes, config, indent, result)?;
}
"text" => {
let text = node.utf8_text(source)?;
format_text_with_indent(text, nodes, config, indent, result)?;
}
_ => {
let current_indent = " ".repeat((*indent as usize) * config.indent_size);
result.push_str(&format!("{}{}\n", current_indent, node.utf8_text(source)?));
}
}
Ok(())
}
fn format_text_with_indent(
text: &str,
nodes: &[AskamaNode],
config: &Config,
indent: &mut i32,
result: &mut String,
) -> Result<(), Box<dyn std::error::Error>> {
for token in tokenize(text) {
if let Some(idx) = extract_placeholder_index(token) {
if let Some(node) = nodes.get(idx) {
let (pre, post) = node.indent_delta();
*indent = (*indent + pre).max(0);
let node_prefix = " ".repeat((*indent as usize) * config.indent_size);
let formatted_node = format_node(node, (*indent).try_into().unwrap_or(0), config);
result.push_str(&format!("{}{}\n", node_prefix, formatted_node));
*indent = (*indent + post).max(0);
}
} else if !token.trim().is_empty() {
let wrapped_lines = wrap_text_content(token, (*indent).try_into().unwrap_or(0), config);
for line in wrapped_lines {
result.push_str(&line);
result.push('\n');
}
}
}
Ok(())
}
fn format_element(
node: &Node,
source: &[u8],
nodes: &[AskamaNode],
config: &Config,
indent: &mut i32,
result: &mut String,
) -> Result<(), Box<dyn std::error::Error>> {
let prefix = " ".repeat((*indent as usize) * config.indent_size);
let mut cursor = node.walk();
let children: Vec<Node> = node.children(&mut cursor).collect();
let start_tag = children.iter().find(|n| n.kind() == "start_tag");
let end_tag = children.iter().find(|n| n.kind() == "end_tag");
let content: Vec<&Node> = children
.iter()
.filter(|n| n.kind() != "start_tag" && n.kind() != "end_tag")
.collect();
if let Some(start) = start_tag {
let start_text = start.utf8_text(source)?;
if should_inline(&content, source, nodes, config) {
if let Some(end) = end_tag {
let content_text = content[0].utf8_text(source)?.trim();
result.push_str(&format!(
"{}{}{}{}\n",
prefix,
start_text,
content_text,
end.utf8_text(source)?
));
}
} else {
result.push_str(&format!("{}{}\n", prefix, start_text));
*indent += 1;
for child in &content {
format_html_with_indent(child, source, nodes, config, indent, result)?;
}
*indent -= 1;
if let Some(end) = end_tag {
result.push_str(&format!("{}{}\n", prefix, end.utf8_text(source)?));
}
}
}
Ok(())
}
fn should_inline(content: &[&Node], source: &[u8], _nodes: &[AskamaNode], config: &Config) -> bool {
content.len() == 1
&& content[0].kind() == "text"
&& content[0]
.utf8_text(source)
.map(|t| !t.contains('\n') && t.len() < config.max_line_length / 2)
.unwrap_or(false)
}
fn wrap_text_content(text: &str, indent: usize, config: &Config) -> Vec<String> {
let prefix = " ".repeat(indent * config.indent_size);
let mut result = Vec::new();
for line in text.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let wrapped = wrap_text_with_indent(trimmed, &prefix, config.max_line_length);
result.extend(wrapped);
}
result
}