Add support for hashtags in md parser
This commit is contained in:
		
							parent
							
								
									a6e73f4667
								
							
						
					
					
						commit
						4fa3a0f6ee
					
				@ -20,58 +20,117 @@ pub fn requires_login(message: &str, url: Uri) -> Flash<Redirect> {
 | 
				
			|||||||
    Flash::new(Redirect::to(format!("/login?m={}", gettext(message.to_string()))), "callback", url.to_string())
 | 
					    Flash::new(Redirect::to(format!("/login?m={}", gettext(message.to_string()))), "callback", url.to_string())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Returns (HTML, mentions)
 | 
					#[derive(Debug)]
 | 
				
			||||||
pub fn md_to_html(md: &str) -> (String, Vec<String>) {
 | 
					enum State {
 | 
				
			||||||
 | 
					    Mention,
 | 
				
			||||||
 | 
					    Hashtag,
 | 
				
			||||||
 | 
					    Word,
 | 
				
			||||||
 | 
					    Ready,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns (HTML, mentions, hashtags)
 | 
				
			||||||
 | 
					pub fn md_to_html(md: &str) -> (String, Vec<String>, Vec<String>) {
 | 
				
			||||||
    let parser = Parser::new_ext(md, Options::all());
 | 
					    let parser = Parser::new_ext(md, Options::all());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let (parser, mentions): (Vec<Vec<Event>>, Vec<Vec<String>>) = parser.map(|evt| match evt {
 | 
					    let (parser, mentions, hashtags): (Vec<Vec<Event>>, Vec<Vec<String>>, Vec<Vec<String>>) = parser.map(|evt| match evt {
 | 
				
			||||||
        Event::Text(txt) => {
 | 
					        Event::Text(txt) => {
 | 
				
			||||||
            let (evts, _, _, _, new_mentions) = txt.chars().fold((vec![], false, String::new(), 0, vec![]), |(mut events, in_mention, text_acc, n, mut mentions), c| {
 | 
					            let (evts, _, _, _, new_mentions, new_hashtags) = txt.chars().fold((vec![], State::Ready, String::new(), 0, vec![], vec![]), |(mut events, state, text_acc, n, mut mentions, mut hashtags), c| {
 | 
				
			||||||
                if in_mention {
 | 
					                match state {
 | 
				
			||||||
                    let char_matches = c.is_alphanumeric() || c == '@' || c == '.' || c == '-' || c == '_';
 | 
					                    State::Mention => {
 | 
				
			||||||
                    if char_matches && (n < (txt.chars().count() - 1)) {
 | 
					                        let char_matches = c.is_alphanumeric() || c == '@' || c == '.' || c == '-' || c == '_';
 | 
				
			||||||
                        (events, in_mention, text_acc + c.to_string().as_ref(), n + 1, mentions)
 | 
					                        if char_matches && (n < (txt.chars().count() - 1)) {
 | 
				
			||||||
                    } else {
 | 
					                            (events, State::Mention, text_acc + c.to_string().as_ref(), n + 1, mentions, hashtags)
 | 
				
			||||||
                        let mention = if char_matches {
 | 
					 | 
				
			||||||
                            text_acc + c.to_string().as_ref()
 | 
					 | 
				
			||||||
                        } else {
 | 
					                        } else {
 | 
				
			||||||
                            text_acc
 | 
					                            let mention = if char_matches {
 | 
				
			||||||
                        };
 | 
					                                text_acc + c.to_string().as_ref()
 | 
				
			||||||
                        let short_mention = mention.clone();
 | 
					                            } else {
 | 
				
			||||||
                        let short_mention = short_mention.splitn(1, '@').nth(0).unwrap_or("");
 | 
					                                text_acc
 | 
				
			||||||
                        let link = Tag::Link(format!("/@/{}/", mention).into(), short_mention.to_string().into());
 | 
					                            };
 | 
				
			||||||
 | 
					                            let short_mention = mention.clone();
 | 
				
			||||||
 | 
					                            let short_mention = short_mention.splitn(1, '@').nth(0).unwrap_or("");
 | 
				
			||||||
 | 
					                            let link = Tag::Link(format!("/@/{}/", mention).into(), short_mention.to_string().into());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        mentions.push(mention);
 | 
					                            mentions.push(mention);
 | 
				
			||||||
                        events.push(Event::Start(link.clone()));
 | 
					                            events.push(Event::Start(link.clone()));
 | 
				
			||||||
                        events.push(Event::Text(format!("@{}", short_mention).into()));
 | 
					                            events.push(Event::Text(format!("@{}", short_mention).into()));
 | 
				
			||||||
                        events.push(Event::End(link));
 | 
					                            events.push(Event::End(link));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        (events, false, c.to_string(), n + 1, mentions)
 | 
					                            (events, State::Ready, c.to_string(), n + 1, mentions, hashtags)
 | 
				
			||||||
                    }
 | 
					                        }
 | 
				
			||||||
                } else {
 | 
					                    }
 | 
				
			||||||
                    if c == '@' {
 | 
					                    State::Hashtag => {
 | 
				
			||||||
                        events.push(Event::Text(text_acc.into()));
 | 
					                        let char_matches = c.is_alphanumeric();
 | 
				
			||||||
                        (events, true, String::new(), n + 1, mentions)
 | 
					                        if char_matches && (n < (txt.chars().count() -1)) {
 | 
				
			||||||
                    } else {
 | 
					                            (events, State::Hashtag, text_acc + c.to_string().as_ref(), n+1, mentions, hashtags)
 | 
				
			||||||
                        if n >= (txt.chars().count() - 1) { // Add the text after at the end, even if it is not followed by a mention.
 | 
					                        } else {
 | 
				
			||||||
                            events.push(Event::Text((text_acc.clone() + c.to_string().as_ref()).into()))
 | 
					                            let hashtag = if char_matches {
 | 
				
			||||||
 | 
					                                text_acc + c.to_string().as_ref()
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                text_acc
 | 
				
			||||||
 | 
					                            };
 | 
				
			||||||
 | 
					                            let link = Tag::Link(format!("/tag/{}", hashtag).into(), hashtag.to_string().into());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            hashtags.push(hashtag.clone());
 | 
				
			||||||
 | 
					                            events.push(Event::Start(link.clone()));
 | 
				
			||||||
 | 
					                            events.push(Event::Text(format!("#{}", hashtag).into()));
 | 
				
			||||||
 | 
					                            events.push(Event::End(link));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            (events, State::Ready, c.to_string(), n + 1, mentions, hashtags)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    State::Ready => {
 | 
				
			||||||
 | 
					                        if c == '@' {
 | 
				
			||||||
 | 
					                            events.push(Event::Text(text_acc.into()));
 | 
				
			||||||
 | 
					                            (events, State::Mention, String::new(), n + 1, mentions, hashtags)
 | 
				
			||||||
 | 
					                        } else if c == '#' {
 | 
				
			||||||
 | 
					                            events.push(Event::Text(text_acc.into()));
 | 
				
			||||||
 | 
					                            (events, State::Hashtag, String::new(), n + 1, mentions, hashtags)
 | 
				
			||||||
 | 
					                        } else if c.is_alphanumeric() {
 | 
				
			||||||
 | 
					                            if n >= (txt.chars().count() - 1) { // Add the text after at the end, even if it is not followed by a mention.
 | 
				
			||||||
 | 
					                                events.push(Event::Text((text_acc.clone() + c.to_string().as_ref()).into()))
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            (events, State::Word, text_acc + c.to_string().as_ref(), n + 1, mentions, hashtags)
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            if n >= (txt.chars().count() - 1) { // Add the text after at the end, even if it is not followed by a mention.
 | 
				
			||||||
 | 
					                                events.push(Event::Text((text_acc.clone() + c.to_string().as_ref()).into()))
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            (events, State::Ready, text_acc + c.to_string().as_ref(), n + 1, mentions, hashtags)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    State::Word => {
 | 
				
			||||||
 | 
					                        if c.is_alphanumeric() {
 | 
				
			||||||
 | 
					                            if n >= (txt.chars().count() - 1) { // Add the text after at the end, even if it is not followed by a mention.
 | 
				
			||||||
 | 
					                                events.push(Event::Text((text_acc.clone() + c.to_string().as_ref()).into()))
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            (events, State::Word, text_acc + c.to_string().as_ref(), n + 1, mentions, hashtags)
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            if n >= (txt.chars().count() - 1) { // Add the text after at the end, even if it is not followed by a mention.
 | 
				
			||||||
 | 
					                                events.push(Event::Text((text_acc.clone() + c.to_string().as_ref()).into()))
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            (events, State::Ready, text_acc + c.to_string().as_ref(), n + 1, mentions, hashtags)
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        (events, in_mention, text_acc + c.to_string().as_ref(), n + 1, mentions)
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            (evts, new_mentions)
 | 
					            (evts, new_mentions, new_hashtags)
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        _ => (vec![evt], vec![])
 | 
					        _ => (vec![evt], vec![], vec![])
 | 
				
			||||||
    }).unzip();
 | 
					    }).fold((vec![],vec![],vec![]), |(mut parser, mut mention, mut hashtag), (p, m, h)| {
 | 
				
			||||||
 | 
					        parser.push(p);
 | 
				
			||||||
 | 
					        mention.push(m);
 | 
				
			||||||
 | 
					        hashtag.push(h);
 | 
				
			||||||
 | 
					        (parser, mention, hashtag)
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    let parser = parser.into_iter().flatten();
 | 
					    let parser = parser.into_iter().flatten();
 | 
				
			||||||
    let mentions = mentions.into_iter().flatten().map(|m| String::from(m.trim()));
 | 
					    let mentions = mentions.into_iter().flatten().map(|m| String::from(m.trim()));
 | 
				
			||||||
 | 
					    let hashtags = hashtags.into_iter().flatten().map(|h| String::from(h.trim()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TODO: fetch mentionned profiles in background, if needed
 | 
					    // TODO: fetch mentionned profiles in background, if needed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut buf = String::new();
 | 
					    let mut buf = String::new();
 | 
				
			||||||
    html::push_html(&mut buf, parser);
 | 
					    html::push_html(&mut buf, parser);
 | 
				
			||||||
    (buf, mentions.collect())
 | 
					    let hashtags = hashtags.collect();
 | 
				
			||||||
 | 
					    (buf, mentions.collect(), hashtags)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[cfg(test)]
 | 
					#[cfg(test)]
 | 
				
			||||||
@ -90,10 +149,30 @@ mod tests {
 | 
				
			|||||||
            ("between parenthesis (@test)", vec!["test"]),
 | 
					            ("between parenthesis (@test)", vec!["test"]),
 | 
				
			||||||
            ("with some punctuation @test!", vec!["test"]),
 | 
					            ("with some punctuation @test!", vec!["test"]),
 | 
				
			||||||
            ("      @spaces     ", vec!["spaces"]),
 | 
					            ("      @spaces     ", vec!["spaces"]),
 | 
				
			||||||
 | 
					            ("not_a@mention", vec![]),
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (md, mentions) in tests {
 | 
					        for (md, mentions) in tests {
 | 
				
			||||||
            assert_eq!(md_to_html(md).1, mentions.into_iter().map(|s| s.to_string()).collect::<Vec<String>>());
 | 
					            assert_eq!(md_to_html(md).1, mentions.into_iter().map(|s| s.to_string()).collect::<Vec<String>>());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn test_hashtags() {
 | 
				
			||||||
 | 
					        let tests = vec![
 | 
				
			||||||
 | 
					            ("nothing", vec![]),
 | 
				
			||||||
 | 
					            ("#hashtag", vec!["hashtag"]),
 | 
				
			||||||
 | 
					            ("#many #hashtags", vec!["many", "hashtags"]),
 | 
				
			||||||
 | 
					            ("#start with a hashtag", vec!["start"]),
 | 
				
			||||||
 | 
					            ("hashtag at #end", vec!["end"]),
 | 
				
			||||||
 | 
					            ("between parenthesis (#test)", vec!["test"]),
 | 
				
			||||||
 | 
					            ("with some punctuation #test!", vec!["test"]),
 | 
				
			||||||
 | 
					            ("      #spaces     ", vec!["spaces"]),
 | 
				
			||||||
 | 
					            ("not_a#hashtag", vec![]),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (md, mentions) in tests {
 | 
				
			||||||
 | 
					            assert_eq!(md_to_html(md).2, mentions.into_iter().map(|s| s.to_string()).collect::<Vec<String>>());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -100,7 +100,7 @@ impl Comment {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn into_activity(&self, conn: &Connection) -> Note {
 | 
					    pub fn into_activity(&self, conn: &Connection) -> Note {
 | 
				
			||||||
        let (html, mentions) = utils::md_to_html(self.content.get().as_ref());
 | 
					        let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let author = User::get(conn, self.author_id).expect("Comment::into_activity: author error");
 | 
					        let author = User::get(conn, self.author_id).expect("Comment::into_activity: author error");
 | 
				
			||||||
        let mut note = Note::default();
 | 
					        let mut note = Note::default();
 | 
				
			||||||
 | 
				
			|||||||
@ -117,8 +117,8 @@ impl Instance {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn update(&self, conn: &Connection, name: String, open_registrations: bool, short_description: SafeString, long_description: SafeString) {
 | 
					    pub fn update(&self, conn: &Connection, name: String, open_registrations: bool, short_description: SafeString, long_description: SafeString) {
 | 
				
			||||||
        let (sd, _) = md_to_html(short_description.as_ref());
 | 
					        let (sd, _, _) = md_to_html(short_description.as_ref());
 | 
				
			||||||
        let (ld, _) = md_to_html(long_description.as_ref());
 | 
					        let (ld, _, _) = md_to_html(long_description.as_ref());
 | 
				
			||||||
        diesel::update(self)
 | 
					        diesel::update(self)
 | 
				
			||||||
            .set((
 | 
					            .set((
 | 
				
			||||||
                instances::name.eq(name),
 | 
					                instances::name.eq(name),
 | 
				
			||||||
 | 
				
			|||||||
@ -35,7 +35,7 @@ fn create(blog_name: String, slug: String, data: LenientForm<NewCommentForm>, us
 | 
				
			|||||||
    let form = data.get();
 | 
					    let form = data.get();
 | 
				
			||||||
    form.validate()
 | 
					    form.validate()
 | 
				
			||||||
        .map(|_| {
 | 
					        .map(|_| {
 | 
				
			||||||
            let (html, mentions) = utils::md_to_html(form.content.as_ref());
 | 
					            let (html, mentions, _hashtags) = utils::md_to_html(form.content.as_ref());
 | 
				
			||||||
            let comm = Comment::insert(&*conn, NewComment {
 | 
					            let comm = Comment::insert(&*conn, NewComment {
 | 
				
			||||||
                content: SafeString::new(html.as_ref()),
 | 
					                content: SafeString::new(html.as_ref()),
 | 
				
			||||||
                in_response_to_id: form.responding_to.clone(),
 | 
					                in_response_to_id: form.responding_to.clone(),
 | 
				
			||||||
 | 
				
			|||||||
@ -183,7 +183,7 @@ fn update(blog: String, slug: String, user: User, conn: DbConn, data: LenientFor
 | 
				
			|||||||
            // actually it's not "Ok"…
 | 
					            // actually it's not "Ok"…
 | 
				
			||||||
            Ok(Redirect::to(uri!(super::blogs::details: name = blog)))
 | 
					            Ok(Redirect::to(uri!(super::blogs::details: name = blog)))
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            let (content, mentions) = utils::md_to_html(form.content.to_string().as_ref());
 | 
					            let (content, mentions, _hashtag) = utils::md_to_html(form.content.to_string().as_ref());//TODO do something with hashtags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let license = if form.license.len() > 0 {
 | 
					            let license = if form.license.len() > 0 {
 | 
				
			||||||
                form.license.to_string()
 | 
					                form.license.to_string()
 | 
				
			||||||
@ -294,7 +294,7 @@ fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: D
 | 
				
			|||||||
            // actually it's not "Ok"…
 | 
					            // actually it's not "Ok"…
 | 
				
			||||||
            Ok(Redirect::to(uri!(super::blogs::details: name = blog_name)))
 | 
					            Ok(Redirect::to(uri!(super::blogs::details: name = blog_name)))
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            let (content, mentions) = utils::md_to_html(form.content.to_string().as_ref());
 | 
					            let (content, mentions, _hashtag) = utils::md_to_html(form.content.to_string().as_ref());//TODO do something with hashtags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let post = Post::insert(&*conn, NewPost {
 | 
					            let post = Post::insert(&*conn, NewPost {
 | 
				
			||||||
                blog_id: blog.id,
 | 
					                blog_id: blog.id,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user