extern crate alloc;
use alloc::rc::Rc;
use core::{error::Error, fmt::Debug};
use std::{
collections::HashMap,
fs::{self, File},
io::{ErrorKind, Read},
path::{Path, PathBuf},
};
use stawege_log::error;
use stawege_plugin::{Plugin, PluginContext};
use crate::{
tag_logics::{BlockTagLogic, ExtendsTagLogic, SetTagLogic, UnsetTagLogic},
AttributeBuilder, AttributeParseState, Attributes, HtmlError, Item, TagLogic, Template,
TemplateContext,
};
#[derive(Default)]
pub struct HtmlPlugin {
pub chars: Vec<char>,
pub index: usize,
pub last_item_end: usize,
pub template: Template,
pub source_root_path: Option<Rc<PathBuf>>,
pub output_root_path: Option<Rc<PathBuf>>,
pub default_variables: HashMap<String, String>,
pub tag_logics: Vec<Rc<dyn TagLogic>>,
pub dependencies: Vec<Rc<PathBuf>>,
}
impl HtmlPlugin {
pub fn new() -> HtmlPlugin {
HtmlPlugin {
chars: Vec::new(),
index: 0,
last_item_end: 0,
template: Template::new(),
source_root_path: None,
output_root_path: None,
default_variables: HashMap::new(),
tag_logics: vec![
Rc::new(UnsetTagLogic::new()),
Rc::new(SetTagLogic::new()),
Rc::new(BlockTagLogic::new()),
Rc::new(ExtendsTagLogic::new()),
],
dependencies: Vec::new(),
}
}
pub fn reset(&mut self) {
self.chars.clear();
self.index = 0;
self.last_item_end = 0;
self.template.clear();
}
pub fn read_file(&mut self, path: impl AsRef<Path>) -> Result<(), HtmlError> {
match fs::read_to_string(path.as_ref()) {
Ok(content) => {
self.chars = content.chars().collect();
Ok(())
}
Err(e) => match e.kind() {
ErrorKind::NotFound => Err(HtmlError::FileNotFound {
path: PathBuf::from(path.as_ref()),
}),
_ => Err(HtmlError::Other {
message: e.to_string(),
}),
},
}
}
pub fn parse_file(&mut self, file: &mut File) -> Result<(), HtmlError> {
let mut content = String::new();
if file.read_to_string(&mut content).is_err() {
Err(HtmlError::NotValidUtf8Encoding)?
};
self.chars = content.chars().collect::<Vec<char>>();
self.parse()
}
pub fn parse(&mut self) -> Result<(), HtmlError> {
if self.chars.is_empty() {
return Ok(());
}
while !self.is_eof() {
if self.next_is(&['<', '!', '-', '-']) {
self.parse_comment();
continue;
}
if self.next_is(&['<', '/']) && self.parse_closing_tag()? {
continue;
}
if self.next_is(&['<']) && self.parse_opening_tag()? {
continue;
}
self.index += 1;
}
self.parse_remaining_as_text();
if let Some(last_item) = self.template.opened_items.last() {
Err(HtmlError::MissingClosingTag {
tag: last_item.name(),
})?
}
Ok(())
}
pub fn parse_remaining_as_text(&mut self) {
let text = &self.chars[self.last_item_end..self.index];
if !text.is_empty() {
self.template.push_item(Item::text(text));
}
}
pub fn parse_comment(&mut self) {
self.parse_remaining_as_text();
if let Some(end) = self.try_find(&['-', '-', '>']) {
self.template
.push_item(Item::comment(&self.chars[self.index + 4..end - 3]));
self.last_item_end = end;
self.index = end;
} else {
self.template
.push_item(Item::comment(&self.chars[self.index..]));
self.index = self.chars.len() - 1;
}
}
pub fn parse_opening_tag(&mut self) -> Result<bool, HtmlError> {
let mut name_chars = Vec::new();
for tag_logic in &self.tag_logics {
for name in tag_logic.names() {
name_chars.clear();
name_chars.push('<');
name_chars.extend(name.chars());
if self.next_is(&name_chars) {
if let Ok(mut item) = Item::try_from((*name, tag_logic.clone())) {
self.parse_remaining_as_text();
self.index += name_chars.len();
self.last_item_end = self.index;
let (attributes, is_self_closing) = self.parse_attributes()?;
item.push_attributes(attributes);
if is_self_closing {
self.template.push_item(item);
} else {
self.template.push_opened_item(item);
}
return Ok(true);
}
}
}
}
Ok(false)
}
pub fn parse_closing_tag(&mut self) -> Result<bool, HtmlError> {
let mut name_chars = Vec::new();
for tag_logic in &self.tag_logics {
for name in tag_logic.names() {
name_chars.clear();
name_chars.extend("</".chars());
name_chars.extend(name.chars());
name_chars.push('>');
if self.next_is(&name_chars) {
if self.template.opened_items.is_empty() {
Err(HtmlError::MissingOpeningTag {
tag: name.to_string(),
})?
}
if let Ok(item) = Item::try_from((*name, tag_logic.clone())) {
if let Some(last_item) = self.template.opened_items.last() {
if !last_item.matches(&item) {
Err(HtmlError::MissingClosingTag { tag: item.name() })?
}
}
}
self.parse_remaining_as_text();
self.index += name_chars.len();
self.last_item_end = self.index;
self.template.close_opened_item();
return Ok(true);
}
}
}
Ok(false)
}
pub fn parse_attributes(&mut self) -> Result<(Attributes, bool), HtmlError> {
let mut is_self_closing = false;
let mut attributes = Attributes::new();
let mut attribute_builder = AttributeBuilder::new();
while !self.is_eof() {
let Some(c1) = self.chars.get(self.index) else {
break;
};
if !attribute_builder.is_in_quote() {
if c1 == &'>' {
self.index += 1;
attribute_builder.pop_attribute_to(&mut attributes);
break;
} else if c1 == &'/' {
if let Some(c2) = self.chars.get(self.index + 1) {
if c2 == &'>' {
self.index += 2;
is_self_closing = true;
attribute_builder.pop_attribute_to(&mut attributes);
break;
}
}
}
}
match attribute_builder.state {
AttributeParseState::Key => {
match c1 {
' ' => {
if !attribute_builder.key.is_empty() {
attribute_builder.assign_state();
}
}
'=' => {
attribute_builder.value_state();
}
_ => {
attribute_builder.push_char_to_key(*c1);
}
};
}
AttributeParseState::Assign => 'this_case: {
if c1 == &' ' {
break 'this_case;
}
if c1 == &'=' {
attribute_builder.value_state();
break 'this_case;
}
attribute_builder
.pop_attribute_to(&mut attributes)
.key_state()
.push_char_to_key(*c1);
}
AttributeParseState::Value => 'this_case: {
if attribute_builder.value.is_empty() {
for quote in &['"', '\''] {
if c1 == quote {
attribute_builder.quoted_value_state(*quote);
break 'this_case;
}
}
}
if c1 == &' ' {
attribute_builder
.pop_attribute_to(&mut attributes)
.key_state();
break 'this_case;
}
attribute_builder.push_char_to_value(*c1);
}
AttributeParseState::QuotedValue(quote) => 'this_case: {
if c1 == "e {
attribute_builder
.pop_attribute_to(&mut attributes)
.key_state();
break 'this_case;
}
attribute_builder.push_char_to_value(*c1);
}
}
self.index += 1;
}
self.last_item_end = self.index;
Ok((attributes, is_self_closing))
}
pub fn process(&self, context: &mut TemplateContext) {
for item in &self.template.items {
if let Err(error) = item.run(context) {
error!("Could not write to file. Reason: {error}");
}
}
}
pub fn try_find(&self, text: &[char]) -> Option<usize> {
for i in self.index..self.chars.len() {
for (text_i, text_char) in text.iter().enumerate() {
if i + text_i >= self.chars.len() {
return None;
} else if &self.chars[i + text_i] != text_char {
break;
} else if text_i == text.len() - 1 {
return Some(i + text.len());
}
}
}
None
}
pub fn next_is(&self, text: &[char]) -> bool {
if let Some(peek) = self.peek(text.len()) {
if peek == text {
return true;
}
}
false
}
pub fn peek(&self, len: usize) -> Option<&[char]> {
if self.chars.len() >= self.index + len {
return Some(&self.chars[self.index..self.index + len]);
}
None
}
pub fn is_eof(&self) -> bool {
self.index >= self.chars.len()
}
}
impl Plugin for HtmlPlugin {
fn extensions(&self) -> &[&str] {
&["html", "htm"]
}
fn prepare(&mut self, context: &PluginContext) {
self.source_root_path = Some(context.source_root_path.clone());
self.output_root_path = Some(context.output_root_path.clone());
self.default_variables = context.default_variables.clone();
}
fn process_file_to(
&mut self,
mut source_file: File,
output_file: File,
) -> Result<(), Box<dyn Error>> {
self.parse_file(&mut source_file)?;
let Some(source_root_path) = &self.source_root_path else {
Err(HtmlError::MissingSourcePath)?
};
let Some(output_root_path) = &self.output_root_path else {
Err(HtmlError::MissingOutputPath)?
};
let mut template_context = TemplateContext {
input_path: source_root_path.clone(),
output_path: output_root_path.clone(),
file: output_file,
other_files: HashMap::new(),
variables: self.default_variables.clone(),
dependencies: Vec::new(),
};
self.process(&mut template_context);
self.dependencies = template_context.dependencies;
Ok(())
}
fn reset(&mut self) {
HtmlPlugin::reset(self);
}
fn get_dependencies(&mut self) -> Option<Vec<Rc<PathBuf>>> {
if self.dependencies.is_empty() {
None?
}
Some(self.dependencies.drain(..).collect())
}
}
impl TryFrom<&Path> for HtmlPlugin {
type Error = HtmlError;
fn try_from(path: &Path) -> Result<Self, Self::Error> {
let mut html_parser = HtmlPlugin::new();
html_parser.read_file(path)?;
Ok(html_parser)
}
}
impl Debug for HtmlPlugin {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("HtmlPlugin")
.field("chars", &self.chars)
.field("index", &self.index)
.field("last_item_end", &self.last_item_end)
.field("template", &self.template)
.field("source_root_path", &self.source_root_path)
.field("output_root_path", &self.output_root_path)
.field("default_variables", &self.default_variables)
.field("dependencies", &self.dependencies)
.finish()
}
}