>(needle: S) -> impl Matcher {
250 | text(move |text| text.contains(needle.as_ref()))
251 | }
252 |
253 | /// Match a [`Event::Text`] node using an arbitrary predicate.
254 | pub fn text(mut predicate: P) -> impl Matcher
255 | where
256 | P: FnMut(&str) -> bool,
257 | {
258 | move |ev: &Event<'_>| match ev {
259 | Event::Text(text) => predicate(text.as_ref()),
260 | _ => false,
261 | }
262 | }
263 |
264 | /// Matches the start of a link who's URL contains a certain string.
265 | ///
266 | /// # Examples
267 | ///
268 | /// ```rust
269 | /// # use markedit::Matcher;
270 | /// use pulldown_cmark::{Event, Tag};
271 | ///
272 | /// let src = "Some text containing [a link to google](https://google.com/).";
273 | /// let mut matcher = markedit::link_with_url_containing("google.com");
274 | ///
275 | /// let events: Vec<_> = markedit::parse(src).collect();
276 | ///
277 | /// let ix = matcher.first_match(&events).unwrap();
278 | ///
279 | /// match &events[ix] {
280 | /// Event::Start(Tag::Link(_, url, _)) => assert_eq!(url.as_ref(), "https://google.com/"),
281 | /// _ => unreachable!(),
282 | /// }
283 | /// ```
284 | pub fn link_with_url_containing>(needle: S) -> impl Matcher {
285 | move |ev: &Event<'_>| match ev {
286 | Event::Start(Tag::Link(_, link, _)) => {
287 | link.as_ref().contains(needle.as_ref())
288 | },
289 | _ => false,
290 | }
291 | }
292 |
293 | /// A glorified `&mut Matcher`.
294 | ///
295 | /// This is the return value for [`Matcher::by_ref()`], you won't normally use
296 | /// it directly.
297 | #[derive(Debug)]
298 | pub struct Ref<'a, M>(&'a mut M);
299 |
300 | impl<'a, M> Matcher for Ref<'a, M>
301 | where
302 | M: Matcher,
303 | {
304 | fn matches_event(&mut self, event: &Event<'_>) -> bool {
305 | self.0.matches_event(event)
306 | }
307 | }
308 |
--------------------------------------------------------------------------------
/src/matchers/one_shot.rs:
--------------------------------------------------------------------------------
1 | use crate::matchers::Matcher;
2 | use pulldown_cmark::Event;
3 |
4 | /// A [`Matcher`] which will only ever return `true` once.
5 | #[derive(Debug, Clone, PartialEq)]
6 | pub struct OneShot {
7 | inner: M,
8 | already_triggered: bool,
9 | }
10 |
11 | impl OneShot {
12 | /// Create a [`OneShot`] matcher.
13 | pub const fn new(inner: M) -> Self {
14 | OneShot {
15 | inner,
16 | already_triggered: false,
17 | }
18 | }
19 | }
20 |
21 | impl Matcher for OneShot {
22 | fn matches_event(&mut self, event: &Event<'_>) -> bool {
23 | if self.already_triggered {
24 | return false;
25 | }
26 |
27 | let got = self.inner.matches_event(event);
28 |
29 | if got {
30 | self.already_triggered = true;
31 | }
32 |
33 | got
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/matchers/start_of_next_line.rs:
--------------------------------------------------------------------------------
1 | use crate::matchers::Matcher;
2 | use pulldown_cmark::Event;
3 |
4 | /// A [`Matcher`] which will match the start of the next top-level element after
5 | /// some inner [`Matcher`] matches.
6 | #[derive(Debug, Clone, PartialEq)]
7 | pub struct StartOfNextLine {
8 | inner: M,
9 | state: State,
10 | current_nesting_level: usize,
11 | }
12 |
13 | impl StartOfNextLine {
14 | /// Create a new [`StartOfNextLine`] matcher.
15 | pub const fn new(inner: M) -> Self {
16 | StartOfNextLine {
17 | inner,
18 | state: State::WaitingForFirstMatch,
19 | current_nesting_level: 0,
20 | }
21 | }
22 |
23 | fn update_nesting(&mut self, event: &Event<'_>) {
24 | match event {
25 | Event::Start(_) => self.current_nesting_level += 1,
26 | Event::End(_) => self.current_nesting_level -= 1,
27 | _ => {},
28 | }
29 | }
30 | }
31 |
32 | impl StartOfNextLine {
33 | fn process_with_inner(&mut self, event: &Event<'_>) {
34 | if self.inner.matches_event(event) {
35 | self.state = State::LookingForLastEndTag;
36 | }
37 | }
38 | }
39 |
40 | impl Matcher for StartOfNextLine {
41 | fn matches_event(&mut self, event: &Event<'_>) -> bool {
42 | self.update_nesting(event);
43 |
44 | match self.state {
45 | State::WaitingForFirstMatch => {
46 | self.process_with_inner(event);
47 | },
48 | State::LookingForLastEndTag => {
49 | if self.current_nesting_level == 0 {
50 | self.state = State::FoundLastEndTag;
51 | }
52 | },
53 | State::FoundLastEndTag => {
54 | self.state = State::WaitingForFirstMatch;
55 | return true;
56 | },
57 | }
58 |
59 | false
60 | }
61 | }
62 |
63 | #[derive(Debug, Copy, Clone, PartialEq)]
64 | enum State {
65 | WaitingForFirstMatch,
66 | LookingForLastEndTag,
67 | FoundLastEndTag,
68 | }
69 |
70 | #[cfg(test)]
71 | mod tests {
72 | use super::*;
73 | use pulldown_cmark::Parser;
74 |
75 | #[test]
76 | fn match_start_of_line_after_heading() {
77 | let src = "# Heading \nSome Text";
78 | let events: Vec<_> = Parser::new(src).collect();
79 | let mut matcher = StartOfNextLine::new(crate::exact_text("Heading"));
80 |
81 | let got = matcher.by_ref().first_match(events).unwrap();
82 |
83 | assert_eq!(got, 3);
84 | // we're on the first start tag, so at nesting level 1
85 | assert_eq!(matcher.current_nesting_level, 1);
86 | assert_eq!(matcher.state, State::WaitingForFirstMatch);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/rewriters/mod.rs:
--------------------------------------------------------------------------------
1 | mod rewritten;
2 | mod writer;
3 |
4 | pub use rewritten::{rewrite, Rewritten};
5 | pub use writer::Writer;
6 |
7 | use crate::Matcher;
8 | use pulldown_cmark::{CodeBlockKind, CowStr, Event, Tag};
9 |
10 | /// Something which can rewrite events.
11 | pub trait Rewriter<'src> {
12 | /// Process a single [`Event`].
13 | ///
14 | /// This may mean ignoring it, mutating it, or adding new events to the
15 | /// [`Writer`]'s buffer.
16 | ///
17 | /// The [`Writer`] is used as a temporary buffer that will then be streamed
18 | /// to the user via [`rewrite()`].
19 | fn rewrite_event(&mut self, event: Event<'src>, writer: &mut Writer<'src>);
20 |
21 | /// Use this [`Rewriter`] to rewrite a stream of [`Event`]s.
22 | fn rewrite(self, events: E) -> Rewritten<'src, E, Self>
23 | where
24 | Self: Sized,
25 | E: IntoIterator- >,
26 | {
27 | Rewritten::new(events, self)
28 | }
29 | }
30 |
31 | impl<'src, F> Rewriter<'src> for F
32 | where
33 | F: FnMut(Event<'src>, &mut Writer<'src>),
34 | {
35 | fn rewrite_event(&mut self, event: Event<'src>, writer: &mut Writer<'src>) {
36 | self(event, writer);
37 | }
38 | }
39 |
40 | /// Inserts some markdown text before whatever is matched by the [`Matcher`].
41 | ///
42 | /// # Examples
43 | ///
44 | /// ```rust
45 | /// use markedit::Matcher;
46 | /// let src = "# Heading\nsome text\n";
47 | ///
48 | /// let first_line_after_heading = markedit::exact_text("Heading")
49 | /// .falling_edge();
50 | /// let rewriter = markedit::insert_markdown_before(
51 | /// "## Second Heading",
52 | /// first_line_after_heading,
53 | /// );
54 | ///
55 | /// let events = markedit::parse(src);
56 | /// let rewritten: Vec<_> = markedit::rewrite(events, rewriter).collect();
57 | ///
58 | /// // if everything went to plan, the output should contain "Second Heading"
59 | /// assert!(markedit::exact_text("Second Heading").is_in(&rewritten));
60 | /// ```
61 | pub fn insert_markdown_before<'src, M, S>(
62 | markdown_text: S,
63 | matcher: M,
64 | ) -> impl Rewriter<'src> + 'src
65 | where
66 | M: Matcher + 'src,
67 | S: AsRef + 'src,
68 | {
69 | let events = crate::parse(markdown_text.as_ref())
70 | .map(owned_event)
71 | .collect();
72 | insert_before(events, matcher)
73 | }
74 |
75 | /// Splice some events into the resulting event stream before every match.
76 | pub fn insert_before<'src, M>(
77 | to_insert: Vec>,
78 | mut matcher: M,
79 | ) -> impl Rewriter<'src> + 'src
80 | where
81 | M: Matcher + 'src,
82 | {
83 | move |ev: Event<'src>, writer: &mut Writer<'src>| {
84 | if matcher.matches_event(&ev) {
85 | writer.extend(to_insert.iter().cloned());
86 | }
87 | writer.push(ev);
88 | }
89 | }
90 |
91 | /// A [`Rewriter`] which lets you update a [`Event::Text`] node based on some
92 | /// predicate.
93 | pub fn change_text<'src, M, F, S>(
94 | mut predicate: M,
95 | mut mutator: F,
96 | ) -> impl Rewriter<'src> + 'src
97 | where
98 | M: FnMut(&str) -> bool + 'src,
99 | F: FnMut(CowStr<'src>) -> S + 'src,
100 | S: Into>,
101 | {
102 | move |ev: Event<'src>, writer: &mut Writer<'src>| match ev {
103 | Event::Text(text) => {
104 | let text = if predicate(text.as_ref()) {
105 | mutator(text).into()
106 | } else {
107 | text
108 | };
109 | writer.push(Event::Text(text));
110 | },
111 | _ => writer.push(ev),
112 | }
113 | }
114 |
115 | fn owned_event(ev: Event<'_>) -> Event<'static> {
116 | match ev {
117 | Event::Start(tag) => Event::Start(owned_tag(tag)),
118 | Event::End(tag) => Event::End(owned_tag(tag)),
119 | Event::Text(s) => Event::Text(owned_cow_str(s)),
120 | Event::Code(s) => Event::Code(owned_cow_str(s)),
121 | Event::Html(s) => Event::Html(owned_cow_str(s)),
122 | Event::FootnoteReference(s) => {
123 | Event::FootnoteReference(owned_cow_str(s))
124 | },
125 | Event::SoftBreak => Event::SoftBreak,
126 | Event::HardBreak => Event::HardBreak,
127 | Event::Rule => Event::Rule,
128 | Event::TaskListMarker(t) => Event::TaskListMarker(t),
129 | }
130 | }
131 |
132 | fn owned_cow_str(s: CowStr<'_>) -> CowStr<'static> {
133 | match s {
134 | CowStr::Borrowed(_) => CowStr::from(s.into_string()),
135 | CowStr::Boxed(boxed) => CowStr::Boxed(boxed),
136 | CowStr::Inlined(inlined) => CowStr::Inlined(inlined),
137 | }
138 | }
139 |
140 | fn owned_tag(tag: Tag<'_>) -> Tag<'static> {
141 | match tag {
142 | Tag::Paragraph => Tag::Paragraph,
143 | Tag::Heading(h) => Tag::Heading(h),
144 | Tag::BlockQuote => Tag::BlockQuote,
145 | Tag::CodeBlock(CodeBlockKind::Indented) => {
146 | Tag::CodeBlock(CodeBlockKind::Indented)
147 | },
148 | Tag::CodeBlock(CodeBlockKind::Fenced(s)) => {
149 | Tag::CodeBlock(CodeBlockKind::Fenced(owned_cow_str(s)))
150 | },
151 | Tag::List(u) => Tag::List(u),
152 | Tag::Item => Tag::Item,
153 | Tag::FootnoteDefinition(s) => Tag::FootnoteDefinition(owned_cow_str(s)),
154 | Tag::Table(alignment) => Tag::Table(alignment),
155 | Tag::TableHead => Tag::TableHead,
156 | Tag::TableRow => Tag::TableRow,
157 | Tag::TableCell => Tag::TableCell,
158 | Tag::Emphasis => Tag::Emphasis,
159 | Tag::Strong => Tag::Strong,
160 | Tag::Strikethrough => Tag::Strikethrough,
161 | Tag::Link(t, url, title) => {
162 | Tag::Link(t, owned_cow_str(url), owned_cow_str(title))
163 | },
164 | Tag::Image(t, url, alt) => {
165 | Tag::Image(t, owned_cow_str(url), owned_cow_str(alt))
166 | },
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/rewriters/rewritten.rs:
--------------------------------------------------------------------------------
1 | use crate::{Rewriter, Writer};
2 | use pulldown_cmark::Event;
3 |
4 | /// The whole point.
5 | ///
6 | /// This function takes a stream of [`Event`]s and a [`Rewriter`], and gives
7 | /// you a stream of rewritten [`Event`]s.
8 | pub fn rewrite<'src, E, R>(
9 | events: E,
10 | rewriter: R,
11 | ) -> impl Iterator
- > + 'src
12 | where
13 | E: IntoIterator
- >,
14 | E::IntoIter: 'src,
15 | R: Rewriter<'src> + 'src,
16 | {
17 | Rewritten::new(events.into_iter(), rewriter)
18 | }
19 |
20 | /// A stream of [`Event`]s that have been modified by a [`Rewriter`].
21 | #[derive(Debug)]
22 | pub struct Rewritten<'src, E, R> {
23 | events: E,
24 | rewriter: R,
25 | writer: Writer<'src>,
26 | }
27 |
28 | impl<'src, E, R> Rewritten<'src, E, R> {
29 | /// Create a new [`Rewritten`] iterator.
30 | pub fn new(events: E, rewriter: R) -> Self {
31 | Rewritten {
32 | rewriter,
33 | events,
34 | writer: Writer::new(),
35 | }
36 | }
37 | }
38 |
39 | impl<'src, E, R> Iterator for Rewritten<'src, E, R>
40 | where
41 | E: Iterator
- >,
42 | R: Rewriter<'src>,
43 | {
44 | type Item = Event<'src>;
45 |
46 | fn next(&mut self) -> Option {
47 | loop {
48 | // we're still working through items buffered by the rewriter
49 | if let Some(ev) = self.writer.buffer.pop_front() {
50 | return Some(ev);
51 | }
52 |
53 | // we need to pop another event and process it
54 | let event = self.events.next()?;
55 | self.rewriter.rewrite_event(event, &mut self.writer);
56 | }
57 | }
58 | }
59 |
60 | #[cfg(test)]
61 | mod tests {
62 | use super::*;
63 |
64 | use pulldown_cmark::Tag;
65 |
66 | #[test]
67 | fn ignore_some_events() {
68 | let events = vec![
69 | Event::Start(Tag::Paragraph),
70 | Event::Text("This is some text.".into()),
71 | Event::Start(Tag::Heading(2)),
72 | Event::Text("This is some more text.".into()),
73 | ];
74 |
75 | let rewritten: Vec> = rewrite(
76 | events,
77 | |event: Event<'static>, writer: &mut Writer<'static>| {
78 | if let event @ Event::Text(_) = event {
79 | writer.push(event);
80 | }
81 | },
82 | )
83 | .collect();
84 |
85 | assert_eq!(
86 | rewritten,
87 | vec![
88 | Event::Text("This is some text.".into()),
89 | Event::Text("This is some more text.".into()),
90 | ]
91 | );
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/rewriters/writer.rs:
--------------------------------------------------------------------------------
1 | use pulldown_cmark::Event;
2 | use std::collections::VecDeque;
3 |
4 | #[allow(unused_imports)] // for rustdoc
5 | use crate::Rewriter;
6 |
7 | /// The output buffer given to [`Rewriter::rewrite_event()`].
8 | #[derive(Debug)]
9 | pub struct Writer<'a> {
10 | pub(crate) buffer: VecDeque>,
11 | }
12 |
13 | impl<'a> Writer<'a> {
14 | pub(crate) fn new() -> Writer<'a> {
15 | Writer {
16 | buffer: VecDeque::new(),
17 | }
18 | }
19 |
20 | /// Queue an [`Event`] to be emitted.
21 | pub fn push(&mut self, event: Event<'a>) { self.buffer.push_back(event); }
22 | }
23 |
24 | impl<'a> Extend> for Writer<'a> {
25 | fn extend>>(&mut self, iter: I) {
26 | self.buffer.extend(iter);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------