summaryrefslogtreecommitdiff
path: root/src/compile.rs
blob: ccbf6f077454b0a16235dc5ad8062a118a81a4ac (plain)
use html_escape::{encode_double_quoted_attribute, encode_single_quoted_attribute};

use crate::analyze::{ConstantExpression, ConstantSymbolId, ConstantValue, ScopedSymbolTable};

fn symbol_table_from_path<'a>(
	symbol_table: &'a ScopedSymbolTable<'a>,
	path: &'a str,
) -> Option<&'a ScopedSymbolTable<'a>> {
	let mut symbol_table = symbol_table;
	for component in path.split("::") {
		if let Some(module) = symbol_table
			.modules
			.values()
			.find(|module| module.name == Some(component))
			.and_then(|module| module.symbol_table)
		{
			symbol_table = module;
		} else {
			return None;
		}
	}

	Some(symbol_table)
}

fn compile_attribute_value<'a>(
	buffer: &mut String,
	symbol_table: &'a ScopedSymbolTable<'a>,
	name: &'a str,
	value: Option<ConstantSymbolId>,
) {
	buffer.push(' ');
	buffer.push_str(name);
	let Some(value) = value else {
		return;
	};

	buffer.push('=');

	let mut new_buffer = String::new();
	compile_constant(&mut new_buffer, symbol_table, value);

	let use_double_quotes = !new_buffer.contains('"') || new_buffer.contains('\'');
	let delimiter = if !use_double_quotes { '\'' } else { '"' };
	let value = if use_double_quotes {
		encode_double_quoted_attribute(&new_buffer)
	} else {
		encode_single_quoted_attribute(&new_buffer)
	};

	buffer.push(delimiter);
	buffer.push_str(&value);
	buffer.push(delimiter);
}

fn compile_constant<'a>(
	buffer: &mut String,
	symbol_table: &'a ScopedSymbolTable<'a>,
	id: ConstantSymbolId,
) {
	let Some(constant) = symbol_table.constants.get(&id) else {
		return;
	};
	let Some(expression) = &constant.value else {
		return;
	};
	match expression {
		ConstantExpression::Value(value) => match value {
			ConstantValue::String(value) => buffer.push_str(value),
		},
		ConstantExpression::Name(name) => {
			let mut name = *name;
			let mut symbol_table = symbol_table;
			if let Some((path, item)) = name.rsplit_once("::")
				&& let Some(module) = symbol_table_from_path(symbol_table, path)
			{
				name = item;
				symbol_table = module;
			}
			if let Some(id) = symbol_table
				.constants
				.values()
				.find_map(|value| (value.name == Some(name)).then_some(value.id))
			{
				compile_constant(buffer, symbol_table, id);
			}
		}
		ConstantExpression::Fragment(constant_symbol_ids) => {
			for constant in *constant_symbol_ids {
				compile_constant(buffer, symbol_table, *constant);
			}
		}
		ConstantExpression::Block {
			tag_name_lowercase,
			attributes,
			child,
		} => {
			buffer.push('<');
			buffer.push_str(tag_name_lowercase);
			for (name, value) in attributes {
				compile_attribute_value(buffer, symbol_table, name, *value);
			}
			buffer.push('>');
			compile_constant(buffer, symbol_table, *child);
			buffer.push_str("</");
			buffer.push_str(tag_name_lowercase);
			buffer.push('>');
		}
	}
}

fn compile_head<'a>(buffer: &mut String, symbol_tables: &[ScopedSymbolTable<'a>]) {
	for symbol_table in symbol_tables {
		for head in symbol_table.heads.values().filter_map(|head| head.contents) {
			compile_constant(buffer, symbol_table, head);
		}
	}
}

fn compile_body<'a>(buffer: &mut String, symbol_tables: &[ScopedSymbolTable<'a>]) {
	for symbol_table in symbol_tables {
		for body in symbol_table
			.bodies
			.values()
			.filter_map(|body| body.contents)
		{
			compile_constant(buffer, symbol_table, body);
		}
	}
}

pub fn compile<'a>(symbol_tables: &[ScopedSymbolTable<'a>]) -> Box<str> {
	let mut buffer = String::from("<!DOCTYPE html><html lang=\"en\"><head>");
	compile_head(&mut buffer, symbol_tables);
	buffer.push_str("</head><body>");
	compile_body(&mut buffer, symbol_tables);
	buffer.push_str("</body></html>");

	buffer.into_boxed_str()
}