Skip to content
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

Add Labelled hashtag #69

Open
wants to merge 7 commits into
base: master
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
8 changes: 8 additions & 0 deletions spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- [Delimited Email addresses: `<[email protected]>`](#delimited-email-addresses)
- [Delimited Links: `<http://example.org>`](#delimited-links)
- [Labeled Links: `[Name](url)`](#labled-links)
- [Labeled hashtags: `[Tag][#tag]`](#labeled-tags)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we also want to have this in the desktop set?


## Text Enhancements

Expand Down Expand Up @@ -182,6 +183,13 @@ Optionally, a client can implement a system to trust a domain (a "don't ask agai

URL parsing allows all valid URLs, no restrictions on schemes, no whitelist is needed, because the format already specifies that it is a link.


<a name="labeled-tags" />

### Labelled hashtags

The idea is to have hashtags but labelled with an alternative text. This feature is very unique and less seen in other IMs.

Copy link
Member

@Simon-Laux Simon-Laux May 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please also make an example and spec to define how it looks like:

[Label](#tag)

and say what it does: "clicking on it opens the search with that hashtag like for the normal hashtag but looks like a link", or sth along those lines

## Ideas For The Future:

### `:emoji:`
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::io::{self, Read, Write};

use parser::parse_markdown_text;
use parser::parse_only_text;
#[allow(dead_code)]
mod parser;
extern crate nom;
Expand All @@ -13,7 +13,7 @@ fn main() -> io::Result<()> {

//println!("input: {:?}", buffer);

let output = parse_markdown_text(&buffer);
let output = parse_only_text(&buffer);

io::stdout().write_all(format!("output: {:?}", output).as_bytes())?;

Expand Down
5 changes: 5 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ pub enum Element<'a> {
Text(&'a str),
/// #hashtag
Tag(&'a str),
/// [label](#tag)
LabelledTag {
label: Vec<Element<'a>>,
tag: &'a str,
},
/// Represents a linebreak - \n
Linebreak,
Link {
Expand Down
177 changes: 35 additions & 142 deletions src/parser/parse_from_text/text_elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ use nom::{
streaming::take_till1,
},
character::complete::char,
combinator::{peek, recognize, verify},
sequence::tuple,
combinator::{consumed, peek, recognize, verify},
sequence::{delimited, tuple},
AsChar, IResult, Offset, Slice,
};

use super::base_parsers::CustomError;
use super::{base_parsers::CustomError, parse_only_text};

fn linebreak(input: &str) -> IResult<&str, char, CustomError<&str>> {
char('\n')(input)
Expand Down Expand Up @@ -89,143 +89,6 @@ pub(crate) fn email_address(input: &str) -> IResult<&str, Element, CustomError<&
}
}

/*
fn not_link_part_char(c: char) -> bool {
!matches!(c, ':' | '\n' | '\r' | '\t' | ' ')
}

fn link(input: &str) -> IResult<&str, (), CustomError<&str>> {
let (input, _) = take_while1(link_scheme)(input)?;
}

/// rough recognition of an link, results gets checked by a real link parser
fn link_intern(input: &str) -> IResult<&str, (), CustomError<&str>> {
let (input, _) = take_while1(not_link_part_char)(input)?;
let (input, _) = tag(":")(input)?;
let i = <&str>::clone(&input);
let (remaining, consumed) = take_while1(is_not_white_space)(i)?;

let mut parentheses_count = 0usize; // ()
let mut curly_brackets_count = 0usize; // {}
let mut brackets_count = 0usize; // []
let mut angle_brackets = 0usize; // <>

let mut alternative_offset = None;
for (i, char) in consumed.chars().enumerate() {
match char {
'(' => {
parentheses_count = parentheses_count.saturating_add(1);
// if there is no closing bracket in the link, then don't take the bracket as a part of the link
if (<&str>::clone(&consumed)).slice(i..).find(')').is_none() {
alternative_offset = Some(i);
break;
}
}
'{' => {
curly_brackets_count = curly_brackets_count.saturating_add(1);
// if there is no closing bracket in the link, then don't take the bracket as a part of the link
if (<&str>::clone(&consumed)).slice(i..).find('}').is_none() {
alternative_offset = Some(i);
break;
}
}
'[' => {
brackets_count = brackets_count.saturating_add(1);
// if there is no closing bracket in the link, then don't take the bracket as a part of the link
if (<&str>::clone(&consumed)).slice(i..).find(']').is_none() {
alternative_offset = Some(i);
break;
}
}
'<' => {
angle_brackets = angle_brackets.saturating_add(1);
// if there is no closing bracket in the link, then don't take the bracket as a part of the link
if (<&str>::clone(&consumed)).slice(i..).find('>').is_none() {
alternative_offset = Some(i);
break;
}
}
')' => {
if parentheses_count == 0 {
alternative_offset = Some(i);
break;
} else {
parentheses_count = parentheses_count.saturating_sub(1);
}
}
'}' => {
if curly_brackets_count == 0 {
alternative_offset = Some(i);
break;
} else {
curly_brackets_count = curly_brackets_count.saturating_sub(1);
}
}
']' => {
if brackets_count == 0 {
alternative_offset = Some(i);
break;
} else {
brackets_count = brackets_count.saturating_sub(1);
}
}
'>' => {
if angle_brackets == 0 {
alternative_offset = Some(i);
break;
} else {
angle_brackets = angle_brackets.saturating_sub(1);
}
}
_ => continue,
}
}

if let Some(offset) = alternative_offset {
let remaining = input.slice(offset..);
Ok((remaining, ()))
} else {
Ok((remaining, ()))
}
}

pub(crate) fn link(input: &str) -> IResult<&str, Element, CustomError<&str>> {
// basically
//let (input, content) = recognize(link_intern)(input)?;
// but don't eat the last char if it is one of these: `.,;:`
let i = <&str>::clone(&input);
let i2 = <&str>::clone(&input);
let i3 = <&str>::clone(&input);
let (input, content) = match link_intern(i) {
Ok((remaining, _)) => {
let index = i2.offset(remaining);
let consumed = i2.slice(..index);
match consumed.chars().last() {
Some(c) => match c {
'.' | ',' | ':' | ';' => {
let index = input.offset(remaining).saturating_sub(1);
let consumed = i3.slice(..index);
let remaining = input.slice(index..);
Ok((remaining, consumed))
}
_ => Ok((remaining, consumed)),
},
_ => Ok((remaining, consumed)),
}
}
Err(e) => Err(e),
}?;

// check if result is valid link
let (remainder, destination) = LinkDestination::parse_standalone_with_whitelist(content)?;

if remainder.is_empty() {
Ok((input, Element::Link { destination }))
} else {
Err(nom::Err::Error(CustomError::InvalidLink))
}
}
*/
fn is_allowed_bot_cmd_suggestion_char(char: char) -> bool {
match char {
'@' | '\\' | '_' | '.' | '-' | '/' => true,
Expand Down Expand Up @@ -253,6 +116,32 @@ fn bot_command_suggestion(input: &str) -> IResult<&str, Element, CustomError<&st
}
}

fn labelled_tag(input: &str) -> IResult<&str, Element, CustomError<&str>> {
let (input, label) = delimited(
char('['),
take_while1(|c| !matches!(c, '[' | ']')),
char(']'),
)(input)?;
let elements: Vec<Element> = parse_only_text(label);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we merge #74 first, then we can allow markdown formatting for the thing in markdown mode and in desktop mode this should just allow text. So we don't repeat #59 (there should no be any clickable elements in the label, otherwise it's confusing/not-good-defined what happens)

let (input, tag) = delimited(
char('('),
take_while1(|c| !matches!(c, '(' | ')')),
char(')'),
)(input)?;
let (_, (consumed, _output)) = consumed(hashtag)(tag)?;
if consumed == tag {
Ok((
input,
Element::LabelledTag {
label: elements,
tag: consumed,
},
))
} else {
Err(nom::Err::Error(CustomError::UnexpectedContent))
}
}

pub(crate) fn parse_text_element(
input: &str,
prev_char: Option<char>,
Expand All @@ -263,8 +152,12 @@ pub(crate) fn parse_text_element(
//
// Also as this is the text element parser,
// text elements parsers MUST NOT call the parser for markdown elements internally

if let Ok((i, elm)) = hashtag(input) {
{
//println!("{:?}", labelled_tag(input));
}
if let Ok((i, elm)) = labelled_tag(input) {
Ok((i, elm))
Comment on lines +158 to +159
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fabled tag should only be in the markdown set and maybe also in the desktop set, but not in the text set.
rule of thumb: Things in the text set are kept at the original, just linkified, but text is still the same

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You sure? hashtag is already in text elements.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, labeled link and labeled hashtag both modify the text, while text elements only make the text clickable

} else if let Ok((i, elm)) = hashtag(input) {
Ok((i, elm))
} else if let Ok((i, elm)) = {
if prev_char == Some(' ') || prev_char.is_none() {
Expand Down
63 changes: 0 additions & 63 deletions tests/text_to_ast/mod.rs.orig

This file was deleted.

29 changes: 29 additions & 0 deletions tests/text_to_ast/text_only.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,35 @@ fn persian_hashtag_with_underline() {
);
}

#[test]
fn labelled_hashtag() {
let input = "[Hello](#hello) #world";

assert_eq!(
parse_only_text(input),
vec![
LabelledTag {
label: vec![Text("Hello")],
tag: "#hello",
},
Text(" "),
Tag("#world"),
]
);

let input_bold = "[**Hello**](#hi) ";
assert_eq!(
parse_only_text(input_bold),
vec![
LabelledTag {
label: vec![Text("**Hello**")],
tag: "#hi",
},
Text(" ")
]
);
}

#[test]
fn email_address_standalone() {
let test_cases = vec![
Expand Down
Loading