Skip to content

Commit

Permalink
Implement REPLACE (alias: MAP)
Browse files Browse the repository at this point in the history
replaces the elements matching an CSS selector with the result of a sub-pipeline
  • Loading branch information
kelko committed Sep 28, 2022
1 parent b5cd282 commit d7f46f6
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 11 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "html-streaming-editor"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
authors = [":kelko: <kelko@me.com>"]
repository = "https://github.com/kelko/html-streaming-editor"
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ Currently supported:
- `SET-TEXT-CONTENT`: removes previous children and replaces it with exactly one given text child
- `ADD-TEXT-CONTENT`: appends a new text child
- `ADD-COMMENT`: appends a new comment child
- `CREATE-ELEMENT`: creates a new, empty element, mainly in combination with `ADD-ELEMENT` or `REPLACE-WITH` (alias: `NEW`)
- `ADD-ELEMENT`: appends a new tag/element child
- `REPLACE`: replace all elements matching a CSS selector with new elements (alias: `MAP`)
- `CREATE-ELEMENT`: creates a new, empty element, mainly in combination with `ADD-ELEMENT` or `REPLACE-WITH` (alias: `NEW`)


Planned commands:

- `REPLACE-WITH`: replace all elements matching a CSS selector with new elements (alias: `MAP`)
- `READ-FROM`: reads a DOM from a different file, mainly in combination with `ADD-ELEMENT` or `REPLACE-WITH` (alias: `SOURCE`)


Binary
-------

Expand Down
2 changes: 1 addition & 1 deletion src/bin/hse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fn main() {
pretty_env_logger::init();

let options = clap_app!(hse =>
(version: "0.2.0")
(version: "0.3.0")
(author: ":kelko:")
(about: "Html Streaming Editor")
(@arg input: -i --input +takes_value "File name of the Input. `-` for stdin (default)")
Expand Down
23 changes: 23 additions & 0 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ pub enum Command<'a> {
/// runs a sub-pipeline on each element matching the given CSS selector
/// Returns the input as result.
ForEach(CssSelectorList<'a>, Pipeline<'a>),
/// runs a sub-pipeline and replaces each element matching the given CSS selector with the result of the pipeline
/// Returns the input as result.
Replace(CssSelectorList<'a>, Pipeline<'a>),
/// Remove the given attribute from all currently selected nodes
/// Returns the input as result.
ClearAttribute(String),
Expand Down Expand Up @@ -107,6 +110,7 @@ impl<'a> Command<'a> {
Command::ForEach(selector, pipeline) => Self::for_each(input, selector, pipeline),
Command::AddElement(pipeline) => Self::add_element(input, pipeline),
Command::CreateElement(element_name) => Self::create_element(element_name),
Command::Replace(selector, pipeline) => Self::replace(input, selector, pipeline),
}
}

Expand Down Expand Up @@ -143,6 +147,25 @@ impl<'a> Command<'a> {
Ok(input.clone())
}

fn replace(
input: &Vec<rctree::Node<HtmlContent>>,
selector: &CssSelectorList<'a>,
pipeline: &Pipeline,
) -> Result<Vec<rctree::Node<HtmlContent>>, CommandError> {
let queried_elements = selector.query(input);
let mut created_elements = pipeline.run_on(vec![]).context(SubpipelineFailedSnafu)?;

for mut element_for_replacement in queried_elements {
for new_element in &mut created_elements {
let copy = new_element.make_deep_copy();
element_for_replacement.insert_after(copy);
}
element_for_replacement.detach();
}

Ok(input.clone())
}

fn clear_attr(
input: &Vec<rctree::Node<HtmlContent>>,
attribute: &String,
Expand Down
4 changes: 2 additions & 2 deletions src/html/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const HTML_VOID_ELEMENTS: [&str; 16] = [
"meta", "param", "source", "track", "wbr",
];

#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct HtmlTag {
pub name: String,
pub attributes: BTreeMap<String, String>,
Expand Down Expand Up @@ -95,7 +95,7 @@ impl HtmlTag {
}
}

#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub(crate) enum HtmlContent {
Tag(HtmlTag),
Text(String),
Expand Down
3 changes: 3 additions & 0 deletions src/parsing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ parser! {
= ("WITHOUT" / "FILTER") "{" whitespace()? oc:css_selector_list() whitespace()? "}" { Command::Without(oc) }
rule for_each_command() -> Command<'input>
= "FOR-EACH{" whitespace()? oc:css_selector_list() whitespace()? iterate_marker() whitespace()? sp:pipeline() whitespace()? "}" { Command::ForEach(oc, sp) }
rule replace_command() -> Command<'input>
= ("REPLACE"/"MAP") "{" whitespace()? oc:css_selector_list() whitespace()? assign_marker() whitespace()? sp:element_creating_pipeline() whitespace()? "}" { Command::Replace(oc, sp)}
rule clear_attr_command() -> Command<'input>
= "CLEAR-ATTR{" whitespace()? a:identifier() whitespace()? "}" { Command::ClearAttribute(String::from(a)) }
rule clear_content_command() -> Command<'input>
Expand Down Expand Up @@ -117,6 +119,7 @@ parser! {
/ add_text_content_command()
/ add_comment_command()
/ add_element_command()
/ replace_command()
rule element_source_command() -> Command<'input>
= create_element_command()
rule element_manipulating_pipeline() -> Vec<Command<'input>>
Expand Down
38 changes: 34 additions & 4 deletions src/parsing/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ fn parse_single_add_element_using_new_alias() {
parsed,
Ok(Command::AddElement(Pipeline::new(vec![
Command::CreateElement(String::from("div"))
]),))
])))
);
}

Expand All @@ -307,7 +307,7 @@ fn parse_single_add_element_using_create() {
parsed,
Ok(Command::AddElement(Pipeline::new(vec![
Command::CreateElement(String::from("div"))
]),))
])))
);
}

Expand All @@ -318,7 +318,7 @@ fn parse_single_add_element_with_arrow_using_create() {
parsed,
Ok(Command::AddElement(Pipeline::new(vec![
Command::CreateElement(String::from("div"))
]),))
])))
);
}

Expand All @@ -329,6 +329,36 @@ fn parse_single_add_element_with_ascii_arrow_using_create() {
parsed,
Ok(Command::AddElement(Pipeline::new(vec![
Command::CreateElement(String::from("div"))
]),))
])))
);
}

//noinspection DuplicatedCode
#[test]
fn parse_single_replace_using_create() {
let parsed = super::grammar::command("REPLACE{.replace-me ↤ CREATE-ELEMENT{p} }");
assert_eq!(
parsed,
Ok(Command::Replace(
CssSelectorList::new(vec![CssSelectorPath::single(CssSelector::for_class(
"replace-me"
))]),
Pipeline::new(vec![Command::CreateElement(String::from("p"))])
)),
);
}

//noinspection DuplicatedCode
#[test]
fn parse_single_replace_with_ascii_arrow_using_create() {
let parsed = super::grammar::command("REPLACE{.replace-me <= CREATE-ELEMENT{p} }");
assert_eq!(
parsed,
Ok(Command::Replace(
CssSelectorList::new(vec![CssSelectorPath::single(CssSelector::for_class(
"replace-me"
))]),
Pipeline::new(vec![Command::CreateElement(String::from("p"))])
)),
);
}
29 changes: 29 additions & 0 deletions src/pipeline/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,32 @@ fn run_on_single_add_element_from_create_for_tag() {
String::from(r#"<div class="bar" data-test="foo">Some Content<div></div></div>"#)
);
}

//noinspection DuplicatedCode
#[test]
fn run_on_single_replace_from_create() {
let pipeline = Pipeline::new(vec![Command::Replace(
CssSelectorList::new(vec![CssSelectorPath::single(CssSelector::for_class(
"replace-me",
))]),
Pipeline::new(vec![Command::CreateElement(String::from("p"))]),
)]);

let dom = tl::parse(
r#"<body><div class="replace-me">Some Content</div><div class="stay">This will be kept</div></body>"#,
tl::ParserOptions::default(),
)
.unwrap();
let starting_elements = HtmlContent::import(dom).unwrap();

let mut result = pipeline
.run_on(vec![rctree::Node::clone(&starting_elements)])
.unwrap();

assert_eq!(result.len(), 1);
let first_result = result.pop().unwrap();
assert_eq!(
first_result.outer_html(),
String::from(r#"<body><p></p><div class="stay">This will be kept</div></body>"#)
);
}
46 changes: 46 additions & 0 deletions tests/replace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use html_streaming_editor::*;

const HTML_INPUT: &str = r#"<html>
<head></head>
<body>
<h1>Title</h1>
<p id="first-para">Some first text</p>
<p id="second-para">Some more text, even with an <img src=""></p>
<p id="third-para">Third text of <abbr>HTML</abbr>, but no <abbr>CSS</abbr></p>
<ul id="list">
<li id="item-1">1</li>
<li id="item-2">2</li>
<li id="item-3">3</li>
</ul>
</body>
</html>"#;

#[test]
fn replace_ul_with_created_div() -> Result<(), StreamingEditorError> {
let command = "REPLACE{ul ↤ CREATE-ELEMENT{div} | SET-TEXT-CONTENT{'this was an UL'} | SET-ATTR{id ↤ 'new'}}";

let mut input = Box::new(HTML_INPUT.as_bytes());
let mut output = Vec::new();
let hse = HtmlStreamingEditor::new(&mut input, &mut output);

let _ = hse.run(command)?;
let result_string = String::from_utf8(output).unwrap();

assert_eq!(
result_string,
String::from(
r#"<html>
<head></head>
<body>
<h1>Title</h1>
<p id="first-para">Some first text</p>
<p id="second-para">Some more text, even with an <img src=""></p>
<p id="third-para">Third text of <abbr>HTML</abbr>, but no <abbr>CSS</abbr></p>
<div id="new">this was an UL</div>
</body>
</html>"#
)
);

Ok(())
}

0 comments on commit d7f46f6

Please sign in to comment.