Repository: yshavit/mdq (rust)

Issue:
Add a regex selector for paragraph text
It would be nice to select paragraph text lines by regex.

Pull request description:
add paragraph selector
resolves #140

Patch:
diff --git a/README.md b/README.md
index ca97626..ee1c1cf 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,14 @@ You can select...
   ```bash
   $ cat example.md | mdq '</> foo'  # find html tags containing "foo"
   ```
+  
+- Paragraphs
+
+  ```bash
+  $ cat example.md | mdq 'P: foo'  # find paragraphs containing "foo"
+  ```
+
+
 
 The `foo`s and `bar`s above can be:
 
diff --git a/src/select/api.rs b/src/select/api.rs
index e67823e..2d20bd3 100644
--- a/src/select/api.rs
+++ b/src/select/api.rs
@@ -8,6 +8,7 @@ use crate::select::sel_image::ImageSelector;
 use crate::select::sel_link::LinkSelector;
 use crate::select::sel_list_item::ListItemSelector;
 use crate::select::sel_list_item::ListItemType;
+use crate::select::sel_paragraph::ParagraphSelector;
 use crate::select::sel_section::SectionSelector;
 use crate::tree::{Formatting, Inline, Link, Text, TextVariant};
 use crate::tree_ref::{HtmlRef, ListItemRef, MdElemRef};
@@ -131,6 +132,8 @@ selectors![
         '-' => ListItemType::Unordered
     } ListItem,
 
+    {'P'} Paragraph,
+
     {'['} Link,
     ! {'['} Image,
 
@@ -341,6 +344,22 @@ mod test {
             expect_ok(mdq_ref_sel_parsed, MdqRefSelector::Html(item_parsed));
         }
 
+        #[test]
+        fn paragraph() {
+            let input = "P: foo";
+            let mdq_ref_sel_parsed = MdqRefSelector::parse_selector(&mut ParsingIterator::new(input));
+            let item_parsed = ParagraphSelector::read(&mut ParsingIterator::new(&input[1..])).unwrap();
+            expect_ok(mdq_ref_sel_parsed, MdqRefSelector::Paragraph(item_parsed));
+        }
+
+        #[test]
+        fn paragraph_no_matcher() {
+            let input = "P: |";
+            let mdq_ref_sel_parsed = MdqRefSelector::parse_selector(&mut ParsingIterator::new(input));
+            let item_parsed = ParagraphSelector::read(&mut ParsingIterator::new(&input[1..])).unwrap();
+            expect_ok(mdq_ref_sel_parsed, MdqRefSelector::Paragraph(item_parsed));
+        }
+
         #[test]
         fn unknown() {
             let input = "\u{2603}";
@@ -364,6 +383,7 @@ mod test {
             BlockQuote(_),
             CodeBlock(_),
             Html(_),
+            Paragraph(_),
         });
     }
 
diff --git a/src/select/mod.rs b/src/select/mod.rs
index 9bfe1a3..e498afb 100644
--- a/src/select/mod.rs
+++ b/src/select/mod.rs
@@ -6,6 +6,7 @@ mod sel_html;
 mod sel_image;
 mod sel_link;
 mod sel_list_item;
+mod sel_paragraph;
 mod sel_section;
 
 pub use api::*;
diff --git a/src/select/sel_paragraph.rs b/src/select/sel_paragraph.rs
new file mode 100644
index 0000000..7475676
--- /dev/null
+++ b/src/select/sel_paragraph.rs
@@ -0,0 +1,25 @@
+use crate::matcher::StringMatcher;
+use crate::parsing_iter::ParsingIterator;
+use crate::select::base::Selector;
+use crate::select::{ParseResult, SELECTOR_SEPARATOR};
+use crate::tree::Paragraph;
+
+#[derive(Debug, PartialEq)]
+pub struct ParagraphSelector {
+    matcher: StringMatcher,
+}
+
+impl ParagraphSelector {
+    pub fn read(iter: &mut ParsingIterator) -> ParseResult<Self> {
+        iter.require_char(':')?;
+        iter.require_whitespace_or(SELECTOR_SEPARATOR, "P:")?;
+        let matcher = StringMatcher::read(iter, SELECTOR_SEPARATOR)?;
+        Ok(Self { matcher })
+    }
+}
+
+impl<'a> Selector<'a, &'a Paragraph> for ParagraphSelector {
+    fn matches(&self, paragraph: &'a Paragraph) -> bool {
+        self.matcher.matches_inlines(&paragraph.body)
+    }
+}
diff --git a/src/tree_ref.rs b/src/tree_ref.rs
index 0a3aeae..cb30ac9 100644
--- a/src/tree_ref.rs
+++ b/src/tree_ref.rs
@@ -82,6 +82,12 @@ impl<'a> From<&'a Link> for MdElemRef<'a> {
     }
 }
 
+impl<'a> From<&'a Paragraph> for MdElemRef<'a> {
+    fn from(value: &'a Paragraph) -> Self {
+        MdElemRef::Paragraph(value)
+    }
+}
+
 impl<'a> From<&'a Section> for MdElemRef<'a> {
     fn from(value: &'a Section) -> Self {
         MdElemRef::Section(value)


Test patch:
diff --git a/tests/md_cases/select_paragraphs.toml b/tests/md_cases/select_paragraphs.toml
new file mode 100644
index 0000000..4a6c6dc
--- /dev/null
+++ b/tests/md_cases/select_paragraphs.toml
@@ -0,0 +1,67 @@
+[given]
+md = '''
+Hello, world.
+
+> Paragraph within a block quote.
+
+This paragraph has _inline_ **formatting**.
+'''
+
+
+[expect."all"]
+cli_args = ["P:"]
+output = '''
+Hello, world.
+
+   -----
+
+Paragraph within a block quote.
+
+   -----
+
+This paragraph has _inline_ **formatting**.
+'''
+
+
+[expect."all but with explicit all-matcher"]
+cli_args = ["P: *"]
+output = '''
+Hello, world.
+
+   -----
+
+Paragraph within a block quote.
+
+   -----
+
+This paragraph has _inline_ **formatting**.
+'''
+
+
+[expect."select within a block quote"]
+cli_args = ["P: block"]
+output = '''
+Paragraph within a block quote.
+'''
+
+
+[expect."matcher ignores inline formatting"]
+cli_args = ["P: has inline"]
+# The markdown is "has _inline_", but the emphasis formatting is ignored for matching. It's still used for output.
+output = '''
+This paragraph has _inline_ **formatting**.
+'''
+
+
+[expect."no colon after p"]
+cli_args = ["P *"]
+expect_success = false
+output = '''
+'''
+
+
+[expect."space before colon"]
+cli_args = ["P : *"]
+expect_success = false
+output = '''
+'''