Skip to content

feat: enhance indentation handling #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: feat-init-lsp
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/parser/attribute/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,31 +369,31 @@ mod tests {
#[test]
fn it_should_process_attribute_with_multiline_value() {
let input = r#"class="{
'is-active': isActive,
'is-disabled': isDisabled,
'is-active': isActive,
'is-disabled': isDisabled,
}"
:key="item.id""#;
:key="item.id""#;

let (rest, attribute) = process_attribute(
input,
&mut HsmlProcessContext {
indent_level: 1,
indent_string: Some(String::from(" ")),
nested_tag_level: 1,
indent_string: String::from(" "),
},
)
.unwrap();

assert_eq!(
attribute,
r#"class="{
'is-active': isActive,
'is-disabled': isDisabled,
'is-active': isActive,
'is-disabled': isDisabled,
}""#
);
assert_eq!(
rest,
r#"
:key="item.id""#
:key="item.id""#
);
}

Expand Down
10 changes: 8 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ pub enum HsmlNode {

#[derive(Debug, Default)]
pub struct HsmlProcessContext {
pub indent_level: usize,
pub indent_string: Option<String>,
// TODO @Shinigami92 2025-03-16: Currently nested_tag_level is not used, but should be later to allow mixed spaces and tabs in indentation
/// The tracked nested tag level
pub nested_tag_level: usize,

/// The tracked indentation string
///
/// Can be a combination of spaces and tabs
pub indent_string: String,
}

pub fn process_newline(input: &str) -> IResult<&str, &str> {
Expand Down
12 changes: 11 additions & 1 deletion src/parser/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,19 @@ pub fn parse(input: &str) -> IResult<&str, RootNode> {

loop {
// eat leading and trailing newlines and whitespace if there are any
if let Ok((rest, _)) =
if let Ok((rest, taken)) =
take_till::<_, &str, nom::error::Error<&str>>(|c: char| !c.is_whitespace())(input)
{
// take the leading spaces and tabs after the last newline as indentation
context.indent_string = taken
.chars()
.rev()
.take_while(|c| c.is_whitespace() && *c != '\n')
.collect::<String>()
.chars()
.rev()
.collect();

input = rest;

if input.is_empty() {
Expand Down
34 changes: 13 additions & 21 deletions src/parser/tag/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,34 +120,25 @@ pub fn tag_node<'a>(input: &'a str, context: &mut HsmlProcessContext) -> IResult

if !indentation.is_empty() {
// check that the indentation is consistent and does not include tabs and spaces at the same time
// if it does, throw an error
// if it does, collect an error for diagnostics

if indentation.contains('\t') && indentation.contains(' ') {
// TODO @Shinigami92 2023-05-18: This error could be more specific
return Err(nom::Err::Error(Error::new(input, ErrorKind::Tag)));
}

// if we never hit an indentation yet, set it
// this only happens once
if context.indent_string.is_none() {
// println!("set indent string = \"{}\"", indentation);
context.indent_string = Some(indentation.to_string());
// TODO @Shinigami92 2025-03-16: This should collect an error or diagnostics
}

// persist the indentation level so we can restore it later
let indentation_level = context.indent_level;

context.indent_level += 1;
let nested_tag_level = context.nested_tag_level;
let indent_string = context.indent_string.clone();

// check that we are at the correct indentation level, otherwise break out of the loop
let indent_string_len = context.indent_string.as_ref().unwrap().len();
let indent_size = indent_string_len * context.indent_level;
// dbg!(indent_size, indentation.len());
if indent_size != indentation.len() {
if indentation.len() <= context.indent_string.len() {
// dbg!("break out of loop");
break;
}

context.nested_tag_level += 1;
context.indent_string = indentation.to_string();

// we are at the correct indentation level, so we can continue parsing the child tag nodes

// there could be a comment (dev or native) node
Expand All @@ -169,8 +160,9 @@ pub fn tag_node<'a>(input: &'a str, context: &mut HsmlProcessContext) -> IResult
}
}

// restore the indentation level
context.indent_level = indentation_level;
// restore the nested_tag_level level
context.nested_tag_level = nested_tag_level;
context.indent_string = indent_string;

continue;
}
Expand Down Expand Up @@ -207,8 +199,8 @@ mod tests {
#[test]
fn it_should_return_tag_node_with_piped_text() {
let context = &mut HsmlProcessContext {
indent_level: 3,
indent_string: Some(String::from(" ")),
nested_tag_level: 3,
indent_string: String::from(" "),
};

let (input, tag) = tag_node(
Expand Down
48 changes: 36 additions & 12 deletions src/parser/text/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,12 @@ pub fn text_block_node<'a>(
) -> IResult<&'a str, TextNode> {
let (input, text) = process_text_block(input, context)?;

let indent_string = context
.indent_string
.as_ref()
.unwrap()
.repeat(context.indent_level + 1);

let newline_indent_replacement: &str = &format!("\n{}", &indent_string);

// On every line, replace all leading spaces and tabs with an empty string
let text = text
.trim_start_matches(&indent_string)
.replace(newline_indent_replacement, "\n");
.lines()
.map(|line| line.trim_start())
.collect::<Vec<&str>>()
.join("\n");

Ok((input, TextNode { text }))
}
Expand All @@ -51,8 +46,8 @@ mod tests {
#[test]
fn it_should_return_text_block_node() {
let context = &mut HsmlProcessContext {
indent_string: Some(String::from(" ")),
indent_level: 3,
nested_tag_level: 3,
indent_string: String::from(" "),
};

let (input, text_block) = text_block_node(
Expand All @@ -78,4 +73,33 @@ and the build size is tiny.""#

assert_eq!(input, "\n figcaption.font-medium");
}

#[test]
fn it_should_stop_before_next_tag_node() {
let context = &mut HsmlProcessContext {
nested_tag_level: 1,
indent_string: String::from(" "),
};

let (input, text_block) = text_block_node(
r#".
Sarah Dayan
.text-[#af05c9].dark:text-slate-500.
Staff Engineer, Algolia"#,
context,
)
.unwrap();

assert_eq!(
text_block,
TextNode {
text: String::from(r#"Sarah Dayan"#),
}
);

assert_eq!(
input,
"\n .text-[#af05c9].dark:text-slate-500.\n Staff Engineer, Algolia"
);
}
}
32 changes: 16 additions & 16 deletions src/parser/text/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,9 @@ pub fn process_text_block<'a>(
// eat one \r\n or \n
let (rest, _) = alt((tag("\r\n"), tag("\n"))).parse(rest)?;

let indent_string: &str = if let Some(indent_string) = &context.indent_string {
indent_string
} else {
" "
};

let indent_string: &str = &indent_string.repeat(context.indent_level + 1);

let mut text_block_index = 0;

// loop over each line until we find a line that does not fulfill the indentation
// loop over each line until we find a line that does not starts with the current indent string
for (index, c) in rest.chars().enumerate() {
if c == '\n' {
// if next char is also a \n, then continue
Expand All @@ -38,7 +30,15 @@ pub fn process_text_block<'a>(
let line = &rest[index + 1..];

// otherwise check the indentation and if it does not fulfill the indentation, then break
if !line.starts_with(indent_string) {
// TODO @Shinigami92 2025-03-16: right now this does not support mixed indentations on tag level indentation, but only withing the text block
if !line.starts_with(&context.indent_string) {
break;
}

let line = &line[context.indent_string.len()..];

// break out if the first character is not a space or tab
if !line.starts_with(' ') && !line.starts_with('\t') {
break;
}
} else {
Expand Down Expand Up @@ -69,26 +69,26 @@ mod tests {
#[test]
fn it_should_process_text_block() {
let mut context = HsmlProcessContext {
indent_string: Some(String::from(" ")),
indent_level: 1,
nested_tag_level: 1,
indent_string: String::from(" "),
};

let input = r#".
this is just some text
this is just some text
it can be multiline

and also contain blank lines
and also contain blank lines
span other text
"#;

let (rest, text_block) = process_text_block(input, &mut context).unwrap();

assert_eq!(
text_block,
r#" this is just some text
r#" this is just some text
it can be multiline

and also contain blank lines"#
and also contain blank lines"#
);
assert_eq!(
rest,
Expand Down